从换钱的最少货币数谈动态规划

>>2020,微服务装逼指南

题目

给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种货币都可以使用任意张,再给定一个整数aim代表要找的钱数,求组成aim的最少货币数。
例如 arr=[5, 2, 3], aim=20, 返回4

看到这个题目以后,如果说不知道动态规划,我们可以怎么作答呢?

混搭的解决过程

这个问题问过一个小朋友,他的回答很有意思,仔细分析一下竟然是多种算法的混搭。

  1. 先把钱币按照大小排序  

  2. 然后用aim取余最大的钱币数  

  3. 余数继续找第二大的钱币取余  

  4. 如果不行,则把最大的的钱币回退一张到余数里,继续上面的操作  
    上述的过程有人说是暴力破解,仔细想想并不是。由于求解的是最小货币数,他优先进行了排序,希望用最大的钱币填充最多,这个是典型的贪心法。取余后发现没有结果就把最大的的钱币回退一张到余数里继续上面的操作,这里是在做回溯。
    上述的方法看起来很好,也是一般找零钱的思路,但是这个思路没有继续往下描述。由于不是每张钱都得参加找零,所以回溯本身的做法是不对的,例如要找的零钱为17,我们的钱排序后为532,按照上面的思路。

  5. 17%5  取余为2 数量为3    aim:2  count:3

  6. 3不满足开始回溯   aim:7  count:2

  7. 7%3  取余为1 数量为2  aim:1  count:4
    …………

  8. 最终结果为  5元2张,3元1张,2元2张 结果为5
    大家都发现不对了,2钱币里有,应该直接取2,结果为4就结束了。 但是程序竟然开始回溯了。然后小朋友开始改良,当前钱币不满足的时候继续往下走,走到最后然后再开始做回溯。这样是可以获取正确答案的,但是和暴力破解是一回事了。

经验主义解法

经验的解法在找钱的时候被广泛使用。随着时间的增长,售货员找钱的经验也丰富了。慢慢有了找5元,7元,9元,10元等等的经验。当出现找12元的时候,就可以根据10元经验,9元经验来做决策。

  1. 现有的经验有,5元的经验是5元是1张,10元是2张 。3元的经验9元是3张。2元的经验是7元是2张。

  2. 现在开始找12元

  3. 5元的情况是找不开的

  4. 3元可以依靠9元的经验可以加一张3元来解决,结果是4张

  5. 2元是可以依靠7元的经验,加一张5元,结果是3张

  6. 对比下来最少的情况是3张

图表描述经验

把问题用如下图描述

- 5 3 2
2 - - 1
3 - 1 1
4 - - 2
5 1 1 1
6 - 2 2
7 - - 2

上述图的规则是从左到右,从上倒下的排查。每个经验的格子表示占用的最小数量。经验是从左到右的,例如3元的组成,经验到2元的时候,2元组不成3元,本来是-的,但是前面3元可以,相比之下,最小是1。5元的组成,5元本身是可以组成的,为1,3元组不成,但是可以从5元那里取,还是1,到2元的时候,2加上(5-2)的情况,用钱数为3的经验为1,钱数为2,但是比前面传过来的1大,所以填写还是1。
这个图用dp来表示,每个格子的比较来源是dp[i-1][j]和dp[i][j-money]+1。选取最小的来作为答案。

代码描述

    public static int getCount(Integer[] arr, int aim) {
        //图表的构建
        int[][] dp = new int[arr.length][aim + 1];
        for (int i = 1; i <= aimi++) {
            for (int j = 0; j < arr.lengthj++) {
                if (i - arr[j] < 0) {
                    //把无解的设置为int的最大值
                    dp[j][i] = Integer.MAX_VALUE;
                } else {
                    //获取当前列前面的经验
                    int preNum = dp[j][i - arr[j]];
                    if (j >
 0) {
                        //当前列和前一列的比较,取最小值
                        dp[j][i] = Integer.min(preNum == Integer.MAX_VALUE ? Integer.MAX_VALUE : preNum + 1, dp[j - 1][i]);
                    } else {
                        dp[j][i] = preNum == Integer.MAX_VALUE ? Integer.MAX_VALUE : preNum + 1;
                    }

                }
            }
        }
        return dp[arr.length - 1][aim];
    }

动态规划

上面的经验描述就是动态规划法。很多算法书里讲解这个算法都很高深(其实基础算法里最难的确实是他),必须找公式表达,而且必须能总结画出上面的图。这个是很多书里的表达,这样反而不是很明白了,单纯靠题目总结公式挺难的。所以提出了先画图后出公式的做法,而且很好理解。每行表达的其实就是从左到右,花费最少的意思。具体的公式总结在画图的过程中可以慢慢来看,一旦画图后,有了坐标,很容易找出规律,比空想好多了 。每行只需要关注每行的结果就行,前几列压根用不到经验,所以解题的过程就是经验的具现化。
感觉这个题目好懂的话,就试试看另外一个题目。https://my.oschina.net/xpbob/blog/759137 这个题目我是用最书本的方式来解答的,大家可以对比一下,看看是不是今天的这种思路更让你方便使用动态规划。


原文始发于微信公众号(一次编译多处运行):从换钱的最少货币数谈动态规划