题目描述
给定一个整数数组 coins(不同面额的硬币)以及一个整数 amount(总金额),计算并返回凑成总金额所需的最少硬币个数 。如果没有任何硬币组合能组成总金额,返回 -1。
可以认为每种硬币的数量是无限的。
示例:
- 输入:
coins = [1, 2, 5], amount = 11→ 输出:3(11 = 5 + 5 + 1) - 输入:
coins = [2], amount = 3→ 输出:-1 - 输入:
coins = [1], amount = 0→ 输出:0
解题思路总览
| 方法 | 思路 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 动态规划 | dp[i] 表示凑成金额 i 的最少硬币数,状态转移 dp[i] = min(dp[i], dp[i-coin]+1) |
O(amount × len(coins)) | O(amount) |
| BFS | 从金额 0 出发,每次加上一种硬币,求最少步数 | O(amount × len(coins)) | O(amount) |
本题采用**动态规划(完全背包)**方法。
完整代码
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1);
int n = coins.size();
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < n; j++) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
};
算法流程图
输入: coins = [1, 2, 5], amount = 11
初始化:
dp[0] = 0
dp[1...11] = amount+1 = 12 (表示正无穷,即无法凑成)
i = 1:
j = 0, coin = 1 <= 1
dp[1] = min(12, dp[0]+1) = 1
j = 1, coin = 2 > 1, 跳过
j = 2, coin = 5 > 1, 跳过
dp[1] = 1
i = 2:
j = 0, coin = 1 <= 2
dp[2] = min(12, dp[1]+1) = 2
j = 1, coin = 2 <= 2
dp[2] = min(2, dp[0]+1) = 1
j = 2, coin = 5 > 2, 跳过
dp[2] = 1
i = 3:
j = 0, coin = 1 <= 3
dp[3] = min(12, dp[2]+1) = 2
j = 1, coin = 2 <= 3
dp[3] = min(2, dp[1]+1) = 2
j = 2, coin = 5 > 3, 跳过
dp[3] = 2
i = 4:
j = 0, coin = 1 <= 4
dp[4] = min(12, dp[3]+1) = 3
j = 1, coin = 2 <= 4
dp[4] = min(3, dp[2]+1) = 2
j = 2, coin = 5 > 4, 跳过
dp[4] = 2
i = 5:
j = 0, coin = 1 <= 5
dp[5] = min(12, dp[4]+1) = 3
j = 1, coin = 2 <= 5
dp[5] = min(3, dp[3]+1) = 3
j = 2, coin = 5 <= 5
dp[5] = min(3, dp[0]+1) = 1
dp[5] = 1
... (继续迭代直到 i = 11)
i = 11:
j = 0, coin = 1 <= 11
dp[11] = min(12, dp[10]+1)
j = 1, coin = 2 <= 11
dp[11] = min(dp[11], dp[9]+1)
j = 2, coin = 5 <= 11
dp[11] = min(dp[11], dp[6]+1) = 3
最终 dp[11] = 3
输出: 3
逐行解析
cpp
vector<int> dp(amount + 1, amount + 1);
含义: 创建大小为 amount+1 的数组,dp[i] 表示凑成金额 i 的最少硬币数。初始化为 amount+1 作为"正无穷",因为凑成 amount 最多需要 amount 个 1 元硬币,所以 amount+1 一定大于任何可行解。
cpp
int n = coins.size();
含义: 记录硬币种类数量,方便后续循环使用。
cpp
dp[0] = 0;
含义: 基础情况,凑成金额 0 需要 0 个硬币。
cpp
for (int i = 1; i <= amount; i++)
含义: 从金额 1 到 amount 依次计算每个金额的最少硬币数。
cpp
for (int j = 0; j < n; j++)
含义: 遍历所有硬币面额,尝试用每种硬币来凑当前金额。
cpp
if (coins[j] <= i)
含义: 只有当硬币面额不超过当前要凑的金额 i 时,才能使用这枚硬币(因为不能有负数)。
cpp
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
含义: 状态转移方程。如果使用面额为 coins[j] 的硬币,那么剩下的 i-coins[j] 金额需要 dp[i-coins[j]] 个硬币,再加上当前的 1 枚硬币,总共 dp[i-coins[j]]+1 个。取较小值更新 dp[i]。
cpp
return dp[amount] == amount + 1 ? -1 : dp[amount];
含义: 如果 dp[amount] 仍然是初始值 amount+1,说明没有任何硬币组合能凑成 amount,返回 -1;否则返回实际计算出的最少硬币数。
复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(amount × len(coins)) | 外层循环 amount 次,内层循环 len(coins) 次 |
| 空间复杂度 | O(amount) | 需要大小为 amount+1 的 dp 数组 |
面试追问 FAQ
| 问题 | 答案 |
|---|---|
为什么初始化为 amount+1 而不是 INT_MAX? |
因为 INT_MAX 参与 dp[i-coins[j]]+1 计算时会溢出,而 amount+1 既足够大又不会溢出 |
| 完全背包和 0-1 背包的区别? | 0-1 背包每种物品只能用一次,内层循环倒序;完全背包每种物品无限使用,内层循环正序 |
| 如何输出具体硬币组合? | 额外记录每个状态是从哪个硬币转移来的,最后回溯即可 |
| 如果要求硬币组合数最少怎么办? | 使用 dp[i] += dp[i-coin] 而不是 min,统计组合个数 |
amount = 0 时返回什么? |
返回 0,因为不需要任何硬币就能凑成 0 元 |
相关题目
| 题号 | 题目 | 难度 | 核心思路 |
|---|---|---|---|
| 322 | 零钱兑换 | 中等 | 完全背包 |
| 279 | 完全平方数 | 中等 | 动态规划 |
| 518 | 零钱兑换 II | 中等 | 完全背包(组合数) |
| 416 | 分割等和子集 | 中等 | 0-1 背包 |
总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 完全背包动态规划,枚举所有硬币作为最后一个加数 |
| 状态定义 | dp[i] = 凑成金额 i 的最少硬币数 |
| 状态转移 | dp[i] = min(dp[i], dp[i-coin]+1) |
| 初始化 | dp[0] = 0,其他为 amount+1(正无穷) |
| 返回值 | dp[amount] == amount+1 ? -1 : dp[amount] |