01背包

来源:互联网 发布:windows phone qq在线 编辑:程序博客网 时间:2024/06/05 03:46

动态规划:

有N件物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

状态转移方程:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]} 
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的 
伪码:
  for i=1..N 
   for v=V..0 
    f[v]=max{f[v],f[v-c[i]]+w[i]};
如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,
价值为f[i-1][v];
如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,

此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。


结合例题来看:

采药

Time Limit:   1000MS       Memory Limit:   65535KB 
Submissions:   155       Accepted:   50


Description辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?  
Input输入的第一行有两个整数T(1 <= T <= 1000)和M(1 <= M <= 100),用一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。 
Output输出包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。 
Sample Input
70 3
71 100
69 1
1 2
Sample Output
3

#include <iostream>#include <string>#define max(a,b) a>b?a:b;using namespace std;int main(){int dp[101][1001], m, T, w[101], val[101], i, j;cin >> T >> m;//t代表采药的时间,m代表草药的数目memset(dp,0,sizeof(dp));for (i = 1; i <= m; i++)cin >> w[i] >> val[i];//w表示采药的时间,val代表对应草药的价值for (i = 1; i <= m; i++){for (j = 0; j <= T; j++){//j相当于上面说的V-c[i] 剩余容量if (j >= w[i]){//dp[i][j]表示放i件物品,最大容量为j的 最大价值dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + val[i]);//放还是不放的选择}else{//当前物品放不进去,因此剩余容量还是为jdp[i][j] = dp[i - 1][j];}}}cout << dp[m][T] << endl;return 0;}



效率分析:

此算法的时间复杂度为O(N*V),空间复杂度也为O(N*V)。其中,N 表示物品个数,V 表示背包容量这里,时间复杂度不可以在优化了,但是空间复杂度可以继续优化到O(V)

优化空间复杂度

上述的方法,我们使用二维数组 f[i][v] 保存中间状态,这里我们可以使用一维数组f[v]保存中间状态就能得到结果

分析

我们现在使用f[v]保存中间状态,我们想要达到的效果是,第i次循环后,f[v]中存储的是前i个物体放到容量v时的最大价值

在回顾下之前讲过的状态转移方程:

[cpp] view plaincopy
  1. f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])  

我们可以看到,要想得到 f[i][v],我们需要知道 f[i - 1][v] 和 f[i - 1][v - weight[i]],由于我们使用二维数组保存中间状态,所以可以直接取出这两个状态。

当我们使用一维数组存储状态时,f[v]表示,在执行i次循环后(此时已经处理i个物品),前i个物体放到容量v时的最大价值,即之前的f[i][v]。与二维相比较,它把第一维隐去了,但是二者表达的含义还是相同的,只不过针对不同的i,f[v]一直在重复使用,所以,也会出现第i次循环可能会覆盖第i - 1次循环的结果。

为了求f[v],我们需要知道,前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v]  和 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + cost[i]。

难点:由于我们只使用一维数组存储,则在求这两个子问题时就没有直接取出那么方便了,因为,第i次循环可能会覆盖第i - 1次循环的结果。

现在我们来求这两个值

1)前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v] :

由于,在执行在i次循环时,f[v]存储的是前i个物体放到容量v时的最大价值,在求前i个物体放到容量v时的最大价值(即之前的f[i][v])时,我们是正在执行第 i 次循环,f[ v ]的值还是在第 i - 1  次循环时存下的值,在此时取出的 f[ v ]就是前i - 1个物体放到容量v时的最大价值,即f[i - 1][v]。

2)前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + cost[i]

由于,在执行第i次循环前,f[0 ~ V]中保存的是第i - 1次循环的结果,即是前i - 1个物体分别放到容量0 ~ V时的最大价值,即f[i - 1][0 ~ V]。

则,在执行第i次循环前,f 数组中v - weight[i]的位置存储就是我们要找的 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益 (即之前的f[i - 1][v - weight[i]]),这里假设物品是从数组下标1开始存储的。

伪代码

[cpp] view plaincopy
  1. for i=1..N //枚举物品  
  2.     for v=V..0 //枚举容量,从大到小  
  3.         f[v]=max{f[v],f[v-weight[i]] + cost[i]};  

由上面伪代码可知,在执行第 i 次循环时,需要把背包容量由V..0都要遍历一遍,检测第 i 件物品是否能放。

逆序枚举容量的原因:

注意一点,我们是由第 i - 1 次循环的两个状态推出 第 i 个状态的,而且 v  > v - weight[i],则对于第i次循环,背包容量只有当V..0循环时,才会先处理背包容量为v的状况,后处理背包容量为 v-weight[i] 的情况。

具体来说,由于,在执行v时,还没执行到v - weight[i]的,因此,f[v - weight[i]]保存的还是第i - 1次循环的结果。即在执行第i次循环 且 背包容量为v时,此时的f[v]存储的是 f[i - 1][v] ,此时f[v-weight[i]]存储的是f[i - 1][v-weight[i]]。

相反,如果在执行第 i 次循环时,背包容量按照0..V的顺序遍历一遍,来检测第 i 件物品是否能放。此时在执行第i次循环 且 背包容量为v时,此时的f[v]存储的是 f[i - 1][v] ,但是,此时f[v-weight[i]]存储的是f[i][v-weight[i]]。

因为,v  > v - weight[i],第i次循环中,执行背包容量为v时,容量为v - weight[i]的背包已经计算过,即f[v - weight[i]]中存储的是f[i][v - weight[i]]。即,对于01背包,按照增序枚举背包容量是不对的。

代码

[cpp] view plaincopy
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. const int N = 3;//物品个数  
  5. const int V = 5;//背包最大容量  
  6. int weight[N + 1] = {0,3,2,2};//物品重量  
  7. int value[N + 1] = {0,5,10,20};//物品价值  
  8.   
  9. int f[V + 1] = {0};  
  10.   
  11. int Max(int x,int y)  
  12. {  
  13.     return x > y ? x : y;  
  14. }  
  15.   
  16. /* 
  17. 目标:在不超过背包容量的情况下,最多能获得多少价值 
  18.  
  19. 子问题状态:f[j]:表示前i件物品放入容量为j的背包得到的最大价值 
  20.  
  21. 状态转移方程:f[j] = max{f[j],f[j - weight[i]] + value[i]} 
  22.  
  23. 初始化:f数组全设置为0 
  24. */  
  25. int Knapsack()  
  26. {  
  27.     //初始化  
  28.     memset(f,0,sizeof(f));  
  29.     //递推  
  30.     for (int i = 1;i <= N;i++) //枚举物品  
  31.     {  
  32.         for (int j = V;j >= weight[i];j--) //枚举背包容量,防越界,j下限为 weight[i]  
  33.         {  
  34.             f[j] = Max(f[j],f[j - weight[i]] + value[i]);  
  35.         }  
  36.     }  
  37.     return f[V];  
  38. }  
  39.   
  40. int main()  
  41. {  
  42.     cout<<Knapsack()<<endl;  
  43.     system("pause");  
  44.     return 1;  
  45. }  

但是,增序枚举背包容量会达到什么效果:它会重复的装入某个物品,而且尽可能多的,使价值最大,当然不会不超过背包容量

而逆序枚举背包容量:背包中的物品至多装一次,使价值最大,当然不会不超过背包容量



0 0
原创粉丝点击