文章摘要
本文介绍了使用动态规划解决硬币找零问题的方法。给定不同面额的硬币和总金额,目标是找到使用最少硬币的组合。通过建立dp数组记录每个金额的最优解,初始化dp[0]=0,其余为无穷大。递推过程中,对每种金额尝试所有硬币面额,更新最优解。若最终解大于总金额,说明无解返回-1。C#实现展示了具体代码,时间复杂度为O(amount*硬币种数)。文章还提到可扩展输出具体硬币组合或转换为0/1背包问题。
一、问题描述
给定若干种面额的硬币,每种面额可用无限次,要求用最少硬币组合,凑出指定金额N。如果无法凑出,则返回-1。
例如:
- 硬币面额:[1, 2, 5]
- 金额:11
- 答案:3(5+5+1)
二、建模分析
- 输入:面额数组 coins[],整数金额 amount
- 输出:凑成金额 amount 最少需要几枚硬币
本质:
- 完全背包、最少个数、无序组合
- 用dp数组记录每个金额的最优解
三、动态规划思路
- dp[i] = 凑成金额i最少需要多少硬币
- 初值:dp[0]=0, 其它dp[i]=无穷大
- 递推:对于每种硬币,尝试“用一次”,即dp[i]=min( dp[i], dp[i-coin]+1 ) (如果i>=coin)
四、C#实现及逐行解释
using System;
class Solution {
public int CoinChange(int[] coins, int amount) {
// 1. 初始化dp数组,dp[i]是金额i最少硬币数
int[] dp = new int[amount + 1];
for (int i = 1; i <= amount; i++)
dp[i] = amount + 1; // 用amount+1视为无穷大(不可能的极大值)
dp[0] = 0; // 金额0不需要硬币
// 2. 状态转移
for (int i = 1; i <= amount; i++) { // 对每种金额
foreach (int coin in coins) { // 枚举每一种硬币
if (i >= coin) { // 只有能用(金额够)
dp[i] = Math.Min(dp[i], dp[i - coin] + 1); // 用一次coin,看是否更优
}
}
}
// 3. 判断是否可达
if (dp[amount] > amount)
return -1; // 无法找零
else
return dp[amount]; // 找零的最优硬币数
}
}
逐行解释
int[] dp = new int[amount + 1];
- 创建dp数组,dp[i]存凑i元最优硬币数
for (int i = 1; i <= amount; i++)
dp[i] = amount + 1;
- 初始化除0外所有金额为最大值
dp[0] = 0;
- 金额为0时不需要硬币
for (int i = 1; i <= amount; i++) {
foreach (int coin in coins) {
if (i >= coin) {
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
}
}
}
- 对每个金额i,枚举所有面额,如果金额够,就看上一步再加1是否更优(最少硬币)
if (dp[amount] > amount)
return -1;
else
return dp[amount];
- 金额amount大于最大可能硬币数,说明无解,否则返回最优解
使用举例
int[] coins = {1, 2, 5};
int amount = 11;
var sol = new Solution();
Console.WriteLine(sol.CoinChange(coins, amount)); // 输出3
时间复杂度
- O(amount * 硬币种数)
- 空间复杂度 O(amount)
五、拓展
- 若要输出实际硬币组合,可增parent数组反向回溯
- 若硬币数量有限,是0/1背包问题
我们以本句为核心,逐步剖析后二重循环中的动态规划原理和具体执行流程。
重点讲解下面关键部分:
for (int i = 1; i <= amount; i++) {
foreach (int coin in coins) {
if (i >= coin) {
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
}
}
}
1. 外循环:金额i
for (int i = 1; i <= amount; i++) {
...
}
作用:
- 从1元到目标金额amount,每个金额都求“最少硬币数”
- 逐步填表dp,低金额的最优方案可为高金额复用
2. 内循环:枚举所有硬币
foreach (int coin in coins) {
...
}
作用:
- 针对每个金额i,尝试所有可用的硬币面额coin
- 问“能不能用1枚coin凑i?”
3. 判定与转移:“剩余金额能凑吗?”
if (i >= coin) {
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
}
- 只考虑i比coin大(即当前金额至少大于某一面额),否则无法使用此硬币。
- 假设此刻你手上有i元,要选择用不用户coin,若用一枚coin,则还剩i-coin元。
- **dp[i-coin]**:这部分已知“最优解”,就是“如何用最少硬币凑齐i-coin(比i小的某个金额)”;已在之前循环完成。
再加1:
- 若现在用一枚coin,那么总硬币数就是“凑i-coin最少方案+1枚”
再与现有最优解比较,取更小者:
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);- 可能你以前用面额a和b拼出来“巧”,现在用coin凑出来的方案更优,要及时替换。
4. 形象流程举例
以例子:coins=[1,2,5], amount=7
我们跟踪 i=4 时的状态
- coins = [1,2,5]
- 目标:填dp[4]
内循环:
-
coin=1:
- i-coin = 3,dp[3] 若已算好(已知3元时最少硬币数,比如dp[3]=2)
- 如果用1元,则需要:dp[3]+1=3
- dp[4]=min(当前dp[4](初值为无穷),3)=3
-
coin=2:
- i-coin=2,dp[2]=1
- 用一枚2元,则dp[2]+1=2
- dp[4]=min(3,2)=2
-
coin=5:
- 4<5,不满足if语句,不执行
最后
- dp[4]=2
也就是7种情况下用2元+2元,凑出4元只需2枚硬币。
5. 为什么是“最优子结构”?
- 每个金额i,在已经知道所有比i小的金额(i-coin)dp值后,用子问题最小解+1再组合,这就是“最优子结构”
- 递推保证不会漏掉所有有效的组合
6. 手动画表(以coins=[1,2,5], amount=7为例)
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| dp[i] | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 |
例如填 dp[6] 时:
- 用 coin=1, dp[5]=1, 1+1=2
- 用 coin=2, dp[4]=2, 2+1=3
- 用 coin=5, dp[1]=1, 1+1=2
- 取最小,dp[6]=2
7. 伪代码小结
for (int i = 1; i <= amount; i++) {
foreach (int coin in coins) {
if (i >= coin) {
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
}
}
}
含义:
- “当前金额i”可由“之前的最优硬币组合”加一个新硬币递推出最优解
8. 总体流程图
- 对每个i
- 遍历每个coin
- 如果i可以用coin组成
- 用“dp[i-coin] + 1”改善当前i的最优解
- 如果i可以用coin组成
- 遍历每个coin
9. 注意
- 这种方法总能找出最少枚数,因为动态规划表格是依次递推,先算小金额后算大金额。
- 若题目问“不关心最少,而是种类数/所有组合数”,那做法不同。
983

被折叠的 条评论
为什么被折叠?



