动态规划(三)——最少硬币和所有硬币问题

一、最少硬币问题

有n种硬币,面值为v1…vn,数量无限,选用硬币,使其和金额为s,要求求出最少的硬币组合。

首先我们应该有打表的思想,将任意金额的最少硬币组合数量存到一个数组里,输入一个金额时就可以直接查询数组中对应的硬币最少数量。

定义一个int Min[MONEY],Min[i]是金额i对应的最少硬币数量。Min[i]这样记录子问题最优解的数据称为“状态”

讲解以5种面值(1、5、10、25、50)的硬币为例:

  1. 第一步:只使用1分硬币。
    初始值Min[0] = 0,表示0个硬币可以表示0金额,其他的Min[i]为无穷大。
    那Min[1]如何计算?
    这时可以在Min[0]的基础上加一个1分硬币,此时Min[1] = Min[0] + 1 = Min[1] = Min[1-1] +1 。
    此时可以推导出第一个递推公式:
    Min[i] = min(Min[i],Min[i-1]+1)
    故只用1分硬币的组合结果如下:
    在这里插入图片描述

  2. 第二步:在使用1分硬币的基础上增加第二大面值的5分硬币
    此时要在第一步状态的基础上从金额为5开始转换(若金额小于5时,不能用5分硬币替换)
    i = 5时,相当于在i = 0的基础上加一个5分硬币,得到Min[5] = Min[5-5] +1 = 1。
    1分硬币+5分硬币组合的结果如下:在这里插入图片描述
    第一步i=5时,只用1分硬币状态的结果Min[5] = 5,此时根据推导公式
    Min[i] = min(Min[i],Min[i-5]+1)
    可知,Min[5-5]+1=1比 Min[5]=5更小,故Min[5]=1。

  3. 第三步:继续处理其他面值的硬币。
    接下来就是在第二步的基础上将面值为10的硬币转换。
    例如i= 10时,相当于在i = 0的基础上加了一个10分硬币,此时Min[10] = Min[10-10] + 1 = 1。
    又第二步中Min[10] = 2,
    故Min[10] = min(Min[10] ,Min[10-10]+1) = Min[10-10]+1 = 1。
    在这里插入图片描述

所以找出规律,所有面值的递推公式都是:
Min[i] = min(Min[i],Min[i-面值]+1)

代码如下:

#include<bits/stdc++.h>
#define fio ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
using namespace std;

const int money = 200;	//定义最大金额
const int num = 5;		//五个硬币
int type[num] = {1,5,10,25,50};		//五种面值
int Min[money];

void solve() {
  for (int i = 1; i < money; i++)
    Min[i] = INT_MAX;//初始值为无穷大
  Min[0] = 0;
  for (int i = 0; i < 5; i++) {
    for (int j = type[i]; j <= money; j++) {
      Min[j] = min(Min[j], Min[j-type[i]]+1);
    }
  }
}
int main() {
  fio
  solve();
  int s;
  while (cin >> s) {
    cout << Min[s] << endl;
  }
}

二、打印最少硬币组合

若要打印最少硬币的组合方案,就存一个记录金额 i 所需要的最后一种面值的数组Min_Path[],用这个数组倒推即可得到硬币组合方案。

查询 i 的最优组合:

  1. 查询Min_Path[i],并得到结果k1
  2. i-k1=a1,并得到Min_Path[a1] = k2
  3. a1-k2 = a2,继续计算Min_Path[a2]。
    重复计算坐标与对应Min_Path[]数组的差值,并计算Min_Path[差值]的结果,直到计算结果为0。

得出的k1…kj即为结果

如图,计算出Min[]和Min_Path[]:在这里插入图片描述
实例解释,当i=7时:
①Min_Path[7] = 5,说明金额为7的组合中最后一张需要的面值为5。
②Min_Path[7-5]=Min_Path[2] = 1,表示接下来需要的最后一张面值为1。
③Min_Path[2-1]=Min_Path[1]=1,还有一张面值为1。
故i=7时,组合为5分硬币+1分硬币+1分硬币。

#include<bits/stdc++.h>
#define fio ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
using namespace std;

const int money = 200;	//定义最大金额
const int num = 5;		//五个硬币
int type[num] = {1,5,10,25,50};		//五种面值
int Min[money];
int Min_Path[money];
void solve() {
  for (int i = 0; i < money; i++)
    Min[i] = INT_MAX; 
  Min[0] = 0;
  for (int i = 0; i < num; i++) {
    for (int j = type[i]; j < money; j++) {
      if (Min[j] > (Min[j-type[i]]+1)) {
        Min[j] = Min[j-type[i]]+1;
        Min_Path[j] = type[i];
      }
    }
  }
}
void print(int *Min_Path, int s) {
  while (s) {
    printf("%d ", Min_Path[s]);
    s -= Min_Path[s];
  }
}
int main() {
  fio
  int s;
  solve();
  while (cin >> s) {
    cout << Min[s];
    print(Min_Path, s);
  }
}

三、所有硬币组合

3.1硬币数量不限制

有n种硬币,面值为v1,v2……vn,数量无限。输入非负整数s,选用硬币,使其和为s,输出所有可能的硬币组合数量。

定义一个记录状态的数组int dp[],dp[i]表示金额i所对应的组合方案数。需要找到dp[i]和dp[i-1]的递推关系。

同样是用1,5,10,25,50五种面值的硬币:

  1. 第一步:只用1分硬币。
    dp[0] = 1设为初始值,其余为0。
    dp[1] = dp[1]+dp[0] = 0+1。
    对于其他dp[i]也有dp[i] = dp[i]+dp[i-1],这称为状态转移方程。
  2. 第二步:加上5分硬币。
    i>=5时,dp[i] = dp[i]+dp[i-5]
  3. 第三步:继续处理其他面值硬币的情况。
    同理有dp[i] = dp[i]+dp[i-10],dp[i] = dp[i]+dp[i-25],dp[i] = dp[i]+dp[i-50]
#include<bits/stdc++.h>
#define fio ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
using namespace std;

const int money = 200;	//定义最大金额
const int num = 5;		//五个硬币
int type[num] = {1,5,10,25,50};		//五种面值
int dp[money];

void solve() {
  dp[0] = 1;
  for (int i = 0; i < num; i++) {
    for (int j = type[i]; j < money; j++) {
      dp[j] = dp[j]+dp[j-type[i]];
    }
  }
}
int main() {
  fio
  int s;
  solve();
  while (cin >> s) {
    cout << dp[s] << endl;
  }
}

3.2硬币数量限制

HDOJ2069-硬币找零

问题描述
假设有5种硬币:50美分,25美分,10美分,5美分和1美分。我们希望以给定的金额使用这些硬币进行更改。

例如,如果我们有11美分,则可以用一枚10美分硬币和一枚1美分硬币,或两枚5美分硬币和一枚1美分硬币,或一枚5美分硬币和六枚1美分硬币进行组合或11个1分硬币。因此,使用上述硬币,有四种方法可以组合11分钱。

编写一个程序,以查找以美分计的任何金额进行组合的不同方式的总数,硬币数量num<=100。

输入项
输入文件包含任意数量的行,每行包含一个数字(≤250),以分币为单位。

输出量
对于每条输入线,输出一条线,其中包含使用上述5种硬币进行更改的不同方式的数量。

样本输入

11
26

样本输出

4
13

这道题要求硬币不能多于100个,因此我们可以建立一个转移矩阵,定义状态为dp[i][j]。其中横向是金额(i<=250),纵向是硬币数(j<=100)。
在这里插入图片描述

  1. 第一步:只用一分硬币。
    type[5] = {1,5,10,25,50}
    初始化dp[0][0] = 1,其他为0,从dp[0][0]推导后面的状态。
    例如dp[1][1]是dp[0][0]进行金额+1、硬币数量+1后的状态转移,状态转移后组合方案数量不变,即dp[0][0] = dp[1][1] = 1。
    dp[1][1] = dp[1][1]+dp[0][0]= dp[1][1]+dp[1-1][1-1]
    =>
    dp[1][1] = dp[1][1]+dp[1-type[0]][1-1]
    => dp[i][j] = dp[i][j]+dp[1-type[0] ][j-1]
    对所有dp[i][j]执行如上操作,看图可观察到红色剪头的规律。在这里插入图片描述
  2. 第二步:加上五分硬币。
    当i>=5时,dp[i][j] = dp[i][j]+dp[i-5][j-1]。
    dp[i][j] = dp[i][j]+dp[i-type[1]][j-1]
    在这里插入图片描述
  3. 第三步:处理其他面值的硬币
    dp[i][j] = dp[i][j] + dp[i-type[k]][j-1],k=2,3,4

矩阵元素dp[i][j]的含义是用j个硬币实现金额i的方案数量。
例如表中dp[6][6]=1,表示用6个硬币凑出6分钱,只有一种方案,即6个1分钱,表中空格即0表示没有方案。
该表中纵坐标相加,就是某金额对应的方案总数。

#include<bits/stdc++.h>
#define fio ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
using namespace std;

const int coin = 101;
const int money = 251;	//定义最大金额
const int num = 5;		//五个硬币
int type[num] = {1,5,10,25,50};		//五种面值
int dp[money][coin];

void solve() {
  dp[0][0] = 1;
  for (int i = 0; i < num; i++) {
    for (int j = 1; j < coin; j++) {
      for (int k = type[i]; k < money; k++) {
        dp[k][j] += dp[k-type[i]][j-1];
      }
    }
    
  }
}

int main() {
  fio
  int ans[money] = {0};
  int s;
  solve();
  for (int i = 0; i < money; i++) 
    for (int j = 0; j < coin; j++)
      ans[i] += dp[i][j];
  while (cin >> s) {
    cout << ans[s] << endl;
  }
}

最少硬币问题是指在给定面额的硬币中,找出能够组成指定金额的最少硬币数。可以使用动态规划来解决这个问题。 以下是用Java实现最少硬币问题的示例代码: ```java public class MinimumCoins { public static int minCoins(int[] coins, int amount) { int[] dp = new int[amount + 1]; Arrays.fill(dp, Integer.MAX_VALUE); dp[0] = 0; for (int i = 1; i <= amount; i++) { for (int j = 0; j < coins.length; j++) { if (coins[j] <= i && dp[i - coins[j]] != Integer.MAX_VALUE) { dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1); } } } return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount]; } public static void main(String[] args) { int[] coins = {1, 2, 5}; int amount = 11; int minCoins = minCoins(coins, amount); System.out.println("Minimum coins required to make " + amount + " is " + minCoins); } } ``` 在上面的代码中,我们使用一个数组dp来存储每个金额所需的最少硬币数。我们首先将dp数组初始化为Integer.MAX_VALUE,然后将dp[0]设置为0,因为组成0元需要0枚硬币。 接下来,我们使用两个嵌套循环来遍历每个金额和每个硬币面额。如果当前硬币面额小于等于当前金额,并且使用当前硬币面额可以组成金额i - coins[j],那么我们更新dp[i]为dp[i - coins[j]] + 1的最小值。最终,dp[amount]存储了组成指定金额所需的最少硬币数。如果dp[amount]等于Integer.MAX_VALUE,则表示无法组成指定金额,返回-1。 在上面的示例中,我们使用coins数组存储硬币面额,amount变量存储指定金额。输出结果为“Minimum coins required to make 11 is 3”,表示组成11元需要3枚硬币,即1枚5元硬币和2枚2元硬币
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

你脸上有BUG

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

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

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

打赏作者

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

抵扣说明:

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

余额充值