题目描述

✅ 322. 零钱兑换

image-20260928190908994

image-20260928190908995

题意分析

给定若干种正整数面额 coins,每种硬币都有无限枚。要求恰好凑出金额 amount,并让使用的硬币总数最少;无法凑出时返回 -1。

求的是最少枚数,不是凑钱方案数,也不要求输出具体用了哪些硬币。目标金额为 0 时不需要任何硬币,答案是 0。面额没有特殊规律,因此不能假设每次优先使用最大面额就能得到最优解。

解法:完全背包动态规划

核心思路

[!blue]

对于一个金额,如果决定再放入一枚面额为 coin 的硬币,就只需凑出剩余金额 money - coin。因此可以保存每个较小金额的最优答案,再逐步计算更大金额,避免反复枚举完整的硬币组合。

用 dp[money] 表示使用目前已经处理过的面额,恰好凑出 money 所需的最少硬币数。初始还没有可用面额,只有 dp[0] = 0;其他金额都设为不可达。每加入一种面额 coin,需要在两种选择之间取最小值:

  • 不使用当前面额,保留原来的 dp[money]。
  • 至少使用一枚当前硬币,先凑出 money - coin,再加一枚,候选值为 dp[money - coin] + 1。

因此转移为 dp[money] = min(dp[money], dp[money - coin] + 1)。任何使用当前面额的最优方案都能拆去一枚 coin;剩余部分如果还能用更少硬币凑出,原方案就不是最优,所以这个转移不会遗漏更好的答案。

金额必须从 coin 向 amount 正序遍历。由于面额为正,money - coin 更小,读取时已经允许使用本轮的硬币;在它的基础上再加一枚,就实现了同一种面额的无限次使用。如果倒序更新,读到的仍是本轮更新前的状态,就会把当前硬币限制为最多使用一次。

不可达标记取 amount + 1:每枚硬币至少值 1,任何可行方案都不会用超过 amount 枚硬币。对不可达状态再加一仍大于标记,不会把另一个不可达状态更新成可达;题目金额上限为 10^4,加一也不会溢出。所有面额处理完后,dp[amount] 就是答案,仍为标记则无解。

解题步骤

  1. 创建长度为 amount + 1 的 dp,设 dp[0] = 0,其余位置为 amount + 1。
  2. 依次枚举硬币面额 coin。若它大于目标金额,本轮金额循环自然跳过。
  3. 从 coin 到 amount 正序枚举 money,用 dp[money - coin] + 1 更新 dp[money] 的最小值。
  4. 若 dp[amount] 仍为不可达标记,返回 -1;否则返回最少硬币数。

代码实现

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 大于所有可能的硬币数,同时给加一留出安全空间。
        int unreachable = amount + 1;
        int[] dp = new int[amount + 1];

        Arrays.fill(dp, unreachable);
        // 空金额不需要硬币,是其他可达状态的递推起点。
        dp[0] = 0;

        for (int coin : coins) {
            // 金额正序读取本轮已更新状态,允许当前硬币重复使用。
            for (int money = coin; money <= amount; money++) {
                dp[money] = Math.min(dp[money], dp[money - coin] + 1);
            }
        }

        return dp[amount] == unreachable ? -1 : dp[amount];
    }
}
func coinChange(coins []int, amount int) int {
    // 大于所有可能的硬币数,同时给加一留出安全空间。
    unreachable := amount + 1
    dp := make([]int, amount+1)
    // 只把正金额设为不可达,金额零保留默认零作为递推起点。
    for i := 1; i <= amount; i++ {
        dp[i] = unreachable
    }

    for _, coin := range coins {
        // 金额正序读取本轮已更新状态,允许当前硬币重复使用。
        for money := coin; money <= amount; money++ {
            candidate := dp[money-coin] + 1
            if candidate < dp[money] {
                dp[money] = candidate
            }
        }
    }

    if dp[amount] != unreachable {
        return dp[amount]
    }
    return -1
}

复杂度分析

设硬币种数为 $n$,目标金额为 $S$。

  • 时间复杂度:$O(n(S+1))$。初始化需要 $O(S+1)$,每种面额最多更新 $S$ 个金额;写成 $S+1$ 也包含目标为零时对面额的遍历。
  • 空间复杂度:$O(S+1)$,状态数组保存从 $0$ 到 $S$ 的答案。

关键点总结

[!green]

  • 每加入一种面额,都比较“不用它”和“在较小金额上再用一枚”的最少枚数。
  • 正序更新让本轮结果继续参与后续转移,对应每种硬币无限使用。
  • dp[0] 是所有可达状态的起点,不可达标记要大于任何合法答案。

易错点总结

[!yellow]

  • 正金额不能初始化为 0,否则会把尚未凑出的金额当成无需硬币就能到达;dp[0] 也不能设为不可达。
  • 金额倒序遍历会把完全背包改成每种硬币最多使用一次,改变题意。
  • 面额没有贪心所需的特殊性质,优先用大硬币不保证剩余金额可达或总枚数最少。
  • 直接用整数最大值表示不可达,再执行 + 1,可能溢出为负数并被误选为最优值。
  • 无解必须把内部的不可达标记转换为 -1,金额为零则应返回 0。

相似题目

题目 难度 关联与区别
518. 零钱兑换 II 中等 候选面额可重复使用,本题最小化硬币数量,原题累计不区分顺序的组合数。
279. 完全平方数 中等 把可选面额限制为完全平方数后,同样求凑成目标所需的最少项数。
377. 组合总和 Ⅳ 中等 按金额或目标长度累积可重复选择的结果;本题求最少硬币数,该题先枚举目标以统计有序方案。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/89162014
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!