暴力递归如何转化为动态规划--以找零钱为例

题目来自牛客网:
给定数组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=0;i++){
            res+=findCount(arr,index+1,aim-i*arr[index]);
        }
        return res;
    }


    private static HashMap 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);
     }
 }

你可能感兴趣的:(数据结构与算法,算法,动态规划)