LeetCode 322. 零钱兑换

题目描述

给定一个整数数组 coins,表示不同面额的硬币;再给定一个整数 amount,表示总金额。

返回可以凑成总金额所需的最少硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

每种硬币的数量可以认为是无限的。

例如:

复制代码
输入:coins = [1,2,5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

原始思路:递归选或不选

最开始的方向是用递归搜索:站在第 i 个硬币的位置,考虑两种选择。

  • coins[i]:金额减少 coins[i],因为硬币可以重复使用,所以仍然停留在 i
  • 不选 coins[i]:跳到 i + 1,后续只考虑剩下的硬币。

因此核心结构是:

复制代码
dfs(i, amount) = min(
    dfs(i, amount - coins[i]) + 1,
    dfs(i + 1, amount)
)

这个方向是成立的,但原始代码把"无解"和"刚好凑完"都返回成了 0,导致无解分支也会被 Math.min 当成很优的答案。

问题出在哪里

用这个测试用例手动模拟:

复制代码
coins = [2]
amount = 3

如果只有硬币 2,显然不能凑出 3,答案应该是 -1

但原始递归里有这样的出口:

复制代码
如果 i 越界 或 amount == 0:
    返回 0

这会让两种完全不同的状态混在一起:

  • amount == 0:刚好凑完,返回 0 是合理的。
  • i >= coins.lengthamount != 0:硬币用完但没凑成,应该表示无解。

如果无解也返回 0,递归就会误以为"不需要硬币也能凑成",最终得到错误答案。

状态定义

定义 dfs(i, amount) 为:只使用 coins[i...] 中的硬币,凑成 amount 所需的最少硬币数。

这个状态必须同时包含 iamount

原因是同样要凑 amount = 6,下面两个状态的含义不同:

复制代码
dfs(0, 6)
dfs(1, 6)

它们剩余可选的硬币范围不同,所以不能只用一维 memo[amount] 缓存结果,而要使用二维 memo[i][amount]

状态转移

amount == 0 时,表示已经刚好凑完,返回 0

i >= coins.lengthamount != 0 时,表示所有硬币都尝试完了仍然凑不出目标金额,返回 INF

如果 coins[i] > amount,当前硬币不能选,只能跳过:

复制代码
dfs(i, amount) = dfs(i + 1, amount)

否则有两种选择:

复制代码
dfs(i, amount) = min(
    dfs(i, amount - coins[i]) + 1,
    dfs(i + 1, amount)
)

这里选当前硬币后仍然是 dfs(i, ...),不是 dfs(i + 1, ...),因为每种硬币可以无限使用。

记忆化搜索

伪代码:

复制代码
INF <- 一个足够大的数

函数 coinChange(coins, amount):
    memo <- 二维数组,大小为 coins.length * (amount + 1)
    ans <- dfs(0, amount)
    如果 ans >= INF,返回 -1
    否则返回 ans

函数 dfs(i, amount):
    如果 amount == 0:
        返回 0
    如果 i 已经越过最后一个硬币:
        返回 INF
    如果 memo[i][amount] 已经计算:
        返回 memo[i][amount]
    如果 coins[i] > amount:
        memo[i][amount] <- dfs(i + 1, amount)
        返回 memo[i][amount]
    memo[i][amount] <- min(
        dfs(i, amount - coins[i]) + 1,
        dfs(i + 1, amount)
    )
    返回 memo[i][amount]

对应 Java 代码:

复制代码
class Solution {
    private static final int INF = Integer.MAX_VALUE / 2;
    int[][] memo;

    public int coinChange(int[] coins, int amount) {
        memo = new int[coins.length][amount + 1];
        int ans = dfs(0, amount, coins);
        return ans >= INF ? -1 : ans;
    }

    public int dfs(int i, int amount, int[] coins) {
        if (amount == 0) {
            return 0;
        }
        if (i >= coins.length) {
            return INF;
        }
        if (memo[i][amount] != 0) {
            return memo[i][amount];
        }
        if (coins[i] > amount) {
            return memo[i][amount] = dfs(i + 1, amount, coins);
        }
        return memo[i][amount] = Math.min(
            dfs(i, amount - coins[i], coins) + 1,
            dfs(i + 1, amount, coins)
        );
    }
}

易错点

1. 无解状态不能返回 0

0 只应该表示"已经凑出目标金额后,还需要 0 枚硬币"。如果硬币用完仍未凑成,应该返回一个很大的数,让它在 Math.min 中表现为很差的选择。

2. 记忆化状态不能只看 amount

dfs(i, amount) 的结果不仅取决于剩余金额,也取决于当前还能使用哪些硬币。因此 memo[amount] 会混淆状态,应该使用 memo[i][amount]

3. 选当前硬币后 i 不变

因为每种硬币数量无限,选择 coins[i] 后仍然可以继续选它,所以递归到 dfs(i, amount - coins[i])

4. INF 不能直接用 Integer.MAX_VALUE

递归式里有 dfs(...) + 1。如果无解状态用 Integer.MAX_VALUE,再加 1 会溢出。因此可以使用 Integer.MAX_VALUE / 2 作为 INF

5. 最终判断不要只依赖相等

最终把无解转成 -1 时,用 ans >= INF 更稳,不依赖结果刚好等于某个固定的 INF 值。

建议测试用例

复制代码
coins = [2], amount = 3       -> -1
coins = [1,2,5], amount = 11  -> 3
coins = [1], amount = 0       -> 0
coins = [2,5,10,1], amount = 27 -> 4

复杂度分析

  • 时间复杂度:O(n * amount),其中 n 是硬币种类数,每个 (i, amount) 状态最多计算一次。
  • 空间复杂度:O(n * amount),主要来自二维 memo 数组;递归调用栈额外占用 O(n + amount) 级别空间。

复盘

这题的关键不是"能不能递归",而是递归状态和返回值语义要清楚。

本轮最核心的三个检查点是:

  • amount == 0 表示成功,返回 0
  • 硬币用完但 amount != 0 表示无解,返回 INF
  • memo 必须覆盖完整状态 iamount

最终递推式为:

复制代码
dfs(i, amount) = min(dfs(i, amount - coins[i]) + 1, dfs(i + 1, amount))
相关推荐
YH55269843 小时前
GPT‑5.6 Sol 原本支持 1M 上下文,Codex 现已放开此前限制,如何看待这次调整?
java·jvm·人工智能·gpt·算法·chatgpt
遥感知识服务5 小时前
从局部阈值、双极化到暗地表剔除:NASA OPERA DSWx-S1全球动态水体算法拆解
大数据·人工智能·深度学习·神经网络·算法·机器学习
晚风醉蝶5 小时前
1-13-TimSort
算法
ZJU_统一阿萨姆5 小时前
【算子开发】算子融合与Softmax_LayerNorm实现
人工智能·算法·语言模型·硬件架构
叩码以求索7 小时前
浅谈:前序遍历反转法求解N叉树的后序遍历
算法
北风toto8 小时前
中缀、前缀、后缀表达式
算法·软件设计师
听取WA声一片(无恶意)8 小时前
CSP-J/CSP-S 深度优先搜索(DFS)完全讲义
c++·算法·深度优先
码流怪侠9 小时前
2026年8月GitHub热榜深度拆解:Agent Skills席卷开源圈,一个“技能包“收割5万星
算法·程序员·github
hold?fish:palm9 小时前
30 两两交换链表中的节点
数据结构·算法·链表
Nil2089 小时前
leetcode 98验证二叉搜索树
算法·leetcode·职场和发展