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

这篇具有很好参考价值的文章主要介绍了动态规划实例——换零钱的方法数(C++详解版)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

原写了 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 元的方法数,所以为 1
    for (int i = 0; i < len; i++) {
        val_map[i][0] = 1;
    }

    // 第一行表示只使用 money[0] 一种钱币兑换钱数为i的方法数
    // 所以是 money[0] 的倍数的位置为 1,否则为 0
    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++) {
            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]的值时,它只依赖于上一行和当前这一行前面的元素。对于我们所求解的问题来说,它仅要求我们给出最终的答案即可,那么前面存储中间结果的那些元素实际上就会空间的浪费,因此我们可以思考一下如何在空间上进行压缩。

实际上我们只需要定义一个一维的数组,采用一些技巧对该数组进行滚动更新,按照合适的方向去更新数组,同样可以达到上面使用二维数组的效果。文章来源地址https://www.toymoban.com/news/detail-476532.html

#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++详解版)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【面试经典150 | 动态规划】零钱兑换

    【动态规划】【数组】 322. 零钱兑换 定义状态 dp[i] 表示凑成总金额的最少硬币个数。 状态转移 从小到大枚举要凑成的金额 i ,如果当前的金额可以使用面额数组中的某个面额 coin 凑成总金额的一部分,则可以更新 d p [ i ] = m i n ( d p [ i ] , d p [ i − c o i n ] + 1 ) dp[i] = min(dp[i

    2024年04月16日
    浏览(71)
  • 零钱兑换 II 力扣(动态规划) JAVA

    给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。 示例 1: 输入:amou

    2024年02月15日
    浏览(38)
  • 零钱兑换 II(力扣)动态规划 JAVA

    给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。 示例 1: 输入:amou

    2024年02月16日
    浏览(43)
  • leetcode 动态规划(爬楼梯、零钱兑换、完全平方数)

    卡码网:57. 爬楼梯(opens new window) 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬至多m (1 = m n)个台阶。你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整数。 输入描述:输入共一行,包含两个正整数,分别表示n, m 输出描述:输出一个整数,表

    2024年01月17日
    浏览(42)
  • leetcode动态规划(零钱兑换II、组合总和 Ⅳ)

    给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 示例 1: 输入: amount = 5, coins = [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1 示例 2: 输入: amount = 3, coins = [2] 输出: 0 解释: 只用面

    2024年02月01日
    浏览(43)
  • [动态规划]凑零钱.钱币的组合有多少种(java)

    添加链接描述@TOC arr是面值数组,其中的值都是正数且没有重复。 再给定一个正数aim。 每个值都认为是一种面值,且认为张数是无限的。 返回组成aim的方法数 例如:arr = {1,2},aim = 4 方法如下:1+1+1+1、1+1+2、2+2 一共就3种方法, 所以返回3 每种钱币都有无数张,在钱币的数组中

    2024年02月08日
    浏览(41)
  • 零钱兑换(Coins Change) -动态规划C语言实现

    1. 前言 零钱兑换是经典的动态规划问题,也是贪心解法不足的反证答案。它要求兑换一定总整数的零钱,满足硬币数量最少的条件。假定我们有3类零钱,构成数组coins[]={1,7,10},现在兑换总额14的金额,如果采用贪心策略,我们有10+1+1+1+1=14, 共需要5枚硬币。实际上本题的最少

    2024年02月09日
    浏览(53)
  • 动态规划part06 518. 零钱兑换 II 377. 组合总和 Ⅳ

     518 零钱兑换|| 377. 组合总和 Ⅳ  

    2024年01月20日
    浏览(50)
  • 【LeetCode动态规划#08】完全背包问题实战与分析(零钱兑换II)

    力扣题目链接(opens new window) 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 示例 1: 输入: amount = 5, coins = [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1 示例 2: 输入: amount = 3

    2023年04月19日
    浏览(47)
  • LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题

    一、题目 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。 示例 1: 示

    2024年02月09日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包