一、完整代码(精简注释)
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// dp[i]:凑出金额 i 所需的最少硬币数
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0; // 凑出0元需要0枚硬币
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.size(); j++) {
if (coins[j] <= i) { // 当前硬币不能超过金额i
// 选择当前硬币,更新最少硬币数
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
// 无法凑出返回-1,否则返回最少硬币数
return dp[amount] > amount ? -1 : dp[amount];
}
};
二、动态规划五步总结
1. 确定 dp 数组含义
dp[i] 表示凑出金额 i 所需要的最少硬币数量。
最终目标是求 dp[amount]。
2. 确定状态转移方程
\ \\boxed{dp\[i=\min(dpi,dpi-coins\[j]+1)} \]
-
dp[i]:当前凑出金额i的最少硬币数。 -
dp[i-coins[j]]:凑出剩余金额需要的最少硬币数。 -
+1:加上当前选择的这枚硬币。 -
min():在所有硬币选择中取最优解。
例如 coins = [1,2,5],计算 dp[6]:
选择1元:dp[5] + 1 = 2
选择2元:dp[4] + 1 = 3
选择5元:dp[1] + 1 = 2
dp[6] = min(2,3,2) = 2
3. 初始化 dp 数组
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
-
第一个
amount+1:数组长度,包含dp[0]到dp[amount]。 -
第二个
amount+1:初始化为一个不可能的较大值,表示暂时无法凑出。 -
dp[0]=0:凑出金额 0 不需要硬币。
这里用 amount+1 代替 INT_MAX,既能表示无解,又能避免状态转移时整数溢出。
4. 确定遍历顺序
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.size(); j++) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
-
外层循环:从小金额到大金额,保证计算当前状态时,之前的小金额已经计算完成。
-
内层循环:遍历所有硬币,寻找最少硬币数量。
-
coins[j] <= i:防止i-coins[j]为负数,导致数组越界。
由于每种硬币可以无限使用,因此同一枚硬币可以在不同状态转移中重复选择。
5. 确定返回值
return dp[amount] > amount ? -1 : dp[amount];
-
如果
dp[amount] > amount,说明无法凑出,返回-1。 -
否则返回最少硬币数量。
三、复杂度分析
设 n 为硬币种类数。
-
时间复杂度: \(O(amount \times n)\),每个金额都需要遍历所有硬币。
-
空间复杂度: \(O(amount)\),使用一维
dp数组存储结果。
四、核心思路总结
这道题属于完全背包问题中的求最小值问题,因为每种硬币可以无限次使用。
核心思想是:
要求凑出金额 i 的最少硬币数,就枚举最后选择的硬币,利用剩余金额的最优解,再加上当前这枚硬币。
记住三个关键点:
-
dp[i]存储的是凑出金额i的最优解,而不是具体硬币组合。 -
dp[i-coins[j]]+1表示选择一枚当前硬币后的总数量。 -
amount+1作为无解标记,是这道题初始化的关键技巧。
这与之前做的「完全平方数」是同一种动态规划思路,只不过一个枚举硬币面值,一个枚举完全平方数。