初探动态规划——LeetCode找零钱问题

1.简介:

在leetcode上刷题的时候,遇到了一道找零钱的动态规划题,后台测试用例很变态,必须把算法优化的很好才能通过。也借此机会好好的研究了一下动态规划。在下小白一个,大神轻喷。

2.题目如下:

《初探动态规划——LeetCode找零钱问题》 image.png

分析:我们可以从最简单的暴力搜索开始,先优化成记忆搜索方法,再到动态规划,最后再对动态规划进行空间优化。

1)最简单的思路就是用暴力搜索的方法,枚举每一种情况,累加求和,但存在大量重复计算。时间复杂度 > O((amount * coins.length)²)

2)进一步我们可以优化成记忆搜索方法,将每一个计算结果保存下来,用空间换时间,减少重复计算。时间复杂度 = O((amount * coins.length)²)

3)在记忆搜索的基础上,优化成动态规划,规划好路径,消除重复计算。和记忆搜索一样,用空间换时间。
时间复杂度O(amount * coins.length)

2.暴力搜索法

暴力搜索即用穷举法解决,穷举每一种可能再累加,基于示例1来看
1)结果 = 当不用coins[0]硬币时的组合数 +
使用1个coins[0]硬币时的组合数 +
使用2个coins[0]硬币时的组合数 +

使用5个coins[0]硬币时的组合数。
2)不使用coins[0]硬币 = 不使用coins[1]硬币 +
使用1个coins[1]硬币 +
使用2个coins[1]硬币+

对每个子项累加等于amount时返回1,否则0
可以用amount累减来代替累加
代码如下:

    public void solveCutCoin(int[] coins, int amount){
        System.out.println(codeCutCoin(coins, 0, amount));
    }
    //返回coinArr[index...N-1]种硬币的分割方法
    public int codeCutCoin(int[] coins, int index, int amount){
        //baseCase
        int res = 0;
        //如果是最后一种硬币,并且amount=0
        if (index >= coins.length){
            res = 0 == amount ? 1 : 0;
            return res;
        }

        //用amount-硬币面值 代替累加。
        for(int i = 0; i* coins[index]<= amount; i++){
            res += codeCutCoin(coins, index+1, amount -i* coins[index]);
        }
        return res;
    }

3.记忆搜索法

分析:记忆搜索方法就是将暴力法里的计算结果存储起来,用空间换时间的方法,需要进行以下两步修改:

1)方法中有两个变量index和amount,所以新建一个
int counts = new int[coins.length][amount],
为了方便边界处理,可以建[coins.length+1][amount+1]的。
counts[i][j]指用coins[i]...coins[n-1]这几种硬币,可以组成 j 金额的组合数

2)在取值的时候,先判断[index][amount]对应位置的值是否计算过,
(1)如果计算过,直接使用;
(2)没计算过,计算后将值记录下来。
Java中int数组默认值为0,为了区分是没计算过,还是组合数为0,我们将组合数为0的位置赋值-1,取用时判断是-1返回0。
代码如下:

    //记忆搜索方法
    public void solveCutCoin2(int[] coins, int aim){
        int[][] count = new int[coins.length+1][aim+1];
        System.out.println(codeCutCoin2(coins, 0, aim, count));
    }
    public int codeCutCoin2(int[] coins, int index, int amount, int[][] counts){
        //baseCase
        int res = 0;
        if (index >= coins.length){
            res = 0 == amount ? 1 : 0;
            return res;
        }

        //当前位置值不为0,表示计算过,直接使用
        if (0 != counts[index][amount]){
            return (-1 == counts[index][amount]) ? 0 : counts[index][amount];
        }
        //计算当前位置的值
        int count;
        for(int i = 0; i* coins[index]<= amount; i++){
            //先判断是否计算过
            count = counts[index+1][amount -i* coins[index]];
            if (0 != count){
                res += (-1 == count) ? 0 : count;
            }else {
                //没计算过,重新计算
                res += codeCutCoin2(coins, index+1, amount -i* coins[index], counts);
            }
        }
        //将每次计算过的值保存,用-1标记组合数为0的位置
        counts[index][amount] = (0 == res) ? -1 : res;
        return res;
    }

4.动态规划

分析:动态规划是将一个问题化简为子问题,通过子问题的最优解,组成整体最优,要理解整个过程,首先我们得画个图,分析一下暴力搜索的过程。

暴力搜索:
表格填充的数值为:只用coins[i]有多少种组合得到amount[j],非0即1.
第二行的取值为下面公式的值, 图1此时index=0,coins[index]=1
counts[index+1][amount -i* coins[index]];
为了帮助理解我特意画了两个图,示例1画出的图很多细节没法展示出来。

《初探动态规划——LeetCode找零钱问题》 初始化

《初探动态规划——LeetCode找零钱问题》 搜索完成后

上面两张图是从右上角点往下求的,由于解法和硬币面值顺序无关,我们同样可以从下往上求,图2初始index=2, coins[index]=5, amount=10
counts[index-1][amount -i* coins[index]];
如下图所示:

《初探动态规划——LeetCode找零钱问题》 自下而上搜索

我们根据面两个图继续分析,已知:
总组合数:counts[i][j] = 使用0枚5元硬币:counts[i-1][j]
+使用1枚5元:counts[i-1][j-coins[i] * 1]

+使用n枚5元:counts[i-1][j-coins[i] * n ]
即枚举他前一排黄色格子累加
我们发现
counts[i][j-coins[i] * 1] = count[i-1][j-coins[i] * 1]
+counts[i-1][j-coins[i] * 2]

+counts[i-1][j-coins[i] * n]
所以
counts[i][j] = counts[i][j-coins[i] * 1] + counts[i-1][j]
将公式套到上面两个表格,结果成立。
至此,动态规划的思路已经出来了,上面的一大串确实很繁琐,但理解后还是很简单的。其实动态规划就是对暴力搜索的一种化简,使得计算更高效。
只要列举出可能性,解决了counts[i][j] = 什么 + 什么,大部分问题也就迎刃而解了。
代码实现:

    //动态规划
    public void solveCutCoin3(int[] coinArr, int aim){
        int[][] counts = new int[coinArr.length][aim+1];
        System.out.println(codeCutCoin3(coinArr, coinArr.length-1, aim, counts));
    }
    public int codeCutCoin3(int[] coins, int index, int amount, int[][] counts){
        //baseCase
        if (0 == amount){
            return 1;
        }
        if (index < 0 || amount < 0){
            return 0;
        }
        if (0 == counts[index][amount]){
            int res = codeCutCoin3(coins, index-1, amount, counts)
                    + codeCutCoin3(coins, index, amount - coins[index], counts);
            counts[index][amount] = (0 == res) ? -1 : res;
        }
        return -1 == counts[index][amount] ? 0 : counts[index][amount];
    }

再上一张动态规划的路径截图:

《初探动态规划——LeetCode找零钱问题》 image.png

5.大神的解法

上面解法看似已经很好了,但是在大神眼里这什么都不是,leetCode评论区里的解法,时间复杂度O(coins.length * amount), 空间复杂度O(amount)
在绝对的实力面前,瑟瑟发抖。
我们来看看大神的解法:

《初探动态规划——LeetCode找零钱问题》 image.png

6.谈一谈怎么写递归

很多人可能觉得递归函数很难写,事实也如此。递归函数要求每一步都高度一致,很难一步到位,所以不妨分成几个步骤,逐个突破。我写递归一般有4个步骤:
1)确定需要的信息,如组合数
2)写出BaseCase,即在什么情况下不需要递归,返回确定值
3)将递归函数当做黑盒使用
4)解黑盒,将需要的信息返回给上一个调用者

    原文作者:月塘路
    原文地址: https://www.jianshu.com/p/50ccd1c01ff1
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞