动态规划专题 01背包问题详解 HDU 2546 饭卡
我以此题为例,详细分析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背包有所帮助!