零钱兑换 II 图解题解
这道题到底在问什么
- coins
- [1, 2, 5]
- amount
- 5
- 输出
- 4
先想最直接的笨办法
最直接的想法:递归枚举「面额 1 用几枚、面额 2 用几枚、面额 5 用几枚」,凑够 5 就记一种。可这样不同硬币的搭配会指数爆炸,而且「凑剩下 3 块有几种」这种子问题会被反复重算,慢得离谱。(动画第 3 步)
最优解:一步一步想明白
- 3最直接的想法:递归枚举「面额 1 用几枚、面额 2 用几枚、面额 5 用几枚」,凑够 5 就记一种。可这样不同硬币的搭配会指数爆炸,而且「凑剩下 3 块有几种」这种子问题会被反复重算,慢得离谱。
- 4转折:把「凑 j 有几种」记进 dp[j],重叠子问题就只算一次。状态:dp[j]=凑出 j 的组合数;转移:加入面额 coin 后 dp[j] += dp[j−coin](凑 j 的新法 = 先凑好 j−coin 再添一枚 coin)。外层枚举硬币、内层金额从小到大,就保证同一种组合只按「硬币种类顺序」数一次,不会把 2+1 和 1+2 当两种。
- 5dp = [1,0,0,0,0,0]表头是金额 0 到 5,一行 6 格,全程不变。dp[0]=1:凑 0 块有一种办法,就是什么都不拿(空组合)。其余先记 0,还没开始放硬币。
- 6coin=1先放面额 1。从 j=1 到 5 依次 dp[j] += dp[j−1]:dp[1]=dp[0]=1、dp[2]=dp[1]=1…一路全是 1。意思是只用 1时每个金额都只有「全用 1」这一种凑法。
- 7dp[2] += dp[0]换面额 2,内层金额仍从小到大。先填 dp[2]:在原来「1+1」之外,多了一种「直接一枚 2」,来源是 dp[2−2]=dp[0]=1。所以 dp[2] = 1 + 1 = 2。
- 8dp[3] += dp[1]dp[3]:加一枚 2 后剩 1,dp[1]=1(即 2+1)。dp[3] = 原来的 1(1+1+1)+ 1 = 2。
- 9dp[4] += dp[2]dp[4] 用到刚更新过的 dp[2]=2——这正是从小到大的妙处:面额 2 可以重复用。dp[4] = 1 + 2 = 3(对应 1+1+1+1、2+1+1、2+2)。
- 10dp[5] += dp[3]面额 2 收尾,dp[5] += dp[3]=2,从 1 涨到 3。现在只用 1 和 2,凑 5 已经有 3 种。还差面额 5 没放。
- 11coin=5,j 从 5 起换面额 5。内层金额从 coin=5 起步,所以金额 小于 5 的格子这一轮碰都不碰(不可能用上一枚 5),dp[1] 到 dp[4] 保持不变。只剩 dp[5] 要更新。
- 12dp[5] += dp[0]dp[5] += dp[5−5]=dp[0]=1:多出来的这一种就是单独一枚 5。dp[5] 从 3 变成 4。三种硬币全放完了。
- 13dp[5] = 4表格最后一格 dp[5] = 4 就是答案,对应那 4 种组合:5、2+2+1、2+1+1+1、1×5。返回 4。
- 16完全背包内层从小到大(硬币可重复),0-1 背包内层从大到小(每个只用一次)。求组合数外层套物品、内层套容量;求排列数把两层对调。这一句记牢,一类题全通。
⚠️ 容易写错的地方
✗ 错:for j in range(coin, amount+1) 写成内层硬币、外层金额
✓ 对:求组合数必须外层枚举硬币、内层枚举金额
层序反了会把 1+2 和 2+1 当成两种,数出来的是排列数(LC377)不是组合数
✗ 错:for j in range(amount, coin-1, -1) 内层从大到小
✓ 对:完全背包内层从小到大
从大到小会让每种硬币只能用一次,退化成 0-1 背包,凑 5 只剩 2 种
✗ 错:dp[0] 忘了置 1,整张表全 0
✓ 对:dp[0]=1 是递推的种子
dp[0] 是「凑 0 块的空组合」,是所有 dp[j] += dp[j−coin] 的起点,漏了答案恒为 0
完整代码(Python / C++ / Java)
Python
class Solution:
def change(self, amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1 # 凑 0 元:空集算一种
for coin in coins: # 外层硬币 → 只数组合
for x in range(coin, amount + 1): # 正序 → 每币可重复用
dp[x] += dp[x - coin]
return dp[amount]C++
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1; // 凑 0 元:空集算一种
for (int coin : coins) // 外层硬币 → 只数组合
for (int x = coin; x <= amount; x++) // 正序 → 完全背包
dp[x] += dp[x - coin];
return dp[amount];
}
};Java
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1; // 凑 0 元:空集算一种
for (int coin : coins) // 外层硬币 → 只数组合
for (int x = coin; x <= amount; x++) // 正序 → 完全背包
dp[x] += dp[x - coin];
return dp[amount];
}
}套路模板 · 完全背包组合计数(背下来)
# 每个物品可无限次使用,求凑出 W 的组合数
dp = [0] * (W + 1)
dp[0] = 1 # 计数初值 1(最值题改 0 或 INF)
for x in items: # 求组合数:外层物品
for j in range(x, W + 1): # 完全背包:内层从小到大
dp[j] += dp[j - x] # 计数用 +=;最值用 min/max
return dp[W]复杂度
时间复杂度
O(n × amount)
n 种硬币各扫一遍金额,每格做一次加法
空间复杂度
O(amount)
只用一行长度 amount+1 的 dp 数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 零钱兑换 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
核心思路是什么?+
完全背包求「组合数」。dp[x]=凑出金额 x 的组合数,dp[0]=1(空集)。外层遍历硬币、内层金额正序:dp[x] += dp[x−coin]。硬币在外保证只数组合、不数排列。
和「零钱兑换 I(LC322 求最少硬币数)」有何区别?+
LC322 求最少枚数,转移 dp[x]=min(dp[x], dp[x−coin]+1);本题求方案数,转移 dp[x] += dp[x−coin]。一个取 min、一个累加。
若要求「排列数」(顺序不同算不同)怎么改?+
两层循环对调:外层金额、内层硬币,dp[x] += dp[x−coin],那就是 LC377 组合总和 Ⅳ。
复杂度多少?+
两层循环 O(n×amount),一维 dp 数组 O(amount)。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。