零钱兑换 II 图解题解
这道题到底在问什么
- 输入
- coins=[1,2,5], amount=5
- 输出
- 4
最优解:为什么这么做
一句话答案:LeetCode 518 零钱兑换 II 求硬币无限取凑成金额的组合数。完全背包:dp[w] 记凑金额 w 的方案数,外层按硬币、内层按金额从小到大累加,如此数的是组合非排列。O(amount×k) 时间、O(amount) 空间。
零钱兑换 II 到底在数什么
面额固定、每种能无限取用的硬币 coins=[1,2,5],问凑出金额 amount=5 有多少种组合。组合只看各面额用几枚、不计先后。凑 5 元有四种:5 个 1、3 个 1 加 1 个 2、1 个 1 加 2 个 2、1 个 5,答案 4。
为什么不能把所有凑法枚举一遍
把每种硬币各用几枚的所有搭配摆出来逐个验证,面额一多、金额一大,数量成倍翻,试不完。而且不少搭配前半截重叠、被反复重算——凑到 3 元再往上加,和从头拼是同一批中间结果,记下来复用就是动态规划。
dp 数组怎么定义,dp[0] 为什么必须是 1
dp[w] 表示凑出金额 w 的组合数,答案是 dp[amount]。起点 dp[0]=1:凑出金额 0 只有「什么都不拿」这一种。这个 1 是所有累加的种子,写成 0 的话每次 dp[w]+=dp[w-c] 都在加 0,整张表全归零。
这里套的是完全背包(背包 = 把物品装进定容的包;硬币是物品、金额是容量,每种可取无限枚)。
为什么硬币在外层、金额在内层数的才是组合数
转移 dp[w]+=dp[w-c]:凑金额 w,就在已凑出 w-c 的方案上添一枚面额 c。数出组合还是排列,全看两层循环谁在外。外层按硬币枚举,轮到 c 时 dp[w-c] 里累加过的只有「排在 c 之前引入的面额」,所以任何一种组合都只在它用到的最大面额那轮生成一次,天然不重复;5=1+2+2 只在面额 2 那轮按 1 后接 2 生成,不会冒出 2 在前的版本。
把循环换个个儿、金额在外硬币在内,就成了对每个金额都重挑所有面额,1+2 与 2+1 各数一次,得到的是排列数,那是 LeetCode 377 组合总和 Ⅳ。
内层金额还必须从小到大:dp[w] 要用本轮(已加过 c)的 dp[w-c] 新值,这是同种硬币反复取用的通道;从大到小则 dp[w-c] 停在没加 c 的旧值,每种只能用一次,成了 01 背包(每样只取一次)。
拿 coins=[1,2,5]、amount=5 手工填一遍
dp 初值 [1,0,0,0,0,0],下标 0 到 5。处理面额 1,w 从 1 到 5,dp[w]+=dp[w-1]:dp[1]=1、dp[2]=1,后面顺次也变 1,整行 [1,1,1,1,1,1]。
处理面额 2,w 从 2 到 5,dp[w]+=dp[w-2]:dp[2]=1+dp[0]=2,dp[3]=1+dp[1]=2,dp[4]=1+dp[2]=3,dp[5]=1+dp[3]=3,整行 [1,1,2,2,3,3]。处理面额 5,只到 w=5:dp[5]=3+dp[0]=4,就是答案。
复杂度多少,哪三处最容易写错
时间 O(amount×k),k 是面额种数:外层每种硬币一轮、内层从面额扫到 amount。空间 O(amount):dp 压成一维就地覆盖旧值,这种把二维表滚成一行的写法叫滚动数组。
三处最常踩坑:循环写反、金额在外硬币在内,答案变成排列数,样例会比 4 大;内层从大到小,同种硬币只能用一次,退化成 01 背包,方案少一批;dp[0] 忘置 1,累加没种子,dp[amount] 会是 0。
▶ 动画逐步走查(共 34 步)——想跟着动画一帧帧对照就展开
- 3完全背包求组合数的核心:「不选这种」+「选这种(可重复)」相加。按硬币种类分行,能天然避免重复计数。
- 4地基:还没引入任何硬币(∅ 行)。凑出金额 0 只有「什么都不选」这 1 种方案,dp[0][0]=1。
- 5同样在 ∅ 行:一种硬币都不用,却要凑出 1~5 元,谁也凑不出,所以这几格全是 0。哨兵行铺好。
- 6算 dp[1][1](前 1 种硬币、凑 1 元):不选面额 1 → 沿用上行 dp[0][1]=0;选面额 1(可重复)→ 看本行左边 dp[1][0]=1。两条相加。
- 7两条路相加 0+1=1,填进 dp[1][1]。引入面额 1 后,凑 1 元的组合数变成 1。
- 8算 dp[1][2](前 1 种硬币、凑 2 元):不选面额 1 → 沿用上行 dp[0][2]=0;选面额 1(可重复)→ 看本行左边 dp[1][1]=1。两条相加。
- 9两条路相加 0+1=1,填进 dp[1][2]。引入面额 1 后,凑 2 元的组合数变成 1。
- 10算 dp[1][3](前 1 种硬币、凑 3 元):不选面额 1 → 沿用上行 dp[0][3]=0;选面额 1(可重复)→ 看本行左边 dp[1][2]=1。两条相加。
- 11两条路相加 0+1=1,填进 dp[1][3]。引入面额 1 后,凑 3 元的组合数变成 1。
- 12算 dp[1][4](前 1 种硬币、凑 4 元):不选面额 1 → 沿用上行 dp[0][4]=0;选面额 1(可重复)→ 看本行左边 dp[1][3]=1。两条相加。
- 13两条路相加 0+1=1,填进 dp[1][4]。引入面额 1 后,凑 4 元的组合数变成 1。
- 14算 dp[1][5](前 1 种硬币、凑 5 元):不选面额 1 → 沿用上行 dp[0][5]=0;选面额 1(可重复)→ 看本行左边 dp[1][4]=1。两条相加。
- 15两条路相加 0+1=1,填进 dp[1][5]。引入面额 1 后,凑 5 元的组合数变成 1。
- 16算 dp[2][1]:金额 1 比面额 2 还小,这种硬币用不上,只能照搬上一行 dp[1][1]=1。
- 17照搬得到 1,填进 dp[2][1]。
- 18算 dp[2][2](前 2 种硬币、凑 2 元):不选面额 2 → 沿用上行 dp[1][2]=1;选面额 2(可重复)→ 看本行左边 dp[2][0]=1。两条相加。
- 19两条路相加 1+1=2,填进 dp[2][2]。引入面额 2 后,凑 2 元的组合数变成 2。
- 20算 dp[2][3](前 2 种硬币、凑 3 元):不选面额 2 → 沿用上行 dp[1][3]=1;选面额 2(可重复)→ 看本行左边 dp[2][1]=1。两条相加。
- 21两条路相加 1+1=2,填进 dp[2][3]。引入面额 2 后,凑 3 元的组合数变成 2。
- 22算 dp[2][4](前 2 种硬币、凑 4 元):不选面额 2 → 沿用上行 dp[1][4]=1;选面额 2(可重复)→ 看本行左边 dp[2][2]=2。两条相加。
- 23两条路相加 1+2=3,填进 dp[2][4]。引入面额 2 后,凑 4 元的组合数变成 3。
- 24算 dp[2][5](前 2 种硬币、凑 5 元):不选面额 2 → 沿用上行 dp[1][5]=1;选面额 2(可重复)→ 看本行左边 dp[2][3]=2。两条相加。
- 25两条路相加 1+2=3,填进 dp[2][5]。引入面额 2 后,凑 5 元的组合数变成 3。
- 26算 dp[3][1]:金额 1 比面额 5 还小,这种硬币用不上,只能照搬上一行 dp[2][1]=1。
- 27照搬得到 1,填进 dp[3][1]。
- 28算 dp[3][2]:金额 2 比面额 5 还小,这种硬币用不上,只能照搬上一行 dp[2][2]=2。
- 29照搬得到 2,填进 dp[3][2]。
- 30算 dp[3][3]:金额 3 比面额 5 还小,这种硬币用不上,只能照搬上一行 dp[2][3]=2。
- 31照搬得到 2,填进 dp[3][3]。
- 32算 dp[3][4]:金额 4 比面额 5 还小,这种硬币用不上,只能照搬上一行 dp[2][4]=3。
- 33照搬得到 3,填进 dp[3][4]。
- 34算 dp[3][5](前 3 种硬币、凑 5 元):不选面额 5 → 沿用上行 dp[2][5]=3;选面额 5(可重复)→ 看本行左边 dp[3][0]=1。两条相加。
- 35两条路相加 3+1=4,填进 dp[3][5]。引入面额 5 后,凑 5 元的组合数变成 4。
- 36最右下角 dp[3][5]=4,就是用全部三种硬币凑出 5 元的组合总数。
⚠️ 容易写错的地方
✗ 错:两层循环写反(金额在外、硬币在内)
✓ 对:硬币在外、金额在内
反了会把 1+2 和 2+1 算成两种,求出的是「排列数」不是「组合数」
✗ 错:内层金额从大到小
✓ 对:完全背包内层金额从小到大
从小到大才允许同种硬币重复使用;从大到小是 01 背包每种只用一次
✗ 错:dp[0] 忘记置 1
✓ 对:dp[0]=1
凑 0 元有「空选」这 1 种方案,是所有累加的起点,置 0 会全盘归零
完整代码(Python / C++ / Java)
Python
def change(amount, coins):
dp = [1] + [0] * amount # dp[0]=1
for c in coins: # 外层枚举硬币
for w in range(c, amount + 1):
dp[w] += dp[w - c]
return dp[amount]C++
int change(int amount, vector<int>& coins){
vector<long> dp(amount + 1, 0);
dp[0] = 1;
for(int c : coins)
for(int w = c; w <= amount; ++w)
dp[w] += dp[w - c];
return (int)dp[amount];
}Java
int change(int amount, int[] coins){
int[] dp = new int[amount + 1];
dp[0] = 1;
for(int c : coins)
for(int w = c; w <= amount; w++)
dp[w] += dp[w - c];
return dp[amount];
}复杂度
时间
O(amount × k)
k=面额种数,每种硬币扫一遍金额
空间
O(amount)
滚动成一维 dp 数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 零钱兑换 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
本题求组合数,和 LeetCode 322 求最少硬币数有什么区别?+
状态骨架都搭在 dp[w-c] 上,区别在转移。LeetCode 322 求凑出金额的最少硬币数,转移是 dp[w]=min(dp[w-c]+1),取极值;本题求方案总数,转移是 dp[w]+=dp[w-c],做累加。一个求最优、一个求计数,把 min 换成加号即可。
为什么硬币放外层就不会重复计数?+
外层固定了面额被引入的先后。处理某种面额时只在「之前已考虑的面额」上追加它,于是每种组合只在它面额最大那枚被处理的那轮统计一次,天然只数无序组合、不数排列。金额放外层就丢了这个顺序约束,同一组硬币的不同排列会各数一次,那求的是排列数(LeetCode 377)。
dp 能不能从二维降成一维?+
能。二维 dp[i][w] 表示前 i 种硬币凑金额 w 的方案数,转移只用到上一行同列 dp[i-1][w] 和本行左边 dp[i][w-c]。把行压掉、用一维 dp[w] 就地累加,内层金额从小到大保证读到的 dp[w-c] 已是本行新值,正好对上二维的本行左边。空间从 O(amount×k) 降到 O(amount)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 零钱兑换 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。