我以此題為例,詳細分析01背包問題,希望該題能夠為大家對01背包問題的理解有所幫助,對這篇博文有什么問題可以向我提問,一同進步^_^
飯卡
Time Limit: 5000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 14246 Accepted Submission(s): 4952
某天,食堂中有n種菜出售,每種菜可購買一次。已知每種菜的價格以及卡上的余額,問最少可使卡上的余額為多少。
第一行為正整數n,表示菜的數量。n<=1000。
第二行包括n個正整數,表示每種菜的價格。價格不超過50。
第三行包括一個正整數m,表示卡上的余額。m<=1000。
n=0表示數據結束。
這條題目里,我們要先注意要達到最小余額,那么最大的菜價一定是最后要減的,那么我們將這一組飯菜價格按從小到大排序,將最大的那個先放一邊,我們接下來就是要把剩下的一些菜價用我們手頭的余額減,當然必須要保證減去的金額小於等於sum-5,這樣我們才能在最后一次把最大的菜價刷掉。
我們做的轉化就是,把除了最大菜價之外,其他的菜價裝入一個sum-5 的背包里,看最大能裝多少。
首先基於上一篇我們的理論。(很重要!)
【理論講解】http://www.cnblogs.com/fancy-itlife/p/4393213.html
首先看第一個條件—最優子結構。最大的裝入量一定是如果裝入第i個或者不裝入第i個的兩個選擇之一。
第二個條件—子問題重疊。當完成一個階段比如裝第i個,我下面做的是對第i-1個進行抉擇,你可以發現跟前面的問題一樣,裝還是不裝兩個選擇之一。這就是所謂的子問題重疊。
第三個條件—邊界。這樣的選擇總歸要有個結束的時候,當他到了第一個菜價時,如果它的背包容量也就是余額大於菜價,一定要裝進去啊,這才會有可能變得比較大。如果不夠的話,那一定是0。至此選擇全部結束,然后是遞歸地返回上一層,直至抉擇出正確答案。
第四個條件—子問題獨立。裝還是不裝兩個選擇,雙方的選擇不會影響對方。
下面我們就要來考慮一下,第五個條件—備忘錄,也就是記憶化搜索,如果這個結果的值已經得到,那么我們把它記錄下來,以便后面再出現該值時直接使用。那么對於此問題的獨立的小問題的就是執行了前n個菜價的抉擇(裝或不裝),余額還剩m時的最大容量。可以用一個二維數組表示n*m。
那么上面已經詳細敘述了該問題的求解方式,用記憶化的方式先來實現一下!
代碼
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #define MAXN 1005 6 using namespace std; 7 int price[MAXN]; 8 int total[MAXN][MAXN]; 9 int dfs(int m,int k)//利用記憶化搜索實現01背包 10 { 11 int s; 12 if(total[m][k]>=0)//如果該值已經被記錄了那么直接返回 13 return total[m][k]; 14 if(k==1)//處理邊界值 15 { 16 if(m>=price[1])//如果剩余的額度大於等於該菜價,那么一定返回要將該菜價賦給s 17 s=price[1]; 18 else//如果剩余的額度小於該菜價,那么一定返回0 19 s=0; 20 } 21 else if(m>=price[k])//如果此時的額度是大於等於當前的菜價,則是這兩種選擇之中的一個 22 s=max((dfs(m-price[k],k-1)+price[k]),dfs(m,k-1)); 23 else//如果此時的額度是小於當前的菜價,則僅考慮不買這個菜的情況! 24 s=dfs(m,k-1); 25 total[m][k]=s;//記憶化 26 return s; 27 } 28 int main() 29 { 30 int n,i,sum,s; 31 while(scanf("%d",&n)!=EOF) 32 { 33 if(n==0) 34 break; 35 memset(total,-1,sizeof(total)); 36 for(i=1;i<=n;i++) 37 scanf("%d",&price[i]); 38 scanf("%d",&sum); 39 if(sum<=4) 40 printf("%d\n",sum); 41 else 42 { 43 sort(price+1,price+n+1); 44 s=sum; 45 sum=dfs(sum-5,n-1); 46 sum=s-sum-price[n]; 47 printf("%d\n",sum); 48 } 49 } 50 return 0; 51 }
但其實記憶化搜索的方式,比較適合初學時理解,但是其實它的不足在於遞歸開銷太大,效率不算很高。
接下來我們試着用遞推的方式來實現該過程其實我們完全可以將每一個子問題由小到大不斷由前面的已解決的問題中推出,比如只有一個菜價時,根據余額和菜價的關系直接就可以得到最大的價值,(這也一定是正確且最大的)到達第二個菜價時,我們抉擇的還是裝還是不裝,裝的話,我們要把余額減去第二個菜價看看還剩的錢在前一個選擇面前我們能獲得的最大金額再加上第二個菜價與不裝第二個菜的最大金額比較大小,那么不裝第二個菜,那就是第一個菜在這種余額下的最大金額。那么由於第一個階段是滿足最優的,那么你通過兩種選擇,也就得到了第二個階段的最有情況。那么往復這樣的情況我們就獲得了遞推式的01背包求解。
代碼:
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #define MAXN 1005 6 using namespace std; 7 int price[MAXN]; 8 int total[MAXN][MAXN]; 9 int main() 10 { 11 int n,m,i,j,s,sum; 12 while(scanf("%d",&n)!=EOF) 13 { 14 if(n==0) 15 break; 16 memset(total,0,sizeof(total)); 17 for(i=1;i<=n;i++) 18 scanf("%d",&price[i]); 19 scanf("%d",&sum); 20 if(sum<=4) 21 printf("%d\n",sum); 22 else 23 { 24 sort(price+1,price+n+1); 25 for(i=0;i<=sum-5;i++) 26 { 27 if(i<price[1]) 28 total[1][i]=0; 29 else 30 total[1][i]=price[1]; 31 } 32 for(i=2;i<=n-1;i++)//i表示依次選取前n個菜品(標號) 33 for(j=0;j<=sum-5;j++)//j表示余額 34 { 35 if(j<price[i]) 36 total[i][j]=total[i-1][j]; 37 else 38 total[i][j]=max(total[i-1][j-price[i]]+price[i],total[i-1][j]); 39 } 40 s=0; 41 for(i=1;i<=n-1;i++) 42 for(j=0;j<=sum-5;j++) 43 { 44 if(s<total[i][j]) 45 s=total[i][j]; 46 } 47 //cout<<s<<" "<<price[n]<<endl; 48 sum=sum-s-price[n]; 49 printf("%d\n",sum); 50 } 51 } 52 return 0; 53 }
那么我們還可以再將空間減少為一維數組,原因是什么呢,代碼的注釋里詳細的解釋了。
代碼:
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #define MAXN 1005 6 using namespace std; 7 int price[MAXN]; 8 int total[MAXN]; 9 int main() 10 { 11 int n,m,i,j,s,sum; 12 while(scanf("%d",&n)!=EOF) 13 { 14 if(n==0) 15 break; 16 memset(total,0,sizeof(total)); 17 for(i=1;i<=n;i++) 18 scanf("%d",&price[i]); 19 scanf("%d",&sum); 20 if(sum<=4) 21 printf("%d\n",sum); 22 else 23 { 24 sort(price+1,price+n+1); 25 //為什么只要用到一維數組,因為它的第二維只跟前一階段有關, 26 //那么用一維數組就可以保存一個階段的值,下一個階段用上一個階段來更新 27 for(i=1;i<=n-1;i++)//前n個階段 28 for(j=sum-5;j>=0;j--)//表示此時該階段如果為有j余額 29 { 30 if(j>=price[i]) 31 total[j]=max(total[j-price[i]]+price[i],total[j]); 32 /*為什么需要逆序因為逆序可以帶來的正確性是不言而喻的 33 我需要將前一階段的j-price[i]余額的最大的消費獲取到, 34 如果正向的話,我在求取一些余額較大的值時可能獲得了該階段 35 的j-price[i]的最大的消費額,因為小的余額是先更新的。 36 */ 37 } 38 s=0; 39 for(j=1;j<=sum-5;j++) 40 { 41 if(s<total[j]) 42 s=total[j]; 43 } 44 sum=sum-s-price[n]; 45 printf("%d\n",sum); 46 } 47 } 48 return 0; 49 }
再看看這三題的時間空間效率對比
自己也是才接觸這類動態規划問題,也希望此篇博文對大家學習01背包有所幫助!