📌 题目链接:322. 零钱兑换 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:动态规划、完全背包、数组
⏱️ 目标时间复杂度:O(amount × n) (n 为硬币种类数)
💾 空间复杂度:O(amount)
🔍 题目分析
给定一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。
要求:计算并返回可以凑成总金额所需的最少硬币个数。若无法凑出,则返回 -1。
每种硬币数量 无限(典型「完全背包」问题特征)。
✅ 示例回顾
- 示例 1:
coins = [1, 2, 5], amount = 11→ 输出3(5+5+1) - 示例 2:
coins = [2], amount = 3→ 输出-1 - 示例 3:
coins = [1], amount = 0→ 输出0
💡 注意边界:
amount = 0时,不需要任何硬币,答案是0!
🧠 核心算法及代码讲解
本题是 「完全背包问题」 的经典变种 —— 不求最大价值,而是求 最小物品数量 来恰好装满背包。
🎯 为什么是「完全背包」?
- 物品:硬币(每种可选无限次)
- 背包容量:目标金额
amount - 目标:用最少数量的物品(硬币)填满背包(凑出
amount)
⚠️ 区别于 0-1 背包(每件物品只能用一次),完全背包允许重复选取。
📌 动态规划状态定义
设 dp[i] 表示 凑出金额 i 所需的最少硬币数。
✅ 初始状态:
dp[0] = 0:凑出金额 0 不需要任何硬币。- 其他
dp[i]初始化为一个“不可能的大值”(如amount + 1),便于后续取min。
🔁 状态转移方程:
对每个金额 i(从 1 到 amount),遍历所有硬币 coin:
if (coin <= i) {
dp[i] = min(dp[i], dp[i - coin] + 1);
}
🧩 含义:如果当前硬币
coin能用(coin <= i),那么凑出i的方案可以由i - coin的最优解 + 1 枚硬币得到。
🚫 无解判断:
若最终 dp[amount] > amount,说明无法凑出,返回 -1。
(因为最多用 amount 枚 1 元硬币就能凑出 amount,若结果比这还大,必无解)
💻 核心算法代码(带详细行注释)
// dp[i] 表示凑出金额 i 所需的最少硬币数
vector<int> dp(amount + 1, Max); // 初始化为一个大于可能答案的值
dp[0] = 0; // 基础情况:金额 0 需要 0 枚硬币
for (int i = 1; i <= amount; ++i) { // 遍历所有金额 1 ~ amount
for (int j = 0; j < (int)coins.size(); ++j) { // 遍历每种硬币
if (coins[j] <= i) { // 当前硬币面额不超过当前金额
dp[i] = min(dp[i], dp[i - coins[j]] + 1); // 状态转移:取最小值
}
}
}
// 若 dp[amount] 仍为初始大值,说明无法凑出
return dp[amount] > amount ? -1 : dp[amount];
✅ 此写法为 「自底向上」的动态规划(Bottom-up DP) ,避免了递归栈开销,效率更高。
🧩 解题思路(分步拆解)
-
明确问题类型:无限硬币 → 完全背包 → 动态规划。
-
定义状态:
dp[i] = 凑出金额 i 的最少硬币数。 -
初始化:
dp[0] = 0- 其余设为
amount + 1(比最大可能值amount还大,确保min能更新)
-
状态转移:
- 对每个金额
i,尝试所有硬币coin - 若
coin <= i,则dp[i] = min(dp[i], dp[i - coin] + 1)
- 对每个金额
-
结果判断:
- 若
dp[amount] > amount→ 无解,返回-1 - 否则返回
dp[amount]
- 若
🌟 类比爬楼梯:把
amount看作台阶,coins是每次能跨的步数,求最少步数登顶!
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(amount × n),其中 n = coins.size()。需遍历 amount 个状态,每个状态遍历 n 种硬币。 |
| 空间复杂度 | O(amount),仅需一维 dp 数组。 |
| 是否可优化空间? | 已是最优!一维 DP 无法再压缩。 |
| 面试高频点 | ✅ 完全背包 vs 0-1 背包区别 ✅ 状态定义与转移逻辑 ✅ 边界处理(amount=0) ✅ 无解判断技巧 |
💬 面试官可能会问:
- 如果硬币数量有限怎么办?→ 变成多重背包,需加计数维度。
- 能否输出具体用了哪些硬币?→ 需额外记录路径(parent 数组)。
- 能否用 BFS 解?→ 可以!将金额看作图节点,硬币为边,求最短路径(但空间可能更大)。
💻 完整代码
C++ 版本
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < (int)coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
vector<int> coins1 = {1, 2, 5};
cout << sol.coinChange(coins1, 11) << "\n"; // 输出: 3
// 测试用例 2
vector<int> coins2 = {2};
cout << sol.coinChange(coins2, 3) << "\n"; // 输出: -1
// 测试用例 3
vector<int> coins3 = {1};
cout << sol.coinChange(coins3, 0) << "\n"; // 输出: 0
return 0;
}
JavaScript 版本
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function(coins, amount) {
const MAX = amount + 1;
const dp = new Array(amount + 1).fill(MAX);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (const coin of coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
};
// 测试
console.log(coinChange([1, 2, 5], 11)); // 3
console.log(coinChange([2], 3)); // -1
console.log(coinChange([1], 0)); // 0
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!