动态规划找零钱问题

给定一些人民币的面额,数量不限,要求找出金额为m元且人民币张数最少的方案。这个问题既可以是一个贪心问题也可以是一个动态规划的问题。

对于现行的人民币面额:1、2、5、10、20、50、100,我们找任何金额的零钱都可以使用贪心法求解,比如找72元 = 50 + 20 + 2,3张人民币即可实现。

但如果面额发生变化的话,则用贪心算法无法求出最优解,例如面额为1、3、6、7, 要找12元的话只能是 12 = 7 + 3 + 1 + 1必须使用4张人民币,而最优解为12 = 6 + 6,两张人民币即可。因此这种问题一般使用动态规划求解。

说句题外话,其实当前的人民币方案并不是最优的方案,什么叫最优呢?就是找相同的钱用的人民币张数最少,这样可以节约纸张,这里有一篇文章分析了可能的人民币面额设计方案:https://www.guokr.com/article/342796/,

文中写到:事实上,银行在考虑发行哪些数额的纸币的时候主要考虑的就是两个实际因素。第一个是货币面额要考虑人们日常的十进制算术习惯,如果又是最优组合,又是二进制、三进制,数学不好的人士必将苦不堪言,街边买菜的大妈恐怕买次东西算钱也要算上几分钟, 5 元、 10 元、 20 元的面额在数学上未必是最佳的,但是起码算数的时候最方便。

侧面说明一点,贪心法大家都会,动态规划就不一定了!

1、动态规划解题思路

设给定的面额为 x 1 , x 2 , x 3 . . . x m x_1,x_2,x_3...x_m x1,x2,x3...xm,现要求找 n n n元零钱,假设已经找好,也就是你已经拿到一叠零钱,它的张数最少且总和恰好为 n n n

假设 f ( n ) f(n) f(n)表示找 n n n元钱的最少人民币张数,我们现在关注的是找的这些零钱的最后一张,它只能是 x 1 , x 2 , x 3 . . . x m x_1,x_2,x_3...x_m x1,x2,x3...xm中的任意一张,假设最后一张为 x i , ( 1 ≤ i ≤ m ) x_i, (1\leq i \leq m) xi,(1im), 那么要使总的人民币张数最少,必然要使子问题 f ( n − x i ) f(n-x_i) f(nxi)最少。因此,该问题的状态转移方程为:
f ( n ) = m i n ( f ( n − x i ) + 1 ) , n ≥ x i f(n) = min(f(n-x_i)+1), n \geq x_i f(n)=min(f(nxi)+1)nxi

暴力递归DP代码如下:

int m[5] = {0, 1, 3, 4, 7 };
int dp(int n)
{
	if (n == 1 || n == 3 || n == 4 || n == 7)  return  1; //递归出口
	int ans = INT_MAX;  //因为求最小值,所以这里初始为最大值
	for (int i = 1; i <= 4; i++)
		if (m[i] <= n) ans = min(ans, dp(n - m[i]) + 1);
	return ans;
}

记忆化搜索DP代码如下:

int dp(int n)
{
	if (n == 1 || n == 3 || n == 4 || n == 7)  return ans[n] = 1;
	if (ans[n]) return ans[n];
	ans[n] = INT_MAX;
	for (int i = 1; i <= 4; i++)
		if (m[i] <= n) ans[n] = min(ans[n], dp(n - m[i]) + 1);
	return ans[n];
}
2、跟背包问题的关联

上述两段代码均是自顶向下的递归,那么自底向上的代码如何写呢?经过思考,我觉得这个问题的本质上就是完全背包装满的问题,这里的每一种面额都可以取多个,而且最终要求凑出总额为n且张数最少。因此可以直接上完全背包的模板

int dp(int n)
{
	for (int i = 1; i <= n; i++)
		ans[i] = INT_MAX;
	for (int i = 1; i <= 4; i++)
		for (int j = m[i]; j <= n; j++)
			ans[j] = min(ans[j], ans[j - m[i]] + 1);
	return ans[n];
}

其实借找零钱问题可以很好的解释完全背包问题中以下两个问题:
1)为什么使用滚动数组时01背包需要逆序更新而完全背包却要顺序更新(代码中 a n s [ j ] ans[j] ans[j]所在的循环)?

  • 01背包很好理解,当前的状态只跟上一行的状态有关,从前往后更新的话,更新后面的值时它所依赖的上一行的值已经污染了,因此为了保护现场,从后往前更新。

  • 完全背包这里跟找零钱结合,最后一张找了 m [ i ] m[i] m[i],那么子问题 a n s [ j − m [ i ] ] ans[j - m[i]] ans[jm[i]],凑成 j − m [ i ] j - m[i] jm[i]的面额应该是前 i i i(当前行)种面额中的若干张组成,而不是前 i − 1 i-1 i1(上一行)种面额中的若干张组成。

    举个例子:1、3、5、7四种面额找14元,假设最后一张是7元,那么我们现在要找凑成14-7 = 7的最少张数,我们的求解域应该是前4种面额凑7元的最优解:7 = 7,而不是前三种面额找最优解。这里前三种面额凑7元 = 1 + 1 + 5,根本不是最优解。

2)求装满的最小值时,一般需要将 a n s ans ans数组初始化为正无穷,而 a n s [ 0 ] = 0 ans[0] = 0 ans[0]=0

  • a n s [ 0 ] = 0 ans[0] = 0 ans[0]=0表示用0元钱找0元钱用了0张人民币。其他值初始化为正无穷表示0元钱无法凑出金额为 i , 1 ≤ i ≤ n i, 1 \leq i \leq n i,1in的金额,找零钱失败。
  • 后续的求解过程中,均会依赖之前的状态(子问题),如果子问题均无解,那么当前问题也无解。
  • 11
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Researcher-Du

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

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

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

打赏作者

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

抵扣说明:

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

余额充值