518. 零钱兑换 II(Coin Change II)

频次 ★★★★ · 难度 🟡 · 高频:字节/美团

题目

不同面额硬币 coins(无限供应),凑目标金额 amount 的组合数。

示例

输入: amount = 5, coins = [1,2,5]
输出: 4  (5=5, 5=2+2+1, 5=2+1+1+1, 5=1+1+1+1+1)

思路

完全背包求组合数dp[j] 表示凑金额 j 的组合数。

dp[j] += dp[j - coin],初始化 dp[0] = 1。

关键:外层遍历硬币,内层遍历金额(正序)。这样保证”组合数”——每种硬币的选取顺序固定,不会因为顺序不同产生重复计数(如 {1,2} 和 {2,1} 算同一种)。

代码

public int change(int amount, int[] coins) {
    int[] dp = new int[amount + 1];
    dp[0] = 1;                               // 凑 0 元有一种方式(不选任何硬币)
    for (int coin : coins) {                  // 外层:硬币(保证组合数)
        for (int j = coin; j <= amount; j++) { // 内层:金额(正序 = 完全背包)
            dp[j] += dp[j - coin];
        }
    }
    return dp[amount];
}

复杂度

  • 时间:O(n × amount),n = 硬币种类数
  • 空间:O(amount)

边界条件

  • amount = 0:返回 1(空组合)
  • 无解(如 coins=[2], amount=3):dp[amount] 保持 0

变式

易错点

  • 内外层顺序决定组合 vs 排列:外层硬币 = 组合(coin 顺序固定),外层金额 = 排列(不同顺序算不同方案)
  • 内层正序(完全背包可重复选),不是逆序(0-1 背包)
  • dp[0] = 1 是计数 DP 的基值
  • 这道题和 377 的唯一区别就是内外层顺序——面试中常对比问

面试追问

  • 为什么外层硬币保证组合数? 硬币在外层时,遍历顺序固定为先处理 coin1 的所有金额,再处理 coin2……这样每种组合只被计算一次
  • 如果要求排列数(顺序不同算不同)? 内外层交换:外层金额,内层硬币 → 377 题

关联题