322. 零钱兑换(原题卡片见 GRD 动态规划

考点归类

完全背包 · 最值型动态规划。硬币无限取用是完全背包的标志,与 0-1 背包的区别只在遍历方向。

思路推导

暴力思路:枚举所有硬币组合(数量不定、可重复),求凑出 amount 的最少枚数 → 指数级爆炸。

优化核心:子问题划分。凑 amount 的最少硬币数 = min(凑 (amount - coin) 的最少硬币数 + 1),对每个面值取最小。这就是状态转移:

dp[i] = min(dp[i], dp[i - coin] + 1)
  • dp[i]:凑出金额 i 所需的最少硬币数
  • dp[i-coin] + 1:最后一步用一枚 coin 从状态 i-coin 转移过来

为什么外层硬币、内层金额、i 正向?

for (let i = coin; i <= amount; i++) 从小到大遍历,dp[i-coin](更小下标)先于 dp[i] 被更新,且可能已含有当前这枚 coin → 同一枚硬币可被反复使用 → 完全背包。

若改成倒序 for (let i = amount; i >= coin; i--)dp[i-coin] 还是旧值 → 每枚硬币至多用一次 → 变成 0-1 背包。验证 coins=[1,3]amount=6:正序(完全背包)3+3 = 2 枚,倒序(0-1 背包)每个面值最多用一次,最大只能凑 3+1 = 4 < 6,返回 -1结果不一样(正序允许同枚硬币复用,倒序不允许)。

内外层交换(金额在外层)为什么答案仍正确?

本题只求”最少数量”,不关心硬币排列顺序,1+55+1 都是两枚。更严谨地说:金额在外层时,dp[i] 依赖的 dp[i-coin](更小下标)在上一轮外层循环已全部定稿,不会再被本次更新,因此取 Math.min 仍保证不重不漏——“最小值聚合”对遍历顺序不敏感。但对”求组合/排列总数”的题,聚合方式是加法,内外层顺序会改变答案——组合去重用外层硬币,排列计数的外层金额。

完整代码

var coinChange = function (coins, amount) {
    let dp = new Array(amount + 1).fill(Infinity);
    dp[0] = 0                      // 凑 0 元不需要硬币
    for (let coin of coins) {      // 外层:逐个面值
        for (let i = coin; i <= amount; i++) {  // 正向:完全背包,硬币无限用
            dp[i] = Math.min(dp[i], dp[i - coin] + 1)
            //      ↑ 不选这枚硬币   ↑ 选这枚,从凑 i-coin 的状态多花 1 枚
        }
    }
    return dp[amount] === Infinity ? -1 : dp[amount]
};

复杂度

  • 时间 O(amount × n),n 为硬币面值数
  • 空间 O(amount)

易错点

  1. Infinity 哨兵Infinity + 1 仍是 Infinity,因此 dp[amount] === Infinity 能判断凑不出来,返回 -1。不要用 0amount+1 之外不明确的初始值
  2. i 从 coin 起dp[i-coin] 最小取 dp[0],不会越界
  3. 遍历方向:正序 = 完全背包(无限取用),倒序 = 0-1 背包(每件一次),本题目的是无限硬币,必须正序

变体与举一反三


← 速查卡片见 GRD 动态规划