整币兑零
问题描述
把n元整币兑换成1元、5元、10元、20元、50元、100元(共6种零币)的兑换种数?
解题方法
枚举法
基本枚举设计
解题思路
- 该题目显然需要解一次不定方程:
- i + 5j + 10k + 20p + 50q + 100r = n.
- 对这6个变量实施枚举,确定枚举范围:
- 0 <= i <= n.
- 0 <= j <= n/5.
- 0 <= k <= n/10.
- 0 <= p <= n/20.
- 0 <= q <= n/50.
- 0 <= r <= n/100.
- 在以上枚举的6重循环中,满足上述不定方程,则为一种兑换方法。
代码
public static int mei1(int n) {
int m = 0;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n / 5; j++) {
for (int k = 0; k <= n / 10; k++) {
for (int l = 0; l <= n / 20; l++) {
for (int o = 0; o <= n / 50; o++) {
for (int p = 0; p <= n / 100; p++) {
if (i + j * 5 + k * 10 + l * 20 + o * 50 + p * 100 == n) {
m++;
}
}
}
}
}
}
}
return m;
}
精简枚举设计
解题思路
在上述设计的6层循环中,可精简 i 循环,在循环内为:
i = n - (5j + 10k + 20p + 50q + 100r).
如果i>=0,满足不定方程,为一种兑换方法。
代码
public static int mei2(int n) {
int m = 0;
for (int j = 0; j <= n / 5; j++) {
for (int k = 0; k <= n / 10; k++) {
for (int l = 0; l <= n / 20; l++) {
for (int o = 0; o <= n / 50; o++) {
for (int p = 0; p <= n / 100; p++) {
int i = n - (j * 5 + k * 10 + l * 20 + o * 50 + p * 100);
if (i >= 0) {
m++;
}
}
}
}
}
}
return m;
}
优化枚举设计
解题思路
以上程序循环次数已经大大精简了。进一步分析,可以看到在程序的循环设置中,k循环可以从0 ~ n/10改进为0 ~ (n - 5 * j),因为在n中j已经占去了5 * j。依次类推,对剩下的变量作类似的循环参数优化。
代码
public static int mei3(int n) {
int m = 0;
for (int j = 0; j <= n / 5; j++) {
for (int k = 0; k <= (n - j * 5) / 10; k++) {
for (int l = 0; l <= (n - (j * 5 + k * 10)) / 20; l++) {
for (int o = 0; o <= (n - (j * 5 + k * 10 + l * 20)) / 50; o++) {
for (int p = 0; p <= (n - (j * 5 + k * 10 + l * 20 + o * 50)) / 100; p++) {
int i = n - (j * 5 + k * 10 + l * 20 + o * 50 + p * 100);
if (i >= 0) {
m++;
}
}
}
}
}
}
return m;
}
动态规划
二维数组
解题思路
以一种条件去统计兑换种数,防止重复。
以兑换成零币的最大面值来分类(包含最大面值)。
- 定义二维数组f,f[i][j]表示i元整币,兑换成面值最大为下标为j 的零币的兑换种数。(一定注意是下标为j)
- 比如f[37][2] = 12表示37元整币兑换成5元为最大面值的零币的兑换种数为12。(此题中下标为2的是5元)。如果无法兑换则为0,比如:f [2][1] = 0,两元整币无法兑换成最大面额为五元的零币。
- 自顶向下分析,当整币面值为n时:
- n减去其中一种零币i 得到结果小于0,说明无法兑换。即,f [n][i] = 0.
- n减去其中一种零币i 得到结果等于0,说明整币n和零币i 的面值相同,只能兑换一张。即,f [n][i] = 1.
- n减去其中一种零币i 得到结果大于0,说明整币n兑换成以零币i为最大面值的方案数 可以由n-i的所有兑换成最大面值小于等于i的兑换方案 再加上一张面值为i的零币完成。即,f[n][i]等于整币n-i的兑换成的最大面值小于等于i的兑换方案数之和。
注:f[n][i]中的i的位置应该是零币i的下标,即零币在数组中的位置,为了叙述方便,没有修改。
- 最终兑换方案数为f[n]那一行的求和。
举例说明,整币为10:
兑换成最大面额为1的零币时(1在数组中的下标为0),10-1=9,9>0,
所以f [10][0] = f [9][0] = 1.
兑换成最大面额为5的零币时(5在数组中的下标为1),10-5=5,5>0,
所以f [10][1] = f [5][1] + f [5][0] = 2.
兑换成最大面额为10的零币时(10在数组中的下标为2),10-10=0,说明能兑换一张10元零币,
所以f [10][2] = 1.
兑换成最大面额为20的零币时(20在数组中的下标为3),10 - 20 < 0,无法兑换,
所以f [10][3] = 0.兑换成最大面额为50的零币时(50在数组中的下标为4),10 - 50 < 0,无法兑换,
所以f [10][4] = 0.
兑换成最大面额为100的零币时(100在数组中的下标为5),10 - 100 < 0,无法兑换,
所以f [10][5] = 0.
所以最终兑换方案数
sum = f [10][0] + f [10][1] + f [10][2] + f [10][3] + f [10][4] + f [10][5] = 1 + 2 + 1 + 0 + 0 + 0 = 4.
代码
public static int di1(int n) {
int[] money = {1, 5, 10, 20, 50, 100};
int[][] f = new int[n + 1][money.length];
int sum = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < money.length; j++) {
int res = i - money[j];
if (res < 0) {
f[i][j] = 0;//无法兑换
} else if (res == 0) {
f[i][j] = 1;//存在一种零币等于j,自身也是一种兑换方式
} else {
int temp = 0;
for (int k = 0; k <= j; k++) {
temp += f[res][k];
}
f[i][j] = temp;
}
}
}
for (int i = 0; i < money.length; i++) {
sum += f[n][i];
}
return sum;
}
一维数组
解题思路
- 定义一维数组f,f[i]表示i元整币可以兑换成零币的兑换种数。
- 增加零币的种数,进行遍历。(最重要)
- 增加零币的种数也就是再外层循环遍历零币。这样可以避免重复的情况。
举例说明:
把6元整币兑换成零币,有两种兑换方案,一种是6个一元,另一种时一个一元和一个五元。增加零币的种数,是先把一元的遍历完,此时f [1] = 1(1元整币兑换成1个1元零币),f [5] = 1(5元整币兑换成5个1元零币),f [6] =f [6] + f [6 - 1] = 0 + f [5] = 1(6元整币兑换方案在5元整币所有兑换方案的后面加1元零币,即由6个1元零币组成)。之后再遍历五元零币,f[5] = 2(有5个1元组成,或者1个5元组成)。f [6] = f [6] + f [6 - 5] = 1 + f [1] = 2(6元的兑换方案数等于原来6元的兑换方案数 + 在1元所有兑换方案后加上5元零币,即1个一元和一个5元)。
上述情况中六元 的兑换方案数为2,即
6 = 1 + 1 + 1 + 1 + 1 + 1.
6 = 1 + 5.
不会出现再去在五元的所有兑换方案方案后面加一元,也就是不会出现6 = 5 + 1的情况,避免了和6 = 1 + 5重复。
3. 自顶向下分析,当整币面值为n时:
- 兑换种数等于n减去其中一种零币i 得到结果大于0的兑换种数和。
- 递推关系式: f[n] = f[n] + f [n - i].
- 如果整币n的面值等于零币i 的面值,则n本身也是一种兑换方案。
- 递推关系式:f[n] = f[n] + 1.
代码
public static int di2(int n) {
int[] money = {1, 5, 10, 20, 50, 100};
int[] f = new int[n + 1];
for (int i = 0; i < money.length; i++) {
if (money[i] <= n) {
f[money[i]] += 1;
}
for (int j = money[i] + 1; j <= n; j++) {
f[j] += f[j - money[i]];
}
}
return f[n];
}
代码还可以这样改进:
public static int di2(int n) {
int[] money = {1, 5, 10, 20, 50, 100};
int[] f = new int[n + 1];
f[0] = 1;//将f[0]赋值为1
for (int i = 0; i < money.length; i++) {
for (int j = money[i]; j <= n; j++) {
f[j] += f[j - money[i]];
}
}
return f[n];
}
将f[0]赋值为1,但是笔者想不到f[0]为1的意义。(如有想法,可联系笔者)
只是为了使j==money[i]时,f[j] = f[j] + 1.也满足f[j] += f[j - money[i]].规律。
完整代码
public class Demo2 {
public static void main(String[] args) {
int n;
long begin, end;
Scanner sc = new Scanner(System.in);
System.out.println("input n:");
n = sc.nextInt();
begin = System.currentTimeMillis();
System.out.println("枚举方法1:" + mei1(n));
end = System.currentTimeMillis();
System.out.println("枚举方法1用时:" + (end - begin) + "\n");
begin = System.currentTimeMillis();
System.out.println("枚举方法2:" + mei2(n));
end = System.currentTimeMillis();
System.out.println("枚举方法2用时:" + (end - begin) + "\n");
begin = System.currentTimeMillis();
System.out.println("枚举方法3:" + mei3(n));
end = System.currentTimeMillis();
System.out.println("枚举方法3用时:" + (end - begin) + "\n");
begin = System.currentTimeMillis();
System.out.println("递推1:" + di1(n));
end = System.currentTimeMillis();
System.out.println("递推1用时:" + (end - begin) + "\n");
begin = System.currentTimeMillis();
System.out.println("递推2:" + di2(n));
end = System.currentTimeMillis();
System.out.println("递推2用时:" + (end - begin) + "\n");
}
public static int mei1(int n) {
int m = 0;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n / 5; j++) {
for (int k = 0; k <= n / 10; k++) {
for (int l = 0; l <= n / 20; l++) {
for (int o = 0; o <= n / 50; o++) {
for (int p = 0; p <= n / 100; p++) {
if (i + j * 5 + k * 10 + l * 20 + o * 50 + p * 100 == n) {
m++;
}
}
}
}
}
}
}
return m;
}
public static int mei2(int n) {
int m = 0;
for (int j = 0; j <= n / 5; j++) {
for (int k = 0; k <= n / 10; k++) {
for (int l = 0; l <= n / 20; l++) {
for (int o = 0; o <= n / 50; o++) {
for (int p = 0; p <= n / 100; p++) {
int i = n - (j * 5 + k * 10 + l * 20 + o * 50 + p * 100);
if (i >= 0) {
m++;
}
}
}
}
}
}
return m;
}
public static int mei3(int n) {
int m = 0;
for (int j = 0; j <= n / 5; j++) {
for (int k = 0; k <= (n - j * 5) / 10; k++) {
for (int l = 0; l <= (n - (j * 5 + k * 10)) / 20; l++) {
for (int o = 0; o <= (n - (j * 5 + k * 10 + l * 20)) / 50; o++) {
for (int p = 0; p <= (n - (j * 5 + k * 10 + l * 20 + o * 50)) / 100; p++) {
int i = n - (j * 5 + k * 10 + l * 20 + o * 50 + p * 100);
if (i >= 0) {
m++;
}
}
}
}
}
}
return m;
}
public static int di1(int n) {
int[] money = {1, 5, 10, 20, 50, 100};
int[][] f = new int[n + 1][money.length];
int sum = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < money.length; j++) {
int res = i - money[j];
if (res < 0) {
f[i][j] = 0;
} else if (res == 0) {
f[i][j] = 1;
} else {
int temp = 0;
for (int k = 0; k <= j; k++) {
temp += f[res][k];
}
f[i][j] = temp;
}
}
}
for (int i = 0; i < money.length; i++) {
sum += f[n][i];
}
return sum;
}
public static int di2(int n) {
int[] money = {1, 5, 10, 20, 50, 100};
int[] f = new int[n + 1];
for (int i = 0; i < money.length; i++) {
if (money[i] <= n) {
f[money[i]] += 1;
}
for (int j = money[i] + 1; j <= n; j++) {
f[j] += f[j - money[i]];
}
}
return f[n];
}
}
运行结果
总结
通过运行结果可以看出,枚举法的时间复杂度要远远高于动态规划,但是它可以输出兑换方案中各种零币的数量。总之,各有利弊吧!!!