从缺啥补啥到无懈可击:补齐数组的最优解法

在这里插入图片描述
在这里插入图片描述

摘要

这篇文章要聊的是 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:加 24,才能覆盖到 20。
  • 示例 3:不用补,已经能覆盖 1~5。

时间复杂度

时间复杂度:O(log n)

为什么这么快?因为每次添加数字后,miss 至少会翻倍增长(miss += miss),所以最多循环 log(n) 次。遍历 nums 的次数最多也就 nums.count,整体来看是非常高效的。

空间复杂度

空间复杂度:O(1)

没有使用额外的数据结构,只有一些常量级变量,空间占用几乎可以忽略。

总结

这题的关键在于转换思维方式:
不是从“怎么组合数字”入手,而是从“覆盖区间”来考虑。

每次的目标是拓展我们能表示的最大范围 miss,用已有数组元素能拓展最好,用不了就手动补一个。就像我们拿着一盏灯照亮前方的路,每次只要能把灯光范围往前推进一截就行。

这种贪心策略非常高效,也适合处理大规模的输入。Swift 的整数类型在这里要特别注意,用 Int64 是为了防止溢出问题,尤其是当 n 非常大(比如 2^31-1)时。