518. 零钱兑换 II

中等 含交互动画

学完这道你应该能: 说清它为什么用「完全背包」,而不是死记步骤; 合上页面,自己默写出核心代码; 用 30 秒把思路讲清楚。

AI 私教 · 就着本题动画讲
卡住了不用硬扛,按 8 步把这题真正走通。
从读题、试解、找法到手写代码,边问边打勾,适合第一次系统学算法的同学。

题目描述

硬币面额可重复使用,求凑出 amount 的组合数(顺序不算,1+2 和 2+1 算同一种)。

coins = [1, 2, 5]
amount = 5
输出 = 4

思路解析

最直接的想法:递归枚举「面额 1 用几枚、面额 2 用几枚、面额 5 用几枚」,凑够 5 就记一种。可这样不同硬币的搭配会指数爆炸,而且「凑剩下 3 块有几种」这种子问题会被反复重算,慢得离谱。

转折:把「凑 j 有几种」记进 dp[j],重叠子问题就只算一次。状态:dp[j]=凑出 j 的组合数;转移:加入面额 coin 后 dp[j] += dp[j−coin](凑 j 的新法 = 先凑好 j−coin 再添一枚 coin)。外层枚举硬币、内层金额从小到大,就保证同一种组合只按「硬币种类顺序」数一次,不会把 2+1 和 1+2 当两种。

建表 · dp[0]=1:表头是金额 0 到 5,一行 6 格,全程不变。dp[0]=1:凑 0 块有一种办法,就是什么都不拿(空组合)。其余先记 0,还没开始放硬币。

硬币1 · 全程能加:先放面额 1。从 j=1 到 5 依次 dp[j] += dp[j−1]:dp[1]=dp[0]=1、dp[2]=dp[1]=1…一路全是 1。意思是只用 1时每个金额都只有「全用 1」这一种凑法。

加入硬币2 · 填 dp[2]:换面额 2,内层金额仍从小到大。先填 dp[2]:在原来「1+1」之外,多了一种「直接一枚 2」,来源是 dp[2−2]=dp[0]=1。所以 dp[2] = 1 + 1 = 2

硬币2 · 填 dp[3]:dp[3]:加一枚 2 后剩 1,dp[1]=1(即 2+1)。dp[3] = 原来的 1(1+1+1)+ 1 = 2

硬币2 · 填 dp[4]:dp[4] 用到刚更新过的 dp[2]=2——这正是从小到大的妙处:面额 2 可以重复用。dp[4] = 1 + 2 = 3(对应 1+1+1+1、2+1+1、2+2)。

硬币2 · 填 dp[5]:面额 2 收尾,dp[5] += dp[3]=2,从 1 涨到 3。现在只用 1 和 2,凑 5 已经有 3 种。还差面额 5 没放。

加入硬币5 · dp[1..4] 不变:换面额 5。内层金额从 coin=5 起步,所以金额 小于 5 的格子这一轮碰都不碰(不可能用上一枚 5),dp[1] 到 dp[4] 保持不变。只剩 dp[5] 要更新。

硬币5 · 填 dp[5]:dp[5] += dp[5−5]=dp[0]=1:多出来的这一种就是单独一枚 5。dp[5] 从 3 变成 4。三种硬币全放完了。

答案:表格最后一格 dp[5] = 4 就是答案,对应那 4 种组合:5、2+2+1、2+1+1+1、1×5。返回 4

完全背包内层从小到大(硬币可重复),0-1 背包内层从大到小(每个只用一次)。求组合数外层套物品、内层套容量;求排列数把两层对调。这一句记牢,一类题全通。

参考代码(Python)

Python
1def change(self, amount, coins):
2    dp = [0] * (amount + 1)
3    dp[0] = 1                          # 凑 0 块:空组合,1
4    for coin in coins:                 # 外层:枚举硬币(保证组合不重复)
5        for j in range(coin, amount + 1):   # 内层:金额从小到大
6            dp[j] += dp[j - coin]      # 凑 j 多了「先凑 j−coin 再添一枚 coin」
7    return dp[amount]

复杂度分析

  • 时间复杂度:O(n × amount) —— n 种硬币各扫一遍金额,每格做一次加法
  • 空间复杂度:O(amount) —— 只用一行长度 amount+1 的 dp 数组

套路模板

记住骨架:外层物品、内层容量从小到大、dp[j] += dp[j−x]。求排列数就把两层循环对调,求最少枚数就把 += 换成 min(dp[j], dp[j−x]+1)。

Python
1# 每个物品可无限次使用,求凑出 W 的组合数
2dp = [0] * (W + 1)
3dp[0] = 1                            # 计数初值 1(最值题改 0 或 INF)
4for x in items:                      # 求组合数:外层物品
5    for j in range(x, W + 1):        # 完全背包:内层从小到大
6        dp[j] += dp[j - x]           # 计数用 +=;最值用 min/max
7return dp[W]

易错点

  • 错误写法: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)

以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握

下一题 →122. 买卖股票 II ← 返回题库