问题描述:
- Given a sorted positive integer array nums and an integer n, add/patch elements to the array such that any number in range [1, n] inclusive can be formed by the sum of some elements in the array. Return the minimum number of patches required.
- 给你有序的正数数组nums, 和正数n。 问, 如果你可以自由选择nums中的数, 想加出1~n上的任何一个数, 你还缺几个数。
- Example 1
nums = [1, 3], n = 6
Return 1.
Combinations of nums are [1], [3], [1,3], which form possible sums of: 1, 3, 4.
Now if we add/patch 2 to nums, the combinations are: [1], [2], [3], [1,3], [2,3], [1,2,3].
Possible sums are 1, 2, 3, 4, 5, 6, which now covers the range [1, 6].
So we only need 1 patch.
- Example 2
nums = [1, 5, 10], n = 20
Return 2.
The two patches can be [2, 4].
- Example 3
nums = [1, 2, 2], n = 5
Return 0.
思路分析
- 首先这题是求最小值,既是一个最优问题。一般来说,最优问题都可以用动态规划或者贪心算法来解决。该题用贪心算法。
- 定义一个变量range,表示当前可以达到的最大范围,也便是用数组(0~i-1)位置上的元素能够加出来(1~range)范围内所有的数,range便是表示该范围的最大右边界。所以初始化range为0,此时还没有计算数组中的任何一个数。
- 大思路从左到右遍历数组,不断更新range,直至
range>n
,结束遍历。那么如何更新(迭代)range以及如何为数组打补丁呢(添加数)?
- 若遍历完(0~i-1)位置,用数组(0~i-1)位置上的元素能够加出来(1~range)范围内所有的数,range表示该范围的最大右边界。
那么如果cur[i] > range+1
,则无法用nums[0~i]加出来 range+1,则出现了断点,此时必须打一个 range+1的补丁,即向数组中添加 range+1这个数,以此保证范围的连续性。此时range更新为range +=range + 1
,继续检查cur[i]与range+1的大小。 如果cur[i] <= range+1
,那么此时用nums[0~i]可以加出来 (1~cur[i]+range)内所有的数,则此刻无需打补丁,range更新为cur[i]+range。继续向右遍历。 - 举例说明,现有数组[1,2,3,9],range初始值为0,对于1而言,range被更新为了1。向右遍历,对于2而言,range被更新为了3,用[1,2]能够加出来1,2,3。
对于3而言,可以这样考虑,3与(1~range)相加,那么可以加出(4~range+3)也就是(4~6),而先前范围为(1~3),那么这两个区间可以无间隔地合并在一起,说明用[1,2,3]可以加出来(1~6),此时range更新为了6。
但对于9,9与(1~range)相加为(10~15),与原范围(1~6)有间隔,无法合并,7,8,9这三个值无法加出来,说明此刻必须打补丁。 - 那么如何判断需不需要打补丁以及如何打补丁呢?
原范围为(1~range),与cur[i]相加后形成(1+cur[i],range+cur[i]),并且cur[i]也可不与其他数相加,即只选这一个值,所以区间被扩(cur[i],range+cur[i]);为了保证两个区间可以无间隔合并为一个区间,必须要求cur[i] <= range+1
;
如果cur[i] > range+1
,必须打补丁,保证区间连续,补充的值便是第一个断点位置,range+1,你可以打小于range+1大于0的数作为补丁,但是range+1是既能修补断点,又能保证打了补丁后,range能更新到最大的一个值,这样便能使补丁数目最小。这便是贪心算法的应用之处,每次的选择都是最优的。 - 注意,可能数组全部遍历结束,还没有加到n,那么每次都打range+1的补丁,不断迭代更新range,直到
range>n
- 若遍历完(0~i-1)位置,用数组(0~i-1)位置上的元素能够加出来(1~range)范围内所有的数,range表示该范围的最大右边界。
经验教训
- 一般来说,最优问题都可以用动态规划或者贪心算法来解决。
- 该题range的概念与用法与另一道题的进阶问题类似,附上链接:正数数组的最小不可组成和问题
- 深刻理解range的用法
- range可能会是数组中所有数的累加和,为了防止溢出,设为long
代码实现
- 方法一
public static int minPatches(int[] nums, int n) {
int patchesNum = 0;
long range = 0;
//遍历数组
for (int i = 0; i < nums.length; i++) {
//range+1不可达:打一个range + 1的补丁,更新补丁数目,打补丁到直至可达
while (nums[i] > range + 1) {
patchesNum++;
range += range + 1;
//检查是否到n
if (range >= n) {
return patchesNum;
}
}
//无需打补丁,直接更新range,并检查是否到n
range += nums[i];
if (range >= n) {
return patchesNum;
}
}
//数组遍历结束还没有加到n,
//每次需要打一个range + 1的补丁,更新补丁数目,更新range,直至range >= n
while (range < n) {
patchesNum++;
range += range+1;
}
return patchesNum;
}
- 方法二
public static int minPatches(int[] nums, int n) {
int patchesNum = 0;
long range = 0;
int index = 0;
while(range < n) {
//数组尚未遍历完,并且nums[index] <= range+1:无需打补丁,直接更新range
if (index < nums.length && nums[index] <= range+1) {
range += nums[index];
index++;
}else {//要么是nums[index] > range+1,导致range+1不可达;要么是数组已经遍历结束还没有到达n
//都需要打一个range + 1的补丁,更新补丁数目,更新range
patchesNum++;
range += range + 1;
}
}
return patchesNum;
}