题目描述
思路解析
一句话答案:LeetCode 330 按要求补齐数组:贪心维护第一个还凑不出的和 miss,够得着的原数就吃进、够不着就补 miss 让覆盖翻倍,补最少个数,时间 O(m+log n)、空间 O(1)。
补几个数,才能让 1 到 n 一个不漏地被凑出来
给一个升序数组 nums 和整数 n,可往里添任意的数。要求:从数组挑若干元素求和,凑出 1 到 n 之间的每个整数,问最少添几个数。题面 nums=[1,5,10]、n=20 时答案是 2(补 2 和 4,1 到 20 就都能凑)。
该往数组里塞哪几个数,根本无从下手
难点在「补哪些数」这个问法本身:候选的数无穷多,组合枚举不完。不如盯住更实在的量——从 1 往上连续数,第一个还凑不出的数是几。盯死它,拿数补数都有了依据。
盯住第一个凑不出的数,够得着就吃、够不着就翻倍
用变量 miss 记「1 到 miss−1 都已能凑出,miss 是第一个还凑不出的缺口」,初始 miss=1。每步只比一次:原数 nums[i] 和 miss 谁大。若 nums[i] ≤ miss,能无缝接上:原来凑到 miss−1,添上后接到 miss+nums[i]−1、中间不断,于是吃进来、miss 加 nums[i]。
若 nums[i] > miss,原数太大接不上,miss 这个缺口得先补一个数。补哪个最赚?补 miss 本身:已能凑到 miss−1,再补进 miss,则 miss 到 2·miss−1 也全能凑,连续上界一口气从 miss 翻倍到 2·miss。贪心在这题就长这样:每步都让可达上界扩得最远,补 miss 正是单步推得最远的补法;补更小的涨得慢,补更大的又留缝。所以补 miss 既最划算也全局最少。等 miss > n,1 到 n 全覆盖。
代码就三行逻辑,miss 一定要用 long
循环 while miss ≤ n:能拿原数(nums[i] ≤ miss)就 miss += nums[i]、i 前移;拿不动就 miss += miss 翻倍、补丁数加一,结束返回补丁数。一个坑:miss 翻倍涨得极快,n 最大到 2^31−1,用 int 存会越上限溢出成负数让循环出错,miss 得用 long(C++ 用 long long)。
nums=[1,5,10]、n=20,两个补丁是怎么冒出来的
开局 miss=1、i 指向 nums[0]=1。1 ≤ miss(1),吃进,miss=1+1=2,i 移到 nums[1]=5。5 > miss(2),补 miss=2 翻倍,miss=2+2=4,计数 1。5 仍 > miss(4),再补 miss=4,miss=4+4=8,计数 2。
5 ≤ miss(8),吃进,miss=8+5=13,i 移到 nums[2]=10。10 ≤ miss(13),吃进,miss=13+10=23。23 > n=20,1 到 20 全覆盖。全程只补 2 和 4,答案是 2。
补多补错,常栽在这两个地方,顺带把复杂度算清
补丁补多,常栽在没先用原数:nums[i] ≤ miss 时能不花补丁拿进来,却急着补 miss 白花一个;每步先判原数能不能吃,吃不动才补。另一处是纠结补别的数,认准补 miss 本身就不会错。
复杂度上,每个原数最多被吃一次是 O(m);补丁每补让 miss 翻倍,翻到超过 n 需 O(log n) 次,时间 O(m+log n),空间 O(1)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
全题就靠一个数 miss:它表示「1 到 miss−1 我都能凑出来,miss 是第一个缺口」。一开始 miss=1,意思是「啥都还没覆盖」。我们要做的就是想办法把 miss 一路推过 n。
关键的两条规矩:能覆盖 [1,miss) 时,再来个 x ≤ miss,新范围 [1, miss+x) 中间不会断;但 x > miss 时,miss 自己就卡死了。补数时补谁最赚?补 miss 本身——它能把覆盖范围一口气翻倍到 [1, 2·miss)。
开局 · miss = 1:目标 n = 20。指针 i 指向 nums[0]=1。miss=1 表示现在连 1 都凑不出。问:手头的 nums[0]=1 能帮上忙吗?
nums[0]=1 ≤ miss=1 · 拿!:1 ≤ miss(1),免费拿来用!覆盖范围接上,miss 从 1 长到 2——现在能凑出 1 了。这格变绿表示已用上,指针准备右移。
指针 → nums[1]:nums[0] 用完封存变灰,指针 i 挪到 nums[1]=5。现在第一个凑不出的数是 2。看看 5 能不能接上去。
比一比 · nums[1]=5 vs miss=2:每一轮都做同一个判断:指针指的数和 miss 谁大?这里 nums[1]=5 比 miss=2 大,意味着 5 暂时接不上当前的覆盖范围。
nums[1]=5 > miss=2 · 出现缺口!:5 > miss(2)!若直接用 5,覆盖会从「能凑 1」跳到 5,中间 2、3、4 全断了。所以不能用 5,得先补一个数把缺口 2 堵上。
补丁① · 补 miss=2:补谁最划算?补 miss 自己 = 2!原来能凑 [1,2),加进一个 2 后,2、3(=1+2)也能凑了 → 覆盖一口气翻倍到 [1,4)。miss 从 2 跳到 4,补丁计数 +1。
nums[1]=5 还是 > miss=4:补完一个 2 后再看:nums[1] 还是 5,5 > miss(4),缺口变成了 4。5 还是接不上,只能再补一次。
补丁② · 补 miss=4:同样的招:补 miss=4,覆盖再翻倍到 [1,8)——现在 1~7 全能凑。miss 从 4 跳到 8,补丁计数到 2。这就是答案里的两个补丁:2 和 4。
再比一比 · nums[1]=5 vs miss=8:miss 翻倍两次到 8 之后再做同样的比较:nums[1]=5 这次 ≤ miss(8) 了!说明 5 现在能无缝接到覆盖范围上,不用再补。
nums[1]=5 ≤ miss=8 · 终于能拿!:5 ≤ miss(8) 成立,免费拿来用!覆盖从 [1,8) 扩到 [1,13),miss 跳到 13。nums[1] 变绿,指针准备移向最后一个数。
指针 → nums[2]:nums[1] 用完封存,指针 i 挪到最后的 nums[2]=10。现在第一个缺口是 13。看 10 能不能接上。
比一比 · nums[2]=10 vs miss=13:最后一个数 nums[2]=10 和 miss=13 比:10 ≤ 13,又能无缝接上,不用补。
nums[2]=10 ≤ miss=13 · 拿!:10 ≤ miss(13),拿!覆盖从 [1,13) 扩到 [1,23),miss 一举冲到 23。注意 23 已经超过 n=20 了。
miss=23 > n=20 · 收工!:miss 已经 > n=20,说明 1~20 全覆盖到了,循环结束。整个过程只补了 两个数(2 和 4)——这就是最少补丁数 2。三个原数(绿色)都派上了用场。
贪心为什么对?因为补 miss 让覆盖范围增长最快(翻倍)——补任何比 miss 小的数都不如它,补比 miss 大的数又会留缝。所以每次补 miss 是局部最优也是全局最优。
再看一例 · nums=[1,3], n=6:换组数据 nums=[1,3], n=6 再走一遍,体会「拿原数」和「补缺口」如何交替。开局 miss=1,指针指向 nums[0]=1。
nums[0]=1 ≤ miss=1 · 拿!:1 ≤ miss,拿来用,miss 长到 2。nums[0] 变绿。
nums[1]=3 > miss=2 · 缺口!补 2:指针到 nums[1]=3,3 > miss(2),缺口 2 卡住了。补 miss=2,覆盖翻倍到 [1,4),miss 跳到 4,补丁计数 1。
nums[1]=3 ≤ miss=4 · 拿!:miss 涨到 4 后 3 ≤ miss(4),拿!覆盖扩到 [1,7),miss 跳到 7。nums[1] 变绿。
miss=7 > n=6 · 收工!答案 = 1:miss=7 已 > n=6,1~6 全覆盖。这例只补了 1 个数(就是 2),答案 1。
雷区实演 · 缺口该补谁:已覆盖 [1,2)、缺口是 2 时:补 2 直接翻倍到 [1,4) 最划算;补 3 连缺口 2 都没堵上;补 5 会让 2、3、4 都断掉。所以只能补 miss 自己。
边界三连:先想空数组(纯翻倍)、刚好够用(补 0)、n 极大(防溢出)三种,代码就稳了。
面试追问:把「补 miss 为何最优」「为何用 long」「O(m+log n)」讲清楚,是这题面试的得分点。
参考代码
class Solution: def minPatches(self, nums, n): patches = 0 i = 0 miss = 1 # 第一个还凑不出的数 while miss <= n: if i < len(nums) and nums[i] <= miss: miss += nums[i] # 免费拿原数, 接上覆盖 i += 1 else: miss += miss # 补 miss, 覆盖翻倍 patches += 1 return patches复杂度
- 时间复杂度:O(m + log n),m 是数组长度(每个原数最多被吃一次);补丁每次让 miss 翻倍,最多 log n 次就能超过 n
- 空间复杂度:O(1),只用了 miss / patches / i 几个变量,没开额外数组
易错点
面试追问把动画讲成自己的话
追问为什么贪心补 miss 一定最优?
追问为什么 miss 要用 long?
追问复杂度为什么是 O(m + log n)?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。再去主线里挑下一道练手。
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题