算法 - 零钱兑换 (Coin Change)
1. 题目
给你一个整数数组 coins 表示不同面额的硬币,以及一个整数 amount 表示总金额。计算凑成总金额所需的最少的硬币个数。每种硬币可以无限使用。如果无法凑出,返回 -1。
示例:
1 | 输入: coins = [1, 2, 5], amount = 11 |
2. 解题思路
这是完全背包求最少数量的经典题。贪心(每次取最大面额)在这里不成立(如 coins=[1,3,4], amount=6 贪心得 4+1+1=3 枚,最优是 3+3=2 枚),必须用 DP。
定义 dp[i] = 凑出金额 i 所需的最少硬币数。
转移:对于金额 i,枚举最后一枚用的硬币 c(c <= i),则
1 | dp[i] = min(dp[i - c]) + 1 |
初始化:
dp[0] = 0(凑 0 需要 0 枚)。- 其余
dp[i] = amount + 1(当作正无穷,因为最多也不会超过 amount 枚),或Infinity。
最后若 dp[amount] > amount 说明没被更新过,返回 -1。
- 时间复杂度:
O(amount × coins数)。 - 空间复杂度:
O(amount)。
3. TypeScript 实现
1 | function coinChange(coins: number[], amount: number): number { |
4. 面试延伸
- 零钱兑换 II(518,凑法总数):把
min+1换成dp[i] += dp[i-c],且外层遍历硬币、内层遍历金额以保证组合不计顺序,是完全背包计数的标准写法。 - 完全背包 vs 0-1 背包:内层金额遍历方向决定「可重复用」——完全背包正序(能用多次),0-1 背包逆序(每枚只用一次)。这是背包问题的核心分水岭。
- 要输出具体用了哪些硬币:额外记录
choice[i]前驱,最后回溯。 - 常见坑:求「最少」用
min,求「方法数」用「加法」;初始化dp[0]的含义要和题目对齐。 - 若问「为什么不用贪心」,一定要举
[1,3,4] 凑 6的反例,这是本题最重要的理解点。
难度:中等 | LeetCode 322 题 | 完全背包基石