sicp(计算机程序的构造和解释 )中有这样一个问题。因为那里的问题用的是美金,我这里就换成人民币吧。这个问题是这样的,现在有1元、5元、10元、20元、50元的纸币,给定任意的数量的现金,请计算出所有数零钱的方式。
咋眼一看,这题觉得挺麻烦的。要怎么去想。我是习惯用类似数学归纳法那种思维去想的。
比如
只有 1 元,那当然只有 1 种换零钱的方式,
如有有 4 元,4 个 1,只有 1 种。
如果有 6 元,那就有 (5,1), (1,1,1,1,1,1), 这 2 种方式
如果是 11 元,那就是 (10,1), (5,5,1), (5,1{6}), (1{11}),这 4 种方式。
来推一下,如果给定的钱是 11 元
- 如果纸币的钱大于 11 元,这是不可能的,所以 20、50 元不能用
- 所以剩下可以用 {10,5,1} 这三种纸币
- 那么换零钱的方式分成两类,有 10 元纸币和没有 10 元纸币的,有 10 元纸币的就有 (10,1) 一种换法,而没有 10 元纸币的换法,看下嘛
- 没有 10 元纸币的换法,也就是只有 5 元和 1 元 纸币的换法。也可以再分,有5元纸币的换法和没有5元纸币的换法。
- 有 5 元纸币有两种,一种是有一张 5 元,(5,1*6),另一种是有二张 5 元,(5,5,1)
- 只有1元纸币的换法,(1*11)
所以共 4 种方式。
再抽象一下。换零钱的所有数目分成两类
-
零钱减去一种纸币后,(包括自己在内的)所有换零钱的方法,比如,对于 5 元, 11 - 5 后,还有 (5,5,1) 这样的一种;对于 20 元,而 11 - 20,这样就直接 0 种了
-
零钱除去一种纸币后的所有换零钱方式。
可能要思考一下。
用上面那个例子解释一下。
比如,换零钱的函数是 F,而数组 A 代表能使用的纸币(假如 A 是由大到小排序)。 递归的过程会是这样的。
如果是给定的条件 11 元,那么其实可以用挺数学的方式去理解的。
F(X=11,A=[50,20,10,5,1])
F(X=11,A=[50,20,10,5,1]) = F(11,[10,5,1])
= F(11-10, [10,5,1]) + F(11, [5,1])
= 1 + F(11,[5,1])
= 1 + F(6, [5,1]) + F(11,[1])
= 4;
那么就可以把代码写成这样的(用个end变量就不用在递归程序中直接裁剪数组中的元素了)
int count(int money, vector<int> &array, int end) {
if (money < 0) {
return 0;
} else if (money == 0) {
return 1;
} else if (end == 0) {
return 0;
} else {
return count(money - array[end], array, end) + count(money, array, end - 1);
}
}
int count(int money) {
vector<int> a = {0, 1, 5, 10, 20, 50};
return count(money, a, a.size() - 1);
}
但与前面的文章类似都有重复计算的问题的。所以,可以用自底向上的方式,反向递推。
用个二维数组存储换零钱的次数,横坐标是金钱,纵坐标是纸币。用 v[i][j] 表示。
- 如果 i == 0 或者 j == 0 ,那么当然是0了。
- 如果 i == 1 也就是说只用 1 元纸币,那么所以金额都是 1 种表示的方式。
- 如果 i == 2,也就说用 1 元或者 5 元。那就要分 3 种情况了
- j < 5 ,金额小于 5 ,但起码有 1 元的换零方式,所以 v[i][j] = v[i-1][j]了。
- j >= 5 ,说明换零的方式中有5 元纸币,所以 v[i][j] = v[i-1][j] + v[i][j-5]
- 如果 i== 3, 也是相类似的。
所以,可以写出这样的代码。
int count(int money) {
vector<int> a = {1, 5, 10, 20, 50};
int v[a.size()][money + 1];
for (int i = 0; i < a.size(); i++) {
for (int j = 0; j < money + 1; j++) {
if (i == 0) {
v[i][j] = 1;
} else {
v[i][j] = v[i - 1][j];
if (j >= a[i]) {
v[i][j] += v[i][j - a[i]];
}
}
}
}
return v[a.size() - 1][money];
}
这段代码,还能优化一下,因为当 i > 0, 每次循环都有v[i][j] = v[i - 1][j];
这样的语句,也就是将上一行复制到当前行,这其实没有必要的。也就是说其实可以用一维数组就可以了,无须用二维数组。
所以最后可以变成这样了。
int count(int money) {
vector<int> a = {1, 5, 10, 20, 50};
int *v = new int[money + 1];
for (int i = 0; i < money + 1; i++)
v[i] = 1;
for (int i = 1; i < a.size(); i++) {
for (int j = a[i]; j < money + 1; j++) {
v[j] += v[j - a[i]];
}
}
int res = v[money];
delete[] v;
return res;
}