摘要
这篇文章要聊的是 LeetCode 第 330 题《按要求补齐数组》。这题的目标是:给你一个已经排好序的正整数数组 nums
,和一个正整数 n
,你需要往 nums
里尽可能少地“补数字”,让从 1 到 n 的所有数,都能被 nums
中的数字组合成“和”。
听上去像是在玩积木,我们想要搭出从 1 到 n 的所有数,那么该怎么搭、最少还得补几个积木?别急,咱们一步一步来拆解这个问题。
描述
先看看题目的核心点:
给定一个升序数组
nums
和一个整数n
,我们可以从[1, n]
里选取任意多个数字补进nums
,目标是让[1, n]
这个区间里的每个数都可以用数组中若干数字之和表示出来。返回你最少要补多少个数。
这题并不要求我们具体列出组合,而是看“能否被表示出来”,我们要关注的是范围覆盖。
题解答案
我们需要尽可能少地补数字,让 [1, n]
中的每个数字都可以被表示成若干个数组元素的和(不一定要全用上)。
换句话说,如果当前我们可以表示的范围是 [1, miss)
,那我们每次希望用一个最小的补丁(或者数组中的一个数)去扩展这个范围。
所以这题的核心是一个“贪心算法”:
- 从 1 开始,如果当前的数字
miss
小于等于数组中的某个值nums[i]
,说明nums[i]
可以用来拓展范围; - 如果当前的
miss
小于nums[i]
,说明我们得手动补一个数字——就是miss
本身; - 每次添加数字 x,范围就可以从
[1, miss)
扩展到[1, miss + x)
。
这样一直扩展到 miss > n
,就表示 [1, n]
都可以覆盖到了。
题解代码分析
下面是用 Swift 写的完整可运行 Demo:
func minPatches(_ nums: [Int], _ n: Int) -> Int {
var miss: Int64 = 1
var added = 0
var index = 0
let nums = nums.map { Int64($0) }
let n = Int64(n)
while miss <= n {
if index < nums.count && nums[index] <= miss {
// nums[index] 可以拓展当前 miss 区间
miss += nums[index]
index += 1
} else {
// 需要手动补一个数:miss
miss += miss
added += 1
}
}
return added
}
关键变量解释
miss
: 当前我们还没有覆盖到的最小值(即最小不能被构造的数)index
: 遍历nums
的位置added
: 我们补了几个数字
思路回顾
每当 miss <= nums[index]
,说明我们能用当前数字扩展范围;否则就必须“手动”补一个数(也就是补 miss
本身),来拓展 miss
。
这段代码的运行是线性的,效率非常高,尤其适合处理较大的 n
。
示例测试及结果
我们来跑几个题目中提到的例子看看效果:
print(minPatches([1, 3], 6)) // 输出 1
print(minPatches([1, 5, 10], 20)) // 输出 2
print(minPatches([1, 2, 2], 5)) // 输出 0
解释:
- 示例 1:加一个
2
就能补齐 1 到 6。 - 示例 2:加
2
和4
,才能覆盖到 20。 - 示例 3:不用补,已经能覆盖 1~5。
时间复杂度
时间复杂度:O(log n)
为什么这么快?因为每次添加数字后,miss
至少会翻倍增长(miss += miss
),所以最多循环 log(n) 次。遍历 nums
的次数最多也就 nums.count
,整体来看是非常高效的。
空间复杂度
空间复杂度:O(1)
没有使用额外的数据结构,只有一些常量级变量,空间占用几乎可以忽略。
总结
这题的关键在于转换思维方式:
不是从“怎么组合数字”入手,而是从“覆盖区间”来考虑。
每次的目标是拓展我们能表示的最大范围 miss
,用已有数组元素能拓展最好,用不了就手动补一个。就像我们拿着一盏灯照亮前方的路,每次只要能把灯光范围往前推进一截就行。
这种贪心策略非常高效,也适合处理大规模的输入。Swift 的整数类型在这里要特别注意,用 Int64
是为了防止溢出问题,尤其是当 n
非常大(比如 2^31-1)时。