给定一些人民币的面额,数量不限,要求找出金额为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 元的面额在数学上未必是最佳的,但是起码算数的时候最方便。
侧面说明一点,贪心法大家都会,动态规划就不一定了!
设给定的面额为 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,(1≤i≤m), 那么要使总的人民币张数最少,必然要使子问题 f ( n − x i ) f(n-x_i) f(n−xi)最少。因此,该问题的状态转移方程为:
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(n−xi)+1),n≥xi
暴力递归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];
}
上述两段代码均是自顶向下的递归,那么自底向上的代码如何写呢?经过思考,我觉得这个问题的本质上就是完全背包装满的问题,这里的每一种面额都可以取多个,而且最终要求凑出总额为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[j−m[i]],凑成 j − m [ i ] j - m[i] j−m[i]的面额应该是前 i i i(当前行)种面额中的若干张组成,而不是前 i − 1 i-1 i−1(上一行)种面额中的若干张组成。
举个例子: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 ?