原写了 Java 版本的如何求解换钱的方法数,近期进行了一些细节上的补充,以及部分错误更正,将语言换为了 C++ 语言。

基础题目

假设你现在拥有不限量的 1 元、5 元、10 元面值纸币,路人甲希望找你换一些零钱,路人甲拿出的是一张 100 元面值的纸币,试求总共有多少种换零钱的方法?

分析:因为总共只有 3 种面值小额纸币,所以将所有可能进行枚举,直接暴力解决即可。

#include<bits/stdc++.h>
using namespace std;int slove() {int ans = 0;// 10 元张数for(int i = 0; i <= 10; i++) {// 5 元张数for(int j = 0; j <= 20; j++) {// 1 元张数for(int k = 0; k <= 100; k++) {int cur = i*10 + j*5 + k*1;if(cur == 100) {ans++;}}}}return ans;
}int main()
{cout<<slove();
}

递归求解

基础题目中是拥有固定种类的小额纸币,即使再多几种小额纸币也没关系,大不了在嵌套几个循环就能解决。现在需要将题目的难度加大一点,改为小额纸币的种类和需要换零钱的总额由用户输入,即小额纸币种类和总额都不在固定,那么如何解决?

输入共有三行:

  • 第一行:小额纸币种类数量
  • 第二行:不同小额纸币的面值
  • 第三行:需要换零钱的总额

分析:虽然现在种类和总额都是变量了,但是上文中的基础版本还是被包含在此问题中,所以我们还是以上文中的 1 元、5 元、10 元换 100 元进行分析,找一找除了枚举是否还有其他方法解决。

我们先固定一种零钱的数量,剩下的钱用剩余零钱去兑换,即:

  • 用 0 张 1 元换,剩下的用 5、10 元换,最终方法数为 count0;
  • 用 1 张 1 元换,剩下的用 5、10 元换,最终方法数为 count1;
  • 用 100 张 1 元换,剩下的用 5、10 元换,最终方法数为 count100;

那么最终换钱的方法综述即为count0 + count1 + count2 + ... + count100

上面的分析中,我们把原来的大问题拆为了 101 个小问题,且每一个小问题都有很相似的地方,即:

  • 求用 5、10 元换 100 元的方法数
  • 求用 5、10 元换 95 元的方法数
  • 求用 5、10 元换 0 元的方法数

如果我们对这 101 个小问题再进行同样思路的分析,即再固定 5 元零钱的数量,那么就能把问题划分成了规模更小,但问题类型一样的小小问题。即递归的思路,可以写出如下代码。

#include<bits/stdc++.h>
using namespace std;// money 表示所有小额纸币的面值
// len 表示 money 数组的长度,即:小额纸币种类
// index 表示上文分析中的当前固定第几张
// target 表示现在要兑换的钱的总额
int slove(int money[], int len, int index, int target) {int ans = 0;if(index == len) {ans = target == 0 ? 1 : 0;} else {for(int i = 0; i*money[index] <= target; i++) {// 剩余待换零钱的总额int cur_total = target-(i * money[index]);ans = ans + slove(money, len, index+1, cur_total);}}return ans;
}int main()
{int m, target;int money[1000]; // 零钱具体面值cin>>m; // 零钱种类for(int i = 0; i < m; i++){cin>>money[i];}cin>>target; // 兑换总额cout<<slove(money, m, 0, target);
}

优化递归

可以发现上文所写的递归代码存在大量的重复过程,比如下面两种情况,后面所求的子问题是完全一样的,导致程序运行时间的浪费。

  • 已经使用了 5 张 1 元、0 张 5 元,剩下的 95 元用 5 元和 10 元兑换
  • 已经使用了 0 张 1 元、1 张 5 元,剩下的 95 元用 5 元 和 10 元兑换

既然前面已经求解过相同的子问题了,那么我们是否可以在第一次求解的时候,将计算结果保存下来,这样下次遇到相同子问题的实际,直接查出来用就可以,省去再次求解的时间。

#include<bits/stdc++.h>
using namespace std;// 用于存储子问题的解
int val_map[1000][1000] = { 0 };// 0 表示该子问题没有算过
// -1 表示算过,但该子问题无解
// 其它值,即此子问题的方法数int slove(int money[], int len, int index, int target) {int ans = 0;if(index == len) {ans = target == 0 ? 1 : 0;} else {for(int i = 0; i*money[index] <= target; i++) {// 剩余待换零钱的总额int cur_total = target-(i * money[index]);int pre_val = val_map[index+1][cur_total];// 如果 val 为 0,说明该子问题没有被计算过if(pre_val == 0) {ans = ans + slove(money, len, index+1, cur_total);} else {ans += pre_val == -1 ? 0 : pre_val;}}}// 存储计算结果val_map[index][target] = ans == 0 ? -1 : ans;return ans;
}int main()
{int m, target; // 零钱种类int money[1000]; // 零钱具体面值cin>>m;for(int i = 0; i < m; i++){cin>>money[i];}cin>>target;cout<<slove(money, m, 0, target);
}

动态规划

上面对递归的优化方案已经能看出来动态规划的影子了,沿着前文先计算再查表的思路继续思考,我们能否提前把所有子问题都计算出答案,对每个子问题都进行查表解决。也即将最初的递归方案改为循环的实现。

所有的递归都能改为循环实现

#include<bits/stdc++.h>
using namespace std;// 用于存储子问题的解
// val_map[i][j] 表示用 money[0...i] 的小面额零钱组成 j 元的方法数
int val_map[1000][1000] = { 0 };int slove(int money[], int len, int target) {// 第一列表示组成 0 元的方法数,所以为 1for (int i = 0; i < len; i++) {val_map[i][0] = 1;}// 第一行表示只使用 money[0] 一种钱币兑换钱数为i的方法数// 所以是 money[0] 的倍数的位置为 1,否则为 0for (int i = 1; money[0]*i <= target; i++) {val_map[0][money[0]*i] = 1;}for (int i = 1; i < len; i++) {for (int j = 1; j <= target; j++) {for (int k = 0; j >= money[i]*k; k++) {/* val_map[i][j] 的值为:用 money[0...i-1] 的零钱组成 j 减去 money[i] 的倍数的方法数因为相比 val_map[i-1][j],只是多了一种零钱的可选项*/val_map[i][j] += val_map[i-1][j-money[i]*k];}}}return val_map[len-1][target];
}int main()
{int m, target; // 零钱种类int money[1000]; // 零钱具体面值cin>>m;for(int i = 0; i < m; i++){cin>>money[i];}cin>>target;cout<<slove(money, m, target);
}

动归优化

在上文第一版动态规划代码的优化中已经能发现,其实val_map[i][j]的值由两部分组成,分别为:

  • 用 money[0…i-1] 的零钱组成换 j 元的方法数
  • 用 money[0…i-1] 的零钱换 j-money[i]*k(k=1,1,2,3…)元的方法数之和

对于第二种情况来说,其累加值实际上就是val_map[i][j-money[i]],即用money[0...i]的零钱换 j-money[i]元的方法数。至于具体为什么累加值与val_map[i][j-money[i]]相等,我们可以借助递归方法时的分析方式进行理解。

用 money[0…i-1] 的零钱组成换 j 元的方法数对应

  • 用 0 张 money[i] 换,剩下的用 money[0…i-1] 换

用 money[0…i-1] 的零钱换 j-money[i]*k(k=1,1,2,3…)元的方法数之和对应

  • 用 1 张 money[i] 换,剩下的用 money[0…i-1] 换
  • 用 2 张 money[i] 换,剩下的用 money[0…i-1] 换

所以第二部分的值即为val_map[i][j-money[i]]。依据此处的分析,我们可以在原有基础上去掉第三层循环,减少程序运行所花费的时间。

#include<bits/stdc++.h>
using namespace std;int val_map[1000][1000] = { 0 };int slove(int money[], int len, int target) {for (int i = 0; i < len; i++) {val_map[i][0] = 1;}for (int i = 1; money[0]*i <= target; i++) {val_map[0][money[0]*i] = 1;}for (int i = 1; i < len; i++) {for (int j = 1; j <= target; j++) {val_map[i][j] = val_map[i-1][j];// 此处需要比较 j 的大小,防止数组越界// 注意条件时 >= ,否则少计算 j 刚好为 money[i] 的情况if(j >= money[i]) {val_map[i][j] += val_map[i][j-money[i]];}}}return val_map[len-1][target];
}int main()
{int m, target; // 零钱种类int money[1000]; // 零钱具体面值cin>>m;for(int i = 0; i < m; i++){cin>>money[i];}cin>>target;cout<<slove(money, m, target);
}

空间压缩

仔细观察能发现,每一次更新val_map[i][j]的值时,它只依赖于上一行和当前这一行前面的元素。对于我们所求解的问题来说,它仅要求我们给出最终的答案即可,那么前面存储中间结果的那些元素实际上就会空间的浪费,因此我们可以思考一下如何在空间上进行压缩。

实际上我们只需要定义一个一维的数组,采用一些技巧对该数组进行滚动更新,按照合适的方向去更新数组,同样可以达到上面使用二维数组的效果。

#include<bits/stdc++.h>
using namespace std;int val_map[1000] = { 0 };int slove(int money[], int len, int target) {// 第一行,只用 money[0] 换零钱// 所以只能换 money[0] 倍数的钱for (int i = 0; money[0]*i <= target; i++) {val_map[money[0] * i] = 1;}for (int i = 1; i < len; i++) {for (int j = 1; j <= target; j++) {if(j >= money[i]) {// 在进行下面一步前 val_map[j] 的值就已经是 val_map[i-1][j] 了val_map[j] += val_map[j-money[i]];}}}return val_map[target];
}int main()
{int m, target; // 零钱种类int money[1000]; // 零钱具体面值cin>>m;for(int i = 0; i < m; i++){cin>>money[i];}cin>>target;cout<<slove(money, m, target);
}

动态规划实例——换零钱的方法数(C++详解版)相关推荐

  1. 动态规划C++实现--换钱的方法数(二)(动态规划及其改进方法)

    题目:换钱的方法数 给定数组 arr, arr中所有的值都为正数且不重复.每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法. 将原文的伪代 ...

  2. python中怎么计数_浅谈python中统计计数的几种方法和Counter详解

    1) 使用字典dict() 循环遍历出一个可迭代对象中的元素,如果字典没有该元素,那么就让该元素作为字典的键,并将该键赋值为1,如果存在就将该元素对应的值加1. lists = ['a','a','b ...

  3. 2010 27寸 imac 升级固态_2017 款 iMac,27 寸升级换 SSD 固态硬盘拆机详解

    想要 iMac玩游戏?怎么能带动?如何解决卡顿问题?别急,给您带来2017 款 iMac,27 寸升级换 SSD 固态硬盘拆机详解,拆机并不复杂,动手能力差的同学看了这篇文章会觉得原来我也可以,那让我 ...

  4. 2048游戏英雄榜java_2048技巧 2048游戏排行榜挑战方法攻略详解

    2048技巧 2048游戏排行榜挑战方法攻略详解 目前很多的小伙伴们都比较关注2048游戏中的排行榜,想啊哟知道自己的分数有多少排名. 下面就来和大家说下排行榜挑战方法攻略技巧详解. 2048排行榜挑 ...

  5. Oracle11g安装教程、配置实例、监听、客户端程序详解_Windows篇

    Oracle11g安装教程.配置实例.监听.客户端程序详解_Windows篇 文章目录 Oracle11g安装教程.配置实例.监听.客户端程序详解_Windows篇 前言 一.数据库的安装前准备,前提 ...

  6. python换零钱有多少种方案_Python3算法实例 1.2:动态规划 之 换零钱

    money.jpg 问题(基础版): 把100元兑换成1元,2元,5元,10元,20元,50元的零钱,共有多少种不同换法. 动态规划思想解析: 拆解子问题 下面以5元换成1,2,3元的零钱为例.T[( ...

  7. 0x55. 动态规划 - 环形与后效性处理(例题详解 × 6)

    目录 0x55.1 环形结构上的动态规划问题 两次DP法 Problem A. Naptime 破环成链法 Problem B. 环路运输 Problem C. Two Rabbits 0x55.2 ...

  8. java的继承实例_java教程之java继承示例详解

    这篇文章主要介绍了java继承示例详解,需要的朋友可以参考下 什么是继承(extends)? 继承是:新定义的类是从已有的类中获取属性和方法的现象. 这个已有的类叫做父类, 从这个父类获取属性和方法的 ...

  9. linux cron实例,cron,linux定时实施工具详解及实例

    cron,linux定时执行工具详解及实例 cron是一个linux下的定时执行工具,可以在无需人工干预的情况下运行作业.由于Cron 是Linux的内置服务,但它不自动起来,可以用以下的方法启动.关 ...

  10. 【学习笔记】Eureka服务治理代码实例、相关配置和原理机制详解

    文章目录 代码示例 启动一个服务注册中心 注册服务提供者 高可用注册中心 服务的发现与消费 Eureka的一些配置 服务注册类配置 服务实例类配置 实例名配置 端点配置 Eureka服务治理基础架构原 ...

最新文章

  1. docker 容器基本的操作
  2. 夏天写代码真难!16G 内存根本不够用! | 每日趣闻
  3. W32.Downedup.B顽固病毒——查杀记
  4. [2019.1.14]BZOJ2005 [Noi2010]能量采集
  5. 神经网络python实例分类_Python使用神经网络进行简单文本分类
  6. linux急救模式_抢救Linux! Windows XP支持今天终止
  7. 达芬奇调色软件被曝两个远程代码执行缺陷
  8. linux media v4l2,Linux kernel drivers/media/v4l2-core/videobuf2-v4l2.c拒绝服务漏洞(CVE-2016-4568)...
  9. javaWeb企业分布式、互联网、云开发平台-Jeesz
  10. django基础入门(3)django中模板
  11. git 取消merge_git 入门教程之备忘录[译]
  12. 使用通达信获取股票历史数据
  13. 提升PPT逼格的利器!只需1招,让PPT页面化腐朽为神奇~
  14. python爬虫文献_Python文献爬虫①
  15. Flink DataStream的多流、键控流、窗口、连接、物理分区转换算子的使用
  16. latex特殊字体咋打?+下标打在左边
  17. APP推广运营小技巧:可复制的APP推广渠道
  18. 在控制台下刻录CD(转)
  19. 运营日记:App推广手段详解
  20. One Step By One Step 解析OkHttp3 - Dispatcher (一)

热门文章

  1. 计算机中继承的英语作文,传统文化的继承To Inherit the Tradition(高中英语作文)
  2. Bootstrap 教程
  3. 架构师之路:数据中台实施模式与落地
  4. 大数据下的世界杯:社交为王
  5. 程序人生(一):面试腾讯,过了.
  6. 深度学习|批处理图像保存到四维数组中
  7. pase.py 解析文字中包含的成语
  8. 国庆出游必看!全国5A景区最详细数据分析来啦
  9. msprofiler 性能调优命令行实战(口罩识别推理)
  10. 世界10大编程语言,Java不是第一,PHP才第五?!