題目來自牛客網:
給定數組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);
}
}