动态规划:最少硬币找零问题详解

文章摘要

本文介绍了使用动态规划解决硬币找零问题的方法。给定不同面额的硬币和总金额,目标是找到使用最少硬币的组合。通过建立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]

内循环:

  1. coin=1:

    • i-coin = 3,dp[3] 若已算好(已知3元时最少硬币数,比如dp[3]=2)
    • 如果用1元,则需要:dp[3]+1=3
    • dp[4]=min(当前dp[4](初值为无穷),3)=3
  2. coin=2:

    • i-coin=2,dp[2]=1
    • 用一枚2元,则dp[2]+1=2
    • dp[4]=min(3,2)=2
  3. 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为例)

i01234567
dp[i]01122122

例如填 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的最优解

9. 注意

  • 这种方法总能找出最少枚数,因为动态规划表格是依次递推,先算小金额后算大金额。
  • 若题目问“不关心最少,而是种类数/所有组合数”,那做法不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值