LeetCode 518. 零钱兑换 II
题目描述


题意分析
给定一个面额数组
coins和一个目标金额amount,问有多少种方式可以正好凑出这个金额,返回方案的数量。题面里有三个必须先咬准的信号。第一,每种硬币的数量是无限的,也就是同一个面额可以被反复使用,这与「每个物品只能拿一次」的场景有本质区别。第二,题目只问有多少种,不要求把方案本身列出来,因此不需要保存任何路径信息,只需要一个能相加的计数。第三,也是最关键的一点,顺序不同算同一种:先拿 1 再拿 2 和先拿 2 再拿 1 都是「一个 1 加一个 2」,只能计一次。换句话说,一个方案的身份由「每种面额各用了几枚」这个多重集合唯一决定,而不是由取用的先后次序决定。
边界方面有两处要留意。其一,
amount可以为 0,此时正确答案是 1——什么都不拿本身就是一种合法的凑法,这个约定后面会直接决定初始值怎么设。其二,coins里的面额互不相同且都为正数,所以不会出现面额为 0 导致自我循环的情况,但完全可能所有面额都大于amount,此时答案是 0。题目还保证结果在 32 位有符号整数范围内,这说明不需要为溢出做额外处理,可以放心用
int累加。
解法:完全背包组合计数
核心思路
问题关键:每种硬币可重复使用,但顺序不同不算新方案。例如
1 + 2与2 + 1是同一种组合,因此既要满足“完全背包”,又要避免排列重复。为什么选一维动态规划:定义
\[dp[j] = dp[j] + dp[j-coin]\]dp[j]为“只使用当前已经枚举过的硬币,凑出金额j的组合数”。处理面额coin时:前一项是不使用
coin的方案,后一项是在凑出j-coin的每个方案后再放一枚coin。金额从小到大枚举,使本轮刚更新的状态还能继续参与转移,因此同一面额可以使用多次。不变量与正确性:处理完前
k种硬币后,dp[j]恰好记录只用这k种硬币凑出j的组合数。任一方案按“是否使用第k种硬币”可唯一分成两类,转移不重不漏。外层按硬币枚举,还给每个组合规定了固定的加入顺序,所以[1,2]不会再以[2,1]被统计。初始化
dp[0] = 1,表示凑出金额 0 有一种空方案,也是所有有效计数的起点。若外层改为枚举金额,统计结果会变成排列数;若金额倒序,当前硬币只能使用一次。
解题步骤
- 创建长度为
amount + 1的数组,令dp[0] = 1。- 外层逐个枚举硬币
coin,固定组合中硬币的加入顺序。- 内层从
coin正序枚举到amount,执行dp[j] += dp[j-coin]。- 返回
dp[amount]。以
amount = 5, coins = [1,2,5]为例,处理三种硬币后,dp依次变为[1,1,1,1,1,1]、[1,1,2,2,3,3]、[1,1,2,2,3,4],答案为 4。
代码实现
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int coin : coins) {
for (int j = coin; j <= amount; j++) {
dp[j] += dp[j - coin];
}
}
return dp[amount];
}
}
func change(amount int, coins []int) int {
dp := make([]int, amount+1)
dp[0] = 1
for _, coin := range coins {
for j := coin; j <= amount; j++ {
dp[j] += dp[j-coin]
}
}
return dp[amount]
}
复杂度分析
- 时间复杂度:$O(n \cdot amount)$,其中
n是硬币种类数。- 空间复杂度:$O(amount)$,使用一维数组保存状态。
关键点总结
- 外层枚举硬币,保证统计的是组合而不是排列。
- 金额正序更新,保证当前硬币可以重复使用。
dp[0] = 1表示空方案,不能初始化为 0。- 完整二维状态是“前
k种硬币凑出金额j”,一维写法只是把k这一维滚动压缩。
易错点总结
- 交换循环顺序:
amount = 5, coins = [1,2,5]会得到 9,把不同顺序当成不同方案。- 金额倒序更新:会退化为 0-1 背包,每种硬币最多使用一次。
- 遗漏
dp[0] = 1:所有状态都无法从 0 累加起来,最终答案恒为 0。- 把
+=写成赋值:会覆盖“不使用当前硬币”的已有方案。- 混淆 322 题:本题求方案数用累加;零钱兑换求最少枚数用取最小值。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 377. 组合总和 Ⅳ | 中等 | 同一份转移求排列数,循环顺序与本题正好相反 |
| 322. 零钱兑换 | 中等 | 同样的完全背包骨架,但求最少枚数而非方案数 |
| 279. 完全平方数 | 中等 | 物品由平方数隐式生成,考察把题面翻译成背包 |
| 416. 分割等和子集 | 中等 | 0-1 背包可行性,内层必须倒序,是正序的直接对照 |
| 494. 目标和 | 中等 | 先做数学变形转成子集和计数,再套 0-1 背包方案数 |
| 474. 一和零 | 中等 | 二维容量的 0-1 背包,考察多约束下的倒序压缩 |
| 1449. 数位成本和为目标值的最大数字 | 困难 | 完全背包求恰好装满下的字典序最大解,需要回溯构造串 |
| LCR 103. 零钱兑换 | 中等 | 322 题的同题异号,适合用来复查最值型初始化 |
| LCR 104. 组合总和 Ⅳ | 中等 | 377 题的同题异号,可与本题对照检验循环顺序 |
| 面试题 08.11. 硬币 | 中等 | 固定面额的组合计数,额外要求对大数取模 |