90.子集II

目标

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

说明:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10

思路

返回数组不重复的子序列。

枚举子序列可以使用迭代或者回溯。迭代的思路是使用 bitmap 表示对应下标是否在子序列中,如果数组中元素个数为 nbitmap 值为 1 << n。回溯的思路是当前元素选或者不选,或者从当前元素到结尾下一个元素选哪一个。

题目的关键是如何判断收集到的子序列是否重复,这里的重复指组成子序列的元素与个数完全相同。

很容易想到使用序列的字符串来判断是否重复,但是如何处理组成元素相同而顺序不同的情况?可以对原数组排序,这样可以保证相同元素在子序列中的出现位置相同。比如 4, 4, 4, 1, 4,排序后 4 只会出现在 1 的后面,如果子序列中 4 的个数相同,那么子序列字符串也相同,这样就可以去重了。

如果使用回溯,则可以在枚举时直接去重。如果不选择某个元素,那么后面 相同的 元素都不选。这样可以保证相同元素同样的出现次数只枚举一次。

也有网友使用桶排序,回溯时对相同元素枚举取 0 ~ k 个。

代码


/**
 * @date 2025-02-05 9:09
 */
public class SubsetsWithDup90 {

    public List<List<Integer>> subsetsWithDup_v1(int[] nums) {
        int n = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        Set<String> set = new HashSet<>();
        Arrays.sort(nums);
        n = 1 << n;
        for (int i = 0; i < n; i++) {
            List<Integer> subset = new ArrayList<>();
            for (int j = 0; j < nums.length; j++) {
                if (((i >> j) & 1) == 1) {
                    subset.add(nums[j]);
                }
            }
            if (!set.contains(subset.toString())) {
                res.add(subset);
            }
            set.add(subset.toString());
        }
        return res;
    }

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        List<Integer> subset = new ArrayList<>();
        dfs(0, nums, subset, res);
        return res;
    }

    public void dfs(int index, int[] nums, List<Integer> subset, List<List<Integer>> res) {
        if (index == nums.length) {
            res.add(new ArrayList<>(subset));
            return;
        }
        subset.add(nums[index]);
        dfs(index + 1, nums, subset, res);
        subset.remove(subset.size() - 1);
        index++;
        while (index < nums.length && nums[index] == nums[index - 1]){
            index++;
        }
        dfs(index, nums, subset, res);
    }

}

性能

bitmap 迭代

回溯

81.搜索旋转排序数组II

目标

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

你必须尽可能减少整个操作步骤。

示例 1:

输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true

示例 2:

输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false

说明:

  • 1 <= nums.length <= 5000
  • -10^4 <= nums[i] <= 10^4
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -10^4 <= target <= 10^4

进阶:

此题与 33.搜索旋转排序数组 相似,但本题中的 nums 可能包含 重复 元素。这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?

思路

有序数组 划分为两个子数组,交换子数组的顺序得到旋转数组 nums,判断旋转数组中是否存在 target

根据题意可知,原来的有序数组被划分成两部分,第一部分的所有元素均大于 等于 第二部分的所有元素。

此题与 33.搜索旋转排序数组 相似,但本题中的 nums 可能包含 重复 元素。如果当前元素恰好与切点的元素值相同,那么我们无法判断该搜索哪一部分。

例如,如果当前元素值大于 target,但是恰好与第一个元素相同:

  • 如果当前元素就是位于第一部分,那么我们应该将 left 左移,以便到达第二部分搜索。
  • 如果当前元素已经位于第二部分,那么我们应该将 right 右移,以便找到更小的target。

最简单的处理方法是缩小右边界,因为这种情况只会出现在切点刚好在连续相同元素中间,这时最左侧的元素一定与最右侧的元素相同,我们可以提前将 right 移到第一个与 left 不同的位置。这样当前元素属于哪一部分就确定了,不会影响判断。

代码


/**
 * @date 2025-02-01 14:33
 */
public class Search81 {

    public boolean search(int[] nums, int target) {
        int n = nums.length;
        int left = 0, right = n - 1;
        while (right > 0 && nums[left] == nums[right]) {
            right--;
        }
        int mid = left + (right - left) / 2;
        while (left <= right) {
            if (check(nums[mid], nums[0], target)) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
            mid = left + (right - left) / 2;
        }
        if (left >= 0 && left < n) {
            return nums[left] == target;
        }
        return false;
    }

    public boolean check(int midValue, int firstValue, int target) {
        if (target < firstValue) {
            if (midValue >= firstValue) {
                return true;
            } else {
                return midValue < target;
            }
        } else {
            if (midValue >= firstValue) {
                return midValue < target;
            } else {
                return false;
            }
        }
    }

}

性能

45.跳跃游戏II

目标

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

示例 2:

输入: nums = [2,3,0,1,4]
输出: 2

说明:

  • 1 <= nums.length <= 10^4
  • 0 <= nums[i] <= 1000
  • 题目保证可以到达 nums[n-1]

思路

数组 nums 的元素表示可以向后跳跃的最大长度,求从 0 跳到 n - 1 所需的最小跳跃次数。

定义 dp[i] 表示到达下标 i 所需的最小跳跃次数,状态转移方程为 dp[i + k] = min(dp[i] + 1),其中 k ∈ [1, nums[i]]

代码


/**
 * @date 2024-03-07 17:14
 */
public class CanJumpII45 {

    public int jump(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        Arrays.fill(dp, 0x3f3f3f);
        dp[0] = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n && j <= i + nums[i]; j++) {
                dp[j] = Math.min(dp[j], dp[i] + 1);
            }
        }
        return dp[n - 1];
    }

}

性能

40.组合总和II

目标

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

说明:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

思路

有一个数组 candidates,用 candidates 中的元素值组成目标值 target,返回每种组合的元素列表。数组中的每个元素在每种组合中只能使用一次。

考虑是否选择某个元素,可能的组合有 2^n 种。但是题目有条件限制,要求组合元素值之和等于target,那么如果组合的和大于 target 可以提前返回。因此可以将 candidates 从小到大排序,回溯。关键是如何去除重复组合,对于相同的元素,只关心取的个数,因此如果不选某个元素,后续相同的元素也都不能选。

代码


/**
 * @date 2025-01-26 15:24
 */
public class CombinationSum40 {

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        dfs(0, 0, target, candidates, path, res);
        return res;
    }

    public void dfs(int index, int sum, int target, int[] candidates, List<Integer> path, List<List<Integer>> res) {
        if (sum == target) {
            List<Integer> tmp = new ArrayList<>(path);
            res.add(tmp);
            return;
        }
        if (index == candidates.length || sum > target) {
            return;
        }
        int cur = candidates[index];
        path.add(cur);
        dfs(index + 1, sum + cur, target, candidates, path, res);
        path.remove(path.size() - 1);
        index++;
        // 如果不选当前的值,也同样不选后续所有相同的值
        // 对于相同的值,只考虑选了几个,如果这里不跳过,就会出现重复
        // 比如有4个相同的值,选 选 不选 不选,如果允许后续再次选择,就会出现
        // 选 不选 不选 选、
        // 选 不选 选 不选
        // 不选 选 选 不选、
        // 不选 选 不选 选、
        // 不选 不选 选 选、
        while (index < candidates.length && cur == candidates[index]) {
            index++;
        }
        dfs(index, sum, target, candidates, path, res);
    }
}

性能

2944.购买水果需要的最少金币数

目标

给你一个 下标从 1 开始的 整数数组 prices ,其中 prices[i] 表示你购买第 i 个水果需要花费的金币数目。

水果超市有如下促销活动:

  • 如果你花费 prices[i] 购买了下标为 i 的水果,那么你可以免费获得下标范围在 [i + 1, i + i + 1] 的水果。

注意 ,即使你 可以 免费获得水果 j ,你仍然可以花费 prices[j] 个金币去购买它以获得它的奖励。

请你返回获得所有水果所需要的 最少 金币数。

示例 1:

输入:prices = [3,1,2]
输出:4
解释:
用 prices[0] = 3 个金币购买第 1 个水果,你可以免费获得第 2 个水果。
用 prices[1] = 1 个金币购买第 2 个水果,你可以免费获得第 3 个水果。
免费获得第 3 个水果。
请注意,即使您可以免费获得第 2 个水果作为购买第 1 个水果的奖励,但您购买它是为了获得其奖励,这是更优化的。

示例 2:

输入:prices = [1,10,1,1]
输出:2
解释:
用 prices[0] = 1 个金币购买第 1 个水果,你可以免费获得第 2 个水果。
免费获得第 2 个水果。
用 prices[2] = 1 个金币购买第 3 个水果,你可以免费获得第 4 个水果。
免费获得第 4 个水果。

示例 3:

输入:prices = [26,18,6,12,49,7,45,45]
输出:39
解释:
用 prices[0] = 26 个金币购买第 1 个水果,你可以免费获得第 2 个水果。
免费获得第 2 个水果。
用 prices[2] = 6 个金币购买第 3 个水果,你可以免费获得第 4,5,6(接下来的三个)水果。
免费获得第 4 个水果。
免费获得第 5 个水果。
用 prices[5] = 7 个金币购买第 6 个水果,你可以免费获得第 7 和 第 8 个水果。
免费获得第 7 个水果。
免费获得第 8 个水果。
请注意,即使您可以免费获得第 6 个水果作为购买第 3 个水果的奖励,但您购买它是为了获得其奖励,这是更优化的。

说明:

1 <= prices.length <= 1000
1 <= prices[i] <= 10^5

思路

有 n 个水果,其价格由 prices 表示,当我们以 prices[i] 枚金币购买了第 i + 1 个苹果时,我们可以免费获得下标 [i + 1, i + i + 1]所有 个苹果(当然也可以购买以获得后面的奖励),求获得全部苹果所需的最少硬币。

最直接的想法是记忆化搜索。

代码


/**
 * @date 2025-01-24 9:07
 */
public class MinimumCoins2944 {

    public int minimumCoins(int[] prices) {
        int[] mem = new int[2 * prices.length + 3];
        Arrays.fill(mem, Integer.MAX_VALUE);
        return dfs(0, prices, mem, 0);
    }

    public int dfs(int index, int[] prices, int[] mem, int cost) {
        int n = prices.length;
        if (index >= n) {
            return cost;
        }
        int res = cost + prices[index];
        int next = index * 2 + 2;
        if (mem[next] == Integer.MAX_VALUE) {
            mem[next] = dfs(next, prices, mem, 0);
        }
        int remainder = mem[next];
        if (remainder == 0) {
            return res;
        }
        for (int i = index + 1; i < n && i <= index * 2 + 1; i++) {
            if (mem[i] == Integer.MAX_VALUE) {
                mem[i] = dfs(i, prices, mem, 0);
            }
            remainder = Math.min(mem[i], remainder);
        }
        return res + remainder;
    }

}

性能

1561.你可以获得的最大硬币数目

目标

有 3n 堆数目不一的硬币,你和你的朋友们打算按以下方式分硬币:

  • 每一轮中,你将会选出 任意 3 堆硬币(不一定连续)。
  • Alice 将会取走硬币数量最多的那一堆。
  • 你将会取走硬币数量第二多的那一堆。
  • Bob 将会取走最后一堆。
  • 重复这个过程,直到没有更多硬币。

给你一个整数数组 piles ,其中 piles[i] 是第 i 堆中硬币的数目。

返回你可以获得的最大硬币数目。

示例 1:

输入:piles = [2,4,1,2,7,8]
输出:9
解释:选出 (2, 7, 8) ,Alice 取走 8 枚硬币的那堆,你取走 7 枚硬币的那堆,Bob 取走最后一堆。
选出 (1, 2, 4) , Alice 取走 4 枚硬币的那堆,你取走 2 枚硬币的那堆,Bob 取走最后一堆。
你可以获得的最大硬币数目:7 + 2 = 9.
考虑另外一种情况,如果选出的是 (1, 2, 8) 和 (2, 4, 7) ,你就只能得到 2 + 4 = 6 枚硬币,这不是最优解。

示例 2:

输入:piles = [2,4,5]
输出:4

示例 3:

输入:piles = [9,8,7,6,5,1,2,3,4]
输出:18

说明:

  • 3 <= piles.length <= 10^5
  • piles.length % 3 == 0
  • 1 <= piles[i] <= 10^4

思路

3n 堆硬币,每次操作可以任选其中3堆,Alice 取走数量最多的那堆硬币,我们取走硬币数量第二多的,Bob 取走剩下的。求我们能获得的最大硬币数目。

为了使我们获取的硬币数量最多,Bob 每次只能取数量最少的那堆,将数量多的留下。根据这种贪心策略,我们可以将每堆硬币按个数从小到大排序,从倒数第二个开始每隔两个向前取,同时 Bob 从第一个往后挨个取,直到这两个指针相遇。

代码


/**
 * @date 2025-01-22 8:50
 */
public class MaxCoins1561 {

    public int maxCoins(int[] piles) {
        Arrays.sort(piles);
        int res = 0;
        int n = piles.length;
        int k = 0;
        for (int i = n - 2; i > k; i -= 2, k++) {
            res += piles[i];
        }
        return res;
    }
}

性能

2266.统计打字方案数

目标

Alice 在给 Bob 用手机打字。数字到字母的 对应 如下图所示。

为了 打出 一个字母,Alice 需要 按 对应字母 i 次,i 是该字母在这个按键上所处的位置。

  • 比方说,为了按出字母 's' ,Alice 需要按 '7' 四次。类似的, Alice 需要按 '5' 两次得到字母 'k' 。
  • 注意,数字 '0' 和 '1' 不映射到任何字母,所以 Alice 不 使用它们。

但是,由于传输的错误,Bob 没有收到 Alice 打字的字母信息,反而收到了 按键的字符串信息 。

  • 比方说,Alice 发出的信息为 "bob" ,Bob 将收到字符串 "2266622" 。

给你一个字符串 pressedKeys ,表示 Bob 收到的字符串,请你返回 Alice 总共可能发出多少种文字信息 。

由于答案可能很大,将它对 10^9 + 7 取余 后返回。

示例 1:

输入:pressedKeys = "22233"
输出:8
解释:
Alice 可能发出的文字信息包括:
"aaadd", "abdd", "badd", "cdd", "aaae", "abe", "bae" 和 "ce" 。
由于总共有 8 种可能的信息,所以我们返回 8 。

示例 2:

输入:pressedKeys = "222222222222222222222222222222222222"
输出:82876089
解释:
总共有 2082876103 种 Alice 可能发出的文字信息。
由于我们需要将答案对 109 + 7 取余,所以我们返回 2082876103 % (109 + 7) = 82876089 。

说明:

  • 1 <= pressedKeys.length <= 10^5
  • pressedKeys 只包含数字 '2' 到 '9' 。

思路

Alice 给 Bob 发短信,由于种种原因,Bob 接收到的是 Alice 的按键的数字字符串,求这些按键信息能够表示多少文字信息。由于一个数字按键可以表示多个字母,那么连续的相同数字就产生了多种可能的文字信息。2 3 4 5 6 8 可以表示 3 个字母,7 9 可以表示 4 个字母。

问题变成求解相同的连续数字可以表示的字母组合个数,或者将 n 个球放入盒子,每个盒子最多放 k 个球,至少放 1 个球,有多少种放法。

定义 dp[i] 表示将 i 个球放入盒子的方法,考虑最后一个盒子我们可以放 1 ~ k 个球,那么当 k = 3k = 4 时:

  • dp3[i] = ((dp3[i - 1] + dp3[i - 2]) % MOD + dp3[i - 3] % MOD) % MOD;
  • dp4[i] = ((dp4[i - 1] + dp4[i - 2]) % MOD + (dp4[i - 3] + dp4[i - 4]) % MOD) % MOD;

遍历字符串,找到连续的字符个数,使用乘法原理计算即可。

代码


/**
 * @date 2025-01-19 2:21
 */
public class CountTexts2266 {

    static int[] dp3 = new int[100001];
    static int[] dp4 = new int[100001];
    static int MOD = 1000000007;
    static Map<Character, int[]> map = new HashMap<>();

    static {
        dp3[1] = 1;
        dp3[2] = 2;
        dp3[3] = 4;
        dp3[4] = 7;
        dp4[1] = 1;
        dp4[2] = 2;
        dp4[3] = 4;
        dp4[4] = 8;
        for (int i = 5; i < 100001; i++) {
            dp3[i] = ((dp3[i - 1] + dp3[i - 2]) % MOD + dp3[i - 3] % MOD) % MOD;
            dp4[i] = ((dp4[i - 1] + dp4[i - 2]) % MOD + (dp4[i - 3] + dp4[i - 4]) % MOD) % MOD;
        }
        map.put('2', dp3);
        map.put('3', dp3);
        map.put('4', dp3);
        map.put('5', dp3);
        map.put('6', dp3);
        map.put('8', dp3);
        map.put('7', dp4);
        map.put('9', dp4);
    }

    public int countTexts(String pressedKeys) {
        char[] chars = pressedKeys.toCharArray();
        char prev = chars[0];
        int cnt = 1;
        long res = 1;
        for (int i = 1; i < chars.length; i++) {
            if (prev == chars[i]) {
                cnt++;
            } else {
                res = res * map.get(prev)[cnt] % MOD;
                prev = chars[i];
                cnt = 1;
            }
        }
        if (cnt > 1){
            res = res * map.get(prev)[cnt] % MOD;
        }
        return (int) res;
    }

}

性能

3097.或值至少为K的最短子数组II

目标

给你一个 非负 整数数组 nums 和一个整数 k 。

如果一个数组中所有元素的按位或运算 OR 的值 至少 为 k ,那么我们称这个数组是 特别的 。

请你返回 nums 中 最短特别非空 子数组 的长度,如果特别子数组不存在,那么返回 -1 。

示例 1:

输入:nums = [1,2,3], k = 2
输出:1
解释:
子数组 [3] 的按位 OR 值为 3 ,所以我们返回 1 。

示例 2:

输入:nums = [2,1,8], k = 10
输出:3
解释:
子数组 [2,1,8] 的按位 OR 值为 11 ,所以我们返回 3 。

示例 3:

输入:nums = [1,2], k = 0
输出:1
解释:
子数组 [1] 的按位 OR 值为 1 ,所以我们返回 1 。

说明:

  • 1 <= nums.length <= 2 * 10^5
  • 0 <= nums[i] <= 10^9
  • 0 <= k <= 10^9

思路

求数组 nums 的最短特别子数组长度,特别子数组的所有元素按位与的结果大于等于 k。

3095.或值至少K的最短子数组I 相比数据范围变大了,O(n^2) 的解法会超时。

记录每一位的出现次数,使用滑动窗口,枚举右收缩左。由于 按位或 没有逆运算,我们可以反向重新计算 按位与。

代码


/**
 * @date 2025-01-17 8:40
 */
public class MinimumSubarrayLength3097 {

    public int minimumSubarrayLength_v2(int[] nums, int k) {
        int res = Integer.MAX_VALUE;
        int right = 0;
        int n = nums.length;
        int or = 0;
        while (right < n) {
            do {
                or |= nums[right++];
            } while (right < n && or < k);
            if (or >= k) {
                int left = right - 1;
                int tmp = 0;
                while (left >= 0) {
                    tmp |= nums[left--];
                    if (tmp >= k) {
                        left++;
                        break;
                    } else {
                        or = tmp;
                    }
                }
                res = Math.min(res, right - left);
            } else {
                break;
            }
        }
        return res == Integer.MAX_VALUE ? -1 : res;
    }

}

性能

3066.超过阈值的最少操作数II

目标

给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。

一次操作中,你将执行:

  • 选择 nums 中最小的两个整数 x 和 y 。
  • 将 x 和 y 从 nums 中删除。
  • 将 min(x, y) * 2 + max(x, y) 添加到数组中的任意位置。

注意,只有当 nums 至少包含两个元素时,你才可以执行以上操作。

你需要使数组中的所有元素都大于或等于 k ,请你返回需要的 最少 操作次数。

示例 1:

输入:nums = [2,11,10,1,3], k = 10
输出:2
解释:第一次操作中,我们删除元素 1 和 2 ,然后添加 1 * 2 + 2 到 nums 中,nums 变为 [4, 11, 10, 3] 。
第二次操作中,我们删除元素 3 和 4 ,然后添加 3 * 2 + 4 到 nums 中,nums 变为 [10, 11, 10] 。
此时,数组中的所有元素都大于等于 10 ,所以我们停止操作。
使数组中所有元素都大于等于 10 需要的最少操作次数为 2 。

示例 2:

输入:nums = [1,1,2,4,9], k = 20
输出:4
解释:第一次操作后,nums 变为 [2, 4, 9, 3] 。
第二次操作后,nums 变为 [7, 4, 9] 。
第三次操作后,nums 变为 [15, 9] 。
第四次操作后,nums 变为 [33] 。
此时,数组中的所有元素都大于等于 20 ,所以我们停止操作。
使数组中所有元素都大于等于 20 需要的最少操作次数为 4 。

说明:

  • 2 <= nums.length <= 2 * 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= k <= 10^9
  • 输入保证答案一定存在,也就是说一定存在一个操作序列使数组中所有元素都大于等于 k 。

思路

求使数组 nums 中所有元素均大于等于 k 的操作次数。每次操作可以将数组中最小的两个元素删除,并将 min(x, y) * 2 + max(x, y) 加入数组。

使用最小堆模拟即可

代码


/**
 * @date 2025-01-14 8:51
 */
public class MinOperations3066 {

    public int minOperations(int[] nums, int k) {
        PriorityQueue<Long> q = new PriorityQueue<>();
        for (int num : nums) {
            q.offer((long) num);
        }
        int res = 0;
        while (q.size() >= 2) {
            Long a = q.poll();
            Long b = q.poll();
            if (a >= k) {
                break;
            }
            q.offer(a * 2L + b);
            res++;
        }
        return res;
    }
}

性能

2270.分割数组的方案数

目标

给你一个下标从 0 开始长度为 n 的整数数组 nums 。

如果以下描述为真,那么 nums 在下标 i 处有一个 合法的分割 :

  • 前 i + 1 个元素的和 大于等于 剩下的 n - i - 1 个元素的和。
  • 下标 i 的右边 至少有一个 元素,也就是说下标 i 满足 0 <= i < n - 1 。

请你返回 nums 中的 合法分割 方案数。

示例 1:

输入:nums = [10,4,-8,7]
输出:2
解释:
总共有 3 种不同的方案可以将 nums 分割成两个非空的部分:
- 在下标 0 处分割 nums 。那么第一部分为 [10] ,和为 10 。第二部分为 [4,-8,7] ,和为 3 。因为 10 >= 3 ,所以 i = 0 是一个合法的分割。
- 在下标 1 处分割 nums 。那么第一部分为 [10,4] ,和为 14 。第二部分为 [-8,7] ,和为 -1 。因为 14 >= -1 ,所以 i = 1 是一个合法的分割。
- 在下标 2 处分割 nums 。那么第一部分为 [10,4,-8] ,和为 6 。第二部分为 [7] ,和为 7 。因为 6 < 7 ,所以 i = 2 不是一个合法的分割。
所以 nums 中总共合法分割方案受为 2 。

示例 2:

输入:nums = [2,3,1,0]
输出:2
解释:
总共有 2 种 nums 的合法分割:
- 在下标 1 处分割 nums 。那么第一部分为 [2,3] ,和为 5 。第二部分为 [1,0] ,和为 1 。因为 5 >= 1 ,所以 i = 1 是一个合法的分割。
- 在下标 2 处分割 nums 。那么第一部分为 [2,3,1] ,和为 6 。第二部分为 [0] ,和为 0 。因为 6 >= 0 ,所以 i = 2 是一个合法的分割。

说明:

  • 2 <= nums.length <= 10^5
  • -10^5 <= nums[i] <= 10^5

思路

求数组的合法分割点个数,下标 i 是合法分割点的条件是 前 i + 1 个元素和大于剩余元素和,且至少要有一个元素。

直接的想法是计算前缀和,然后按照题意计算。

实际实现时发现只用到了 所有元素的和 sum 以及区间 [0, i] 的和 tmp,无需存储前缀,直接在遍历的时候计算就可以。

代码


/**
 * @date 2025-01-13 8:41
 */
public class WaysToSplitArray2270 {

    public int waysToSplitArray(int[] nums) {
        long sum = 0;
        for (int num : nums) {
            sum += num;
        }
        int res = 0;
        long tmp = 0;
        for (int i = 0; i < nums.length - 1; i++) {
            tmp += nums[i];
            if (2 * tmp >= sum) {
                res++;
            }
        }
        return res;
    }

}

性能