暴力遞歸如何轉化爲動態規劃--以找零錢爲例

題目來自牛客網:
給定數組arr,設數組長度爲n,arr中所有的值都爲正整數且不重複。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim,代表要找的錢數,求換錢的方法數有多少種。由於方法的種數比較大,所以要求輸出對10^9+7進行取模後的答案。

本例提供了四種方法,並給出了對應運行時間
1.暴力遞歸
2.記憶搜索
3.二維空間dp
3.壓縮空間一維空間dp

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Calendar;
import java.util.HashMap;

public class DivideMoney {
    public static void main(String[] args) throws IOException {
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        String[] s1 = bf.readLine().split(" ");
        int length = Integer.valueOf(s1[0]);
        int aim = Integer.valueOf(s1[1]);
        int[] arr = new int[length];
        String[] s2 = bf.readLine().split(" ");
        for(int i=0;i<length;i++){
            arr[i] = Integer.valueOf(s2[i]);
        }
        long start1 = Calendar.getInstance().getTimeInMillis();
        System.out.println(findCount(arr,0,aim)%1000_000_007);
        long end1 = Calendar.getInstance().getTimeInMillis();
        System.out.println("fun1花費的時間: "+(end1-start1));

        long start2 = Calendar.getInstance().getTimeInMillis();
        System.out.println(findCount2(arr,0,aim)%1000_000_007);
        long end2 = Calendar.getInstance().getTimeInMillis();
        System.out.println("fun2花費的時間: "+(end2-start2));

        long start3 = Calendar.getInstance().getTimeInMillis();
        System.out.println(findCount3(arr,0,aim)%1000_000_007);
        long end3 = Calendar.getInstance().getTimeInMillis();
        System.out.println("fun3花費的時間: "+(end3-start3));

        long start4 = Calendar.getInstance().getTimeInMillis();
        System.out.println(findCount4(arr,aim));
        long end4 = Calendar.getInstance().getTimeInMillis();
        System.out.println("pro花費的時間: "+(end4-start4));
    }

    /**
     * 暴力遞歸
     * @param arr
     * @param aim
     * @param index
     * @return
     */
    public static long findCount(int[] arr,int index,int aim){
        if(index == arr.length){
            return aim==0?1:0;
        }
        long res=0;
        for(int i=0;aim-i*arr[index]>=0;i++){
            res+=findCount(arr,index+1,aim-i*arr[index]);
        }
        return res;
    }


    private static HashMap<String,Long> hs = new HashMap<>();
    /**
     * 記憶路徑法,避免重複計算走過的路徑
     * @param arr
     * @param index
     * @param aim
     * @return
     */
    public static long findCount2(int[] arr,int index,int aim){
        if(index == arr.length){
            return aim==0?1:0;
        }
        long res=0;
        for(int i=0;aim-i*arr[index]>=0;i++){
            String key = (aim-i*arr[index]) + "_" + (index+1);
            if(hs.containsKey(key)){
                res+=hs.get(key);
            }else {
                res+=findCount2(arr,index+1,aim-i*arr[index]);
            }
        }
        hs.put(aim+"_"+index,res);
        return res;
    }

    /**
     * 常規動態規劃求解 空間複雜度爲n^2,自下而上的解法
     * @param arr
     * @param index
     * @param aim
     * @return
     */
    public static long findCount3(int[] arr, int index,int aim){
        int[][] dp = new int[arr.length+1][aim+1];
        dp[arr.length][0] = 1;
        long res = solve(arr,0,aim,dp);
        return res;
    }

    public static long solve(int[] arr,int index,int aim, int[][] dp){
        if(index==dp.length-1){
            return dp[index][aim];
        }
        for(int i=aim;i>=0;i=i-arr[index]){
            if(dp[index+1][i]!=0){
                dp[index][aim]+=dp[index+1][i];
            }else {
                dp[index][aim]+=solve(arr,index+1,i,dp);
            }
        }
        return dp[index][aim];
    }

    /**
     * 最好的解法: 空間壓縮到O(n)! 當前剩餘aim值(剩餘aim對應爲數組dp的當前下標j) 對 所有零錢求解個數 的疊加
     * @param coin
     * @param aim
     * @return
     */
    public static int findCount4(int[] coin, int aim) {
        int[]dp = new int[aim + 1];
        dp[0] = 1;
        for (int i = 0; i < coin.length; i++) {     //遍歷零錢種類
            for (int j = coin[i]; j <= aim; j++) {      //當前j對應的就是剩餘的aim,從j到j-coin[i]用的是零錢coin[i]
                dp[j] = (dp[j] + dp[j- coin[i]]) % 1_000_000_007;
            }
        }
        return dp[aim];
    }
}

在console輸入測試用例

5 1000
2 3 5 7 10

輸出如下 總共的方法是有20932712種(也太多了吧!)

20932712
fun1花費的時間: 14042
20932712
fun2花費的時間: 66
20932712
fun3花費的時間: 15
20932712
pro花費的時間: 1

可以看到我們花費的時間越來越短
暴力遞歸改動態規劃總結:
1.找出我們的原始目標,在這裏是index =0,aim處,便是我們的原始目標
2.找出不依賴,確定結果的數據,fun3中二位數組dp的最後一行便是我們確定的數據。所以我們令dp[arr.length][0]=1,其他的默認都是0;
3.找出位置間的依賴,也就是當前位置可以由哪些位置的值得到結果;
如fun3中由dp下一行左側每隔arr[index]的數據累加得到:

 for(int i=aim;i>=0;i=i-arr[index]){
     if(dp[index+1][i]!=0){
         dp[index][aim]+=dp[index+1][i];
     }else {
         dp[index][aim]+=solve(arr,index+1,i,dp);
     }
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章