LC322 零钱兑换¶
讲解章节:背包问题
一句话
面值无限供应的完全背包求最少枚数,容量正序推,凑不出返回 -1。
解题思路¶
这题考什么¶
「每种硬币可以取任意多枚,凑出总额所需的最少枚数」是完全背包的最小化形态, 见 背包问题。
状态定义:f[c] 表示凑出金额 c 所需的最少硬币枚数。 转移方程(v 遍历每种面值):
初值 f[0] = 0(凑 0 元一枚不用),其余取一个达不到的大数当无穷。
容量正序遍历是这题的核心,必须讲透:扫到 f[c] 时,f[c - v] 有可能 已经在本轮被同一个面值 v 更新过,于是「在那基础上再放一枚 v」这件事 被自然允许,同一种硬币可以取任意多枚。若改成倒序,f[c - v] 一定还是 「尚未考虑 v」的旧值,每种硬币就只能用一次,求出来是 01 背包的答案。 这与 LC416 分割等和子集恰好相反——那题要的正是「每个数只用一次」,所以必须倒序。
为什么贪心不成立:直觉上「每次尽量用最大的面值」看似合理,但 coins = [1, 3, 4]、amount = 6 时贪心得到 4 + 1 + 1 = 3 枚,最优是 3 + 3 = 2 枚。 只有当面值满足特殊的整除结构(如人民币)时贪心才对,一般面值下必须 DP。
数据规模与复杂度¶
1 <= coins.length <= 12,1 <= coins[i] <= 2^31 - 1,0 <= amount <= 1e4。 时间 O(面值数 * amount) = 12 * 1e4 = 1.2e5 次内层迭代,毫秒级。 空间 O(amount),一个长度 amount+1 的整数列表。
面值上界 2^31 - 1 远大于金额上界 1e4,这个反差是题面刻意设的: 比 amount 还大的硬币一枚也放不进去,先滤掉可以省掉一整轮空转的 range。
坑在哪¶
- 无解要返回 -1,不能返回哨兵值。用 amount + 1 当「无穷」, 是因为全用面值 1 也至多需要 amount 枚,任何真实答案都不会超过 amount, 所以末尾判 f[amount] > amount 即无解。取整数哨兵而不是 float('inf'), 既保持整数运算,又让这条判据自然成立。
- amount = 0 要返回 0(题面样例 3 明确要求)。f 只有 f[0] = 0 一项, 所有面值都因 v > 0 而使内层 range 为空,直接返回 f[0] = 0,无需特判。
- 内层容量必须正序。倒序会退化成每种硬币只用一次: coins = [1, 2, 5]、amount = 11 时子集和最大是 8,凑不出 11, f[11] 停在哨兵上,函数返回 -1,而正确答案是 3。
- 滤掉 v > amount 属于优化而非正确性要求:不滤时 range(v, amount + 1) 为空, 结果照样对。但面值可以大到 2^31 - 1,为一枚放不进去的硬币白建一次空区间纯属浪费, 显式跳过也让「大面值不参与转移」这件事在代码里看得见。
- 题面没有保证 coins 内的面值互不相同,也没保证有序。 这份写法对重复面值免疫(同一面值多推一轮不改变结果),顺序也无关, 所以不需要预先去重或排序。
样例复核¶
coins = [1, 2, 5],amount = 11。
只用 1:f[c] = c,f[11] = 11
加入 2 正序扫:f[2] = 1, f[4] = 2, ..., f[10] = 5, f[11] = f[9] + 1 = 5 + 1 = 6
加入 5 正序扫:f[5] = f[0] + 1 = 1,f[6] = f[1] + 1 = 2(即 5 + 1),
f[10] = f[5] + 1 = 2(即 5 + 5),
最后 f[11] = f[6] + 1 = 3。
f[6] 是在**本轮内**被压到 2 的,f[11] 立刻用上了这个新值——
这就是正序遍历让同一面值被重复取用的具体样子
返回 3,对应 5 + 5 + 1,与期望输出一致。