這個題首先仍是想到枚舉回溯,對a[v]排序及時終止不合適的進行優化搜索,可是可想而知的在最後幾個測試例超時。這是我寫的代碼。php
/* ID: jzzlee1 PROG:money LANG:C++ */ #include<iostream> #include<fstream> #include<cstdio> using namespace std; //ifstream cin("money.in"); //ofstream cout("money.out"); long long ans; int main() { int v,n,i,j,temp,a[25],b[25],d[25]={0},sum[25]={0}; cin>>v>>n; for(i=0;i!=v;++i) cin>>a[i]; //a[i]=25-i; for(i=0;i!=v;++i) for(j=v-1;j!=i;--j) if(a[j]<a[j-1]) { temp=a[j]; a[j]=a[j-1]; a[j-1]=temp; } for(i=0;i!=v;++i) b[i]=n/a[i]; for(d[24]=0;v>=25?d[24]<=b[24]:d[24]==0;++d[24]) { sum[24]=a[24]*d[24]; for(d[23]=0;v>=24?d[23]<=b[23]:d[23]==0;++d[23]) { sum[23]=sum[24]+a[23]*d[23]; if(sum[23]>n) break; for(d[22]=0;v>=23?d[22]<=b[22]:d[22]==0;++d[22]) { sum[22]=sum[23]+a[22]*d[22]; if(sum[22]>n) break; for(d[21]=0;v>=22?d[21]<=b[21]:d[21]==0;++d[21]) { sum[21]=sum[22]+a[21]*d[21]; if(sum[21]>n) break; for(d[20]=0;v>=21?d[20]<=b[20]:d[20]==0;++d[20]) { sum[20]=sum[21]+a[20]*d[20]; if(sum[20]>n) break; for(d[19]=0;v>=20?d[19]<=b[19]:d[19]==0;++d[19]) { sum[19]=sum[20]+a[19]*d[19]; if(sum[19]>n) break; for(d[18]=0;v>=19?d[18]<=b[18]:d[18]==0;++d[18]) { sum[18]=sum[19]+a[18]*d[18]; if(sum[18]>n) break; for(d[17]=0;v>=18?d[17]<=b[17]:d[17]==0;++d[17]) { sum[17]=sum[18]+a[17]*d[17]; if(sum[17]>n) break; for(d[16]=0;v>=17?d[16]<=b[16]:d[16]==0;++d[16]) { sum[16]=sum[17]+a[16]*d[16]; if(sum[16]>n) break; for(d[15]=0;v>=16?d[15]<=b[15]:d[15]==0;++d[15]) { sum[15]=sum[16]+a[15]*d[15]; if(sum[15]>n) break; for(d[14]=0;v>=15?d[14]<=b[14]:d[14]==0;++d[14]) { sum[14]=sum[15]+a[14]*d[14]; if(sum[14]>n) break; for(d[13]=0;v>=14?d[13]<=b[13]:d[13]==0;++d[13]) { sum[13]=sum[14]+a[13]*d[13]; if(sum[13]>n) break; for(d[12]=0;v>=13?d[12]<=b[12]:d[12]==0;++d[12]) { sum[12]=sum[13]+a[12]*d[12]; if(sum[12]>n) break; for(d[11]=0;v>=12?d[11]<=b[11]:d[11]==0;++d[11]) { sum[11]=sum[12]+a[11]*d[11]; if(sum[11]>n) break; for(d[10]=0;v>=11?d[10]<=b[10]:d[10]==0;++d[10]) { sum[10]=sum[11]+a[10]*d[10]; if(sum[10]>n) break; for(d[9]=0;v>=10?d[9]<=b[9]:d[9]==0;++d[9]) { sum[9]=sum[10]+a[9]*d[9]; if(sum[9]>n) break; for(d[8]=0;v>=9?d[8]<=b[8]:d[8]==0;++d[8]) { sum[8]=sum[9]+a[8]*d[8]; if(sum[8]>n) break; for(d[7]=0;v>=8?d[7]<=b[7]:d[7]==0;++d[7]) { sum[7]=sum[8]+a[7]*d[7]; if(sum[7]>n) break; for(d[6]=0;v>=7?d[6]<=b[6]:d[6]==0;++d[6]) { sum[6]=sum[7]+a[6]*d[6]; if(sum[6]>n) break; for(d[5]=0;v>=6?d[5]<=b[5]:d[5]==0;++d[5]) { sum[5]=sum[6]+a[5]*d[5]; if(sum[5]>n) break; for(d[4]=0;v>=5?d[4]<=b[4]:d[4]==0;++d[4]) { sum[4]=sum[5]+a[4]*d[4]; if(sum[4]>n) break; for(d[3]=0;v>=4?d[3]<=b[3]:d[3]==0;++d[3]) { sum[3]=sum[4]+a[3]*d[3]; if(sum[3]>n) break; for(d[2]=0;v>=3?d[2]<=b[2]:d[2]==0;++d[2]) { sum[2]=sum[3]+a[2]*d[2]; if(sum[2]>n) break; for(d[1]=0;v>=2?d[1]<=b[1]:d[1]==0;++d[1]) { sum[1]=sum[2]+a[1]*d[1]; if(sum[1]>n) break; //for(d[0]=0;v>=1?d[0]<=b[0]:d[0]==0;++d[0]) //{ if((n-sum[1])%a[0]==0) ++ans; //else if(sum[1]+a[0]*d[0]>n) //break; //}//0 }//1 }//2 }//3 }//4 }//5 }//6 }//7 }//8 }//9 }//10 }//11 }//12 }//13 }//14 }//15 }//16 }//17 }//18 }//19 }//20 }//21 }//22 }//23 }//24 cout<<ans<<endl; return 0; }
後來想是要動態規劃,本身想了一下,無甚所得,作dp也三五個題了,仍是一點不開竅啊,看來欲速則不達,須要補充一下dp的理論知識了。看了一下別人的題解,http://sdjl.me/index.php/archives/97,三種思路(原文爲java代碼,我用C++改寫,另外,此題夠陰,空間複雜度O(v*n)時運行錯,只有空間複雜度O(n)時才能夠):java
首先我會引導讀者如何去思考這道題的動規方法,經過一個時間效率爲O(v*n*n)、空間效率爲O(V*n)的簡單方法,讓讀者理解程序的正確性。
而後我將改變一下思考的角度,介紹一個時間效率爲O(v*n)、空間效率爲O(v*n)的方法,讓讀者注意到對於一樣的方法當考慮角度稍有變化時如何影響到算法的效率。
最後我將介紹一種時間效率爲O(v*n)、空間效率爲O(n)的方法,指出在動態規劃中一種減小空間使用量的經常使用方法,這對參加編程比賽是頗有用的。
注:dp[i][j]表示在前i種硬幣中,拿出總值爲j元的方案數
方法1:
仍是先模擬過程,而後從過程的最後一步考慮起。注意,這裏所說的「模擬」,就是在告訴讀者如何去思考這個問題。
能夠這樣來模擬:有一排桌子,共有v個,每張桌子上面都有一堆拿不完的硬幣,可是每張桌子上的硬幣都是相同面額的,我從第一張桌子順序走到最後一張桌子,每路過一張桌子的時候我能夠拿取這張桌子上面的任意個硬幣,直到路過全部桌子後我手中的硬幣總額必須爲n元。
考慮最後一步:若是我在第i張桌子上拿了m個硬幣,而後就再也不在其它的桌子上拿硬幣了,且我手中的錢正好是n元,那麼最後一步就是「在第i張桌子上面拿取了m個硬幣」
在最後一步中,我能夠拿0個或多個硬幣,這就是最後一步的選擇,那麼選擇拿取m個硬幣後剩下的子問題就是從前i-1張桌子上拿取j-m×a[i]錢有多少種方法,其中a[i]是第i張桌子上硬幣的面額。
由於每種選擇都是獨立可行的方法,所以有動規方程: dp[i][j] = Sum(dp[i - 1][j - m * a[i]]) {m = 0、一、二、……、k},k是最多能夠拿取的個數。
注意到,這裏問題數就是dp變量的個數,等於v * n,而選擇數最壞狀況下爲n,所以最壞狀況下時間效率爲O(v*n*n)。ios
code 1:算法
/* ID: jzzlee1 PROG:money LANG:C++ */ #include<iostream> #include<fstream> using namespace std; //ifstream cin("money.in"); //ofstream cout("money.out"); unsigned long long dp[25][10001]; int main() { int v,n,a[30]; cin>>v>>n; int i,j; for(i=0;i!=v;++i) cin>>a[i]; for(j=0;j<=n;++j) if(j%a[0]==0) dp[0][j]=1; for(i=0;i!=v;++i) dp[i][0]=1; for(i=1;i!=v;++i) for(j=1;j<=n;++j) { int d=0; while(d*a[i]<=j) { dp[i][j]+=dp[i-1][j-d*a[i]]; ++d; } } cout<<dp[v-1][n]<<endl; return 0; }
方法2:
若是咱們在模擬過程當中稍微作一點變更就會發現一種效率更高的算法,以下:
在方法一的模擬過程當中,對於每一步的描述能夠表達以下「在第i張桌子上我應該拿取多少個硬幣?」,如今改成「在第i張桌子上我是否應該再拿取一個硬幣?(若是不拿,那就走向下一張桌子)」
此時思考的角度就從「拿多少個(選擇數爲O(n))」到「拿與不拿(選擇數爲O(1))」,可見選擇數變少了,可是子問題發生了變化。
方法1的子問題能夠表達以下「在前i-1張桌子上拿取總額爲j-m*a[i]的方法數」,而方法2的子問題變爲「當再拿取一個硬幣時,在前i張桌子上拿取總額爲j – a[i]的方法數」與「再也不拿硬幣時,在前i張桌子上拿取總額爲j的方法數」,至於「最優子結構」問題讀者本身證實。
所以可得以下動規方程:dp[i][j] = dp[i][j-a[i]] + dp[i-1][j],dp[i][j-a[i]]是再拿一個的狀況,dp[i-1][j]是再也不拿走向下一張桌子的狀況。 (提示:設在第i張桌子上拿取了m個硬幣,當m>0時, 全部的方法都被dp[i][j-a[i]]包含了,所以當走向下一張桌子時僅須要考慮m=0的狀況。)
可見子問題數沒變而選擇數減小了一個數量級,所以時間效率提升到O(v*n)
見 code2編程
/* ID: jzzlee1 PROG:money LANG:C++ */ #include<iostream> #include<fstream> using namespace std; //ifstream cin("money.in"); //ofstream cout("money.out"); unsigned long long dp[25][10001]; int main() { int v,n,a[30]; cin>>v>>n; int i,j; for(i=0;i!=v;++i) cin>>a[i]; for(j=1;j<=n;++j) if(j%a[0]==0) dp[0][j]=1; for(i=0;i!=v;++i) dp[i][0]=1; for(i=1;i!=v;++i) for(j=1;j<=n;++j) { if (j - a[i] >= 0) { dp[i][j] = dp[i - 1][j] + dp[i][j - a[i]]; } else { dp[i][j] = dp[i - 1][j]; } } cout<<dp[v-1][n]<<endl; return 0; }
方法3:
注意到方法2中的動規方程:dp[i][j] = dp[i][j-a[i]] + dp[i-1][j]
咱們在求dp[i][*]時僅會用到dp[i-1][*],而不會用到dp[i-2][*],dp[i-3][*]等等。
這就表示,任什麼時候刻咱們均可以僅用兩個數組來保存dp的值,而不用v個,公式就能夠簡化爲: dp_2[j] = dp_2[j-a[i]] + dp_1[j]。
且在求dp_2[j]時,dp_2[j]的值能夠是任意值而不會影響到dp_2[j]的正確性(由於它的值是由dp_2[j-a[i]]與dp_1[j]決定的),那麼咱們就能夠用dp_2[j]的來保存dp_1[j]的值,公式能夠改成: dp_2[j] = dp_2[j-a[i]] + dp_2[j]。
注意,當計算dp_2[j] = dp_2[j-a[i]] + dp_2[j]時,等號左邊的dp_2[j]表示「前i張桌子拿取j元的方案數」,而等號右邊的dp_2[j]表示「前i-1張桌子拿取j元的方案數」。
這就只須要用一個大小爲O(n)的dp數組了。空間效率從O(v*n)提升到了O(n)。
見 code3
數組
/* ID: jzzlee1 PROG:money LANG:C++ */ //#include<iostream> #include<fstream> using namespace std; ifstream cin("money.in"); ofstream cout("money.out"); unsigned long long dp[10001]; int main() { int v,n,a[30]; cin>>v>>n; int i,j; for(i=0;i!=v;++i) cin>>a[i]; for(j=0;j<=n;++j) if(j%a[0]==0) dp[j]=1; for(i=1;i!=v;++i) for(j=1;j<=n;++j) { if (j - a[i] >= 0) { dp[j] = dp[j] + dp[j - a[i]]; } } cout<<dp[n]<<endl; return 0; }
還有一種分析,測試
…………………………………………(usaco分析)…………………………………………優化
We use dynamic programming to count the number of ways to make n cents with the given coins. If we denote the value of the kth coin by c_k, then the recurrence is:spa
nway(n, k) = no. of ways to make n cents with the first k types of coins
nway(n, k) = nway(n, k-1) + nway(n-c_k, k)code
This just says the number of ways to make n cents with the first k coins is the number of ways to make n cents using the first k-1 coins (i.e., without using the kth coin) plus the number of ways to make n-c_k cents using the first k coins. For the second set of ways, we then add the kth coin to arrive at a total of n cents.
We keep track of the number of ways to total "n" cents in "nway", updating the array as we read the value of each coin.
代碼:
#include <iostream> #include<fstream> using namespace std; //ifstream cin("money.in"); //ofstream cout("money.out"); long long nway[10001]; int main() { int i, j, n, v, c; cin>>v>>n; nway[0] = 1; for(i=0; i<v; i++) { cin>>c; for(j=c;j<=n;j++) nway[j]+=nway[j-c]; } cout<<nway[n]<<endl; return 0; }
對於動態規劃,真的須要作點什麼了,先把dd牛的揹包九講好好看下。