整币兑零问题动态规划,枚举方法解决

整币兑零

问题描述

把n元整币兑换成1元、5元、10元、20元、50元、100元(共6种零币)的兑换种数?

解题方法

枚举法

基本枚举设计
解题思路
  1. 该题目显然需要解一次不定方程:
    • i + 5j + 10k + 20p + 50q + 100r = n.
  2. 对这6个变量实施枚举,确定枚举范围:
    • 0 <= i <= n.
    • 0 <= j <= n/5.
    • 0 <= k <= n/10.
    • 0 <= p <= n/20.
    • 0 <= q <= n/50.
    • 0 <= r <= n/100.
  3. 在以上枚举的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;
}

动态规划

二维数组
解题思路

以一种条件去统计兑换种数,防止重复。
以兑换成零币的最大面值来分类(包含最大面值)。

  1. 定义二维数组f,f[i][j]表示i元整币,兑换成面值最大为下标为j 的零币的兑换种数。(一定注意是下标为j)
    • 比如f[37][2] = 12表示37元整币兑换成5元为最大面值的零币的兑换种数为12。(此题中下标为2的是5元)。如果无法兑换则为0,比如:f [2][1] = 0,两元整币无法兑换成最大面额为五元的零币。
  2. 自顶向下分析,当整币面值为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的下标,即零币在数组中的位置,为了叙述方便,没有修改。
  3. 最终兑换方案数为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;
}
一维数组
解题思路
  1. 定义一维数组f,f[i]表示i元整币可以兑换成零币的兑换种数。
  2. 增加零币的种数,进行遍历。(最重要)
    • 增加零币的种数也就是再外层循环遍历零币。这样可以避免重复的情况。

举例说明:
把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];
	}
}

运行结果

在这里插入图片描述

总结

通过运行结果可以看出,枚举法的时间复杂度要远远高于动态规划,但是它可以输出兑换方案中各种零币的数量。总之,各有利弊吧!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值