【演算法】找零錢-動態規劃實現過程解析

一個暱稱而已T發表於2017-09-29

本文章素材來自:https://www.nowcoder.com/study/vod/1/12/1


題目要求:

有陣列penny,penny中所有的值都為正數且不重複。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim(小於等於1000)代表要找的錢數,求換錢有多少種方法。
給定陣列penny及它的大小(小於等於50),同時給定一個整數aim,請返回有多少種方法可以湊成aim。

假設測試資料如下:
int[] penny=new int[]{5, 15, 25, 1};
int aim=1000;

1、暴力求解法(利用遞迴的思想)

實現思想:根據上面的測試的資料,有如下想法
這裡寫圖片描述

因此可以定義遞迴過程:
這裡寫圖片描述

具體的演算法在這裡就不貼出來來,可以參考第2點的演算法實現。


2、記憶化搜尋法(在暴力求法的基礎上修改的)

public static int countWays(int[] penny, int aim) {
        // write code here
        if (penny == null || penny.length == 0 || aim < 0) return 0;
        else {
            HashMap<String, Integer> map = new HashMap<>();
            return fun( map,penny, 0, aim);
        }
    }

    public static int fun(HashMap<String,Integer> map, int[] arr, int index, int aim) {
        if (index == arr.length) {
            //如果aim剛好為0了,就表示找到了一個方案,否則penng陣列中的幣值用完了aim還有剩餘就表示方案行不通,即沒有對應的方案
            return aim == 0 ? 1 : 0;
        }

        String key = index + "-" + aim;
        if (map.containsKey(key)) return map.get(key);

        int res = 0;
        for (int i = 0; arr[index] * i <= aim; i++)
            res += fun(map, arr, index + 1, aim - arr[index] * i);
        map.put(key, res);
        return res;
    }

因為暴力求解時使用的遞迴,因此會存在重複的計算,因此在原基礎上使用一個資料結構將已經計算好的結果儲存起來,如果需要再次計算時就直接從裡面取出結果,這樣就可以避免遞迴的重複計算。

因此,在上述程式碼中,使用來一個HashMap 來儲存計算好的結果,其中Map的key是由indexaim 組成的,對應的value 即為已經計算過的結果。


3、由 記憶化搜尋引出的 動態規劃

這裡寫圖片描述
O(n * aim^2)

public static int countWays(int[] penny, int aim) {
        if (penny == null || penny.length == 0 || aim < 0) return 0;
        int dp[][]=new int[penny.length][aim+1];
        for (int i = 0; i < penny.length; i++)    dp[i][0] = 1;
        for (int i = 1; i <= aim; i++)
        {
            if (i % penny[0] == 0)  dp[0][i] = 1;
            else                    dp[0][i] = 0;
        }
        for (int i = 1; i < penny.length; i++)
        {
            for (int j = 1; j <= aim; j++)
            {
                int count = 0;
                for (int k = 0; penny[i]*k <= j; k++)
                    count += dp[i-1][j-penny[i]*k];
                dp[i][j] = count;
            }
        }
        return dp[penny.length-1][aim];
    }

這裡寫圖片描述

由記憶搜尋法與其引出的動態規劃法的核心演算法的比較,發現並無太大的差異,而主要的區別就是如上圖所說的,動態規劃規定好了計算的順序(計算dp[i][j] 就要先計算出 dp[i-1][0 ~ j] 的結果,然後在列舉求和,而dp[i-1][0 ~ j] 每個元素的結果又需要由對應的上一排的列舉求和實現…),而記憶搜尋法本質還是遞迴,只不過優化了其過程,避免的重複的遞迴計算。


4、優化後的動態規劃

這裡寫圖片描述

根據上圖的可以知道,需要累加的項只有dp[i-1][j-1*arr[i]]以及其所在那一排的且位於它前面的某些項(這一部分就相當於dp[i][j-arr[i]),以及dp[i-1][i] 這一項。因此可以優化原有動態規劃的實現,避免上一排列舉求和的操作。

注意:為什麼說只有部分項需要累加呢,這因為這些項之間都是依次相差 (arr[i]-1)個位置,而兩個項中間的元素的值,其實是為0。舉例說明,因為dp[i-1][j-1*arr[i]] (表示使用1 張arr[i]貨幣),到 dp[i-1][j] 之間(表示完全不用使用arr[i]貨幣),其中包含了 j - arr[i]+1j - arr[i]+2…、、j - arr[i]+(arr[i]-1),而這些是湊不齊1張arr[i]貨幣的,因此會有(零錢)剩餘,而剩餘就表示該方案行不通,即為0。

public static int countWays(int[] penny, int aim) {
        if (penny == null || penny.length == 0 || aim < 0) return 0;
        int dp[][]=new int[penny.length][aim+1];
        for (int i = 0; i < penny.length; i++)     dp[i][0] = 1;
        for (int i = 1; i <= aim; i++)
        {
            if (i % penny[0] == 0)      dp[0][i] = 1;
            else                        dp[0][i] = 0;
        }

        for (int i = 1; i < penny.length; i++)
        {
            for (int j = 1; j <= aim; j++)
            {
                if (j < penny[i])   dp[i][j] = dp[i-1][j];
                else                dp[i][j] = dp[i-1][j] + dp[i][j-penny[i]];
            }
        }
        return dp[penny.length-1][aim];
    }

這裡寫圖片描述

這裡寫圖片描述

相關文章