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))
相关推荐
路径规划6661 小时前
(MATLAB代码) 船舶轨迹聚类:K-means 算法(260912)
算法·matlab·kmeans
wzdark7 小时前
基于堆的优先队列实现原理与复杂度分析4
算法
老王熬夜敲代码9 小时前
BLAKE3:最快的密码学哈希函数
算法·密码学·哈希算法
hansang_IR9 小时前
【题解】P4460 [CQOI2018] 解锁屏幕
c++·算法
见闻小天地9 小时前
简博斯JG-C系列彩色激光同轴位移计:摄像头模组对位与行程测量的技术价值观察
人工智能·算法
葫三生10 小时前
《论三生原理》神话学构想与“神话历史”理论、《古史中的神话》思路异同?
大数据·人工智能·科技·深度学习·算法
liliangcsdn11 小时前
因子分析指标概念与示例
算法·机器学习
SupL!12 小时前
LoftQ原理
算法
Navigator_Z13 小时前
LeetCode //C - 1240. Tiling a Rectangle with the Fewest Squares
c语言·算法·leetcode
稻米哟13 小时前
力扣100——双指针
算法·leetcode