题目来自牛客网:
给定数组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);
}
}