第六章 背包问题——01背包

来源:互联网 发布:sql查全相同重复数据 编辑:程序博客网 时间:2024/04/29 09:23

本章主要讲述最简单的背包问题,从如何建立状态方程到如何根据状态方程来实现代码,再到如何优化数据结构,让我们对动态规划的建立与求解认识更加透彻
题目:
有N件物品和一个容量为V的背包。放入第i件物品的费用是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值和最大。
分析:
(一)建立状态方程
这是最基础的背包问题,直接说状态转移方程了,设dp[i][v]表示前i件物品放入容量为v的背包能获得的最大价值,每件物品可以选择放与不放,则有:
dp[i][v]=max{dp[i-1][v],dp[i-1][v-Ci]+W[i]}
稍微解释一下,当第i件物品不放时,则dp[i][v]=前i-1件物品恰好放入v容量的背包,即dp[i-1][v],当i件物品放时,那就说明前i-1件物品放的容量为v-Ci,这样才能正好将第i件物品放进去,即dp[i-1][v-Ci]+Wi
(二)根据状态方程实现代码
状态方程建好之后,下面就是如何将它用代码实现,要考虑的主要问题就是:在给dp[i][v]赋值前,dp[i-1][v]和dp[i-1][v-Ci]必须已经赋过值了,带着这个问题我们来看,对于这样一个转移方程,二维的,肯定需要至少两层遍历,需要考虑两个方面:
1、这两个二维变量是从前向后遍历还是从后向前遍历
2、这两个变量的先后遍历顺序

先看第一个方面:
看到方程里有两个变量,i与v,先看i,i的关系是靠i-1推过来的,所以i毫无疑问一定是从0开始到n遍历,再看v,v一定是比v-Ci大的,所以v也一定是从最小的值0开始到最大的值V,
然后再来看顺序,即是先遍历i还是先遍历v的问题,我们看到这个状态转移方程的i只与i-1有关,即i变量只变动了1,而此时v变量是从v-Ci到的v,中间变动了Ci,所以此问题毫无疑问,i在外层循环,v在里层循环

最后我们在做下稍微的调整,我们要确保i-1和v-Ci是合法的,所以v不能从0开始了,要从Ci开始,至于i-1,i可以从1开始遍历,用下标1来表示第一件物品,就无需对i-1处理了

根据上面分析,代码就能写出来了,实现代码如下:
#include <iostream>#include <algorithm>using namespace std;#define N 3//物品个数#define V 10//背包容量int dp[N+1][V+1];int C[N+1]={0,10,5,5};//Ci表示第i件物品的费用 (i从1开始)int W[N+1]={0,2,2,1};//W[i]表示第i件物品的价值int ZeroOnePack(){for(int i=1;i<=N;i++){for(int v=C[i];v<=V;v++)dp[i][v]=max(dp[i-1][v],dp[i-1][v-C[i]]+W[i]);}return dp[N][V];}int main(){memset(dp,0,sizeof(dp));//先对dp初始化cout<<ZeroOnePack();    return 0;}

(三)数据结构优化
接下来,我们要考虑如何对此算法进行优化,

我们先对空间复杂度进行优化
考虑到i只与i-1有关,所以我们可以用滚动数组的方法,将二维压缩到一维,那么我们要做的就是把i这一维去掉,即当前的i对应的dp[v]是由上一状态i-1对应的dp[v]和dp[v-Ci]得到,那么我们在对当前状态的dp[v]进行赋值时,必须得确保这个dp[v]在内层for循环执行完之前是不能再用到的,如果内层for循环是从Ci开始到V,那么对于dp[v]=max(dp[v],dp[v-Ci]),我们看到max里面的v一定是上一状态i-1对应的dp[v],因为这个dp[v]我们还没用到,(当然了,此时我们已经赋值过的v应是从Ci到v-1),但是对于v-C[i]就不行了,因为v-C[i]很可能就包含在Ci到v-1中,这些值已经变动过了,不再是i-1状态所对应的值了,所以为了保证v-Ci的状态还是之前的状态,我们必须得从后向前遍历,为什么?还是回到方程dp[v]=max(dp[v],dp[v-Ci]),如果是从后向前遍历,那么我们已经赋过值的dp[v]是(dp[V],dp[V-1]......dp[v+1]),那么dp[v]是没动过的,可以,对于dp[v-Ci],v-Ci<v,一定不在(v+1,V)中,所以成立
做了以上分析,就可以实现代码了
#include <iostream>#include <algorithm>using namespace std;#define N 3//物品个数#define V 10//背包容量int C[N+1]={0,10,5,5};//Ci表示第i件物品的费用 (i从1开始)int W[N+1]={0,2,2,1};//W[i]表示第i件物品的价值int dp[V+1];int ZeroOnePack(){for(int i=1;i<=N;i++){for(int v=V;v>=C[i];v--)dp[v]=max(dp[v],dp[v-C[i]]+W[i]);}return dp[V];}int main(){memset(dp,0,sizeof(dp));//先对dp初始化cout<<ZeroOnePack();    return 0;}

然后我们看时间复杂度,在时间复杂度方面,因为必定要访问到所有的i和所有的v,所以n^2复杂度在所难免,那么我们可不可以以减少些循环的次数?答案是肯定的
由于我们只需要求最终结果dp[V],对于dp[V-1]、dp[V-2]……我们是不需要知道的,但是上面的代码执行完,经过我们以上的分析,这些值是存在的,所以我们可以感觉到,是不是做了些额外的劳动呢?我们来从最终的结果dp[V]往前找找线索,还是这个公式,我们从最后的V向前看:
已知最后一步一定做的是这一步:dp[V]=max(dp[V],dp[V-C[N]]+W[N]),我们看出了dp[V]只与dp[V-C[N]]有关,而比容量V-C[N]小的值是不需要计算的,这也是为什么能求出dp[V-1]、dp[V-2]……d的原因,依次在往前推,在i=N-1时,此时的dp[V]=max(dp[V],dp[V-C[N-1]]+W[N-1]),c此时的V也只与V-C[N-1]有关,由于此前v需要遍历的最小值是V-C[N],这里在此基础之上又需知道V-C[N-1]的值,所以v需要的遍历的最小值也只要是V-(C[N]+C[N-1])就行了,…………规律看出来了吧,话不多说,贴代码:

#include <iostream>#include <algorithm>using namespace std;#define N 3//物品个数#define V 10//背包容量int C[N+1]={0,10,5,5}; //Ci表示第i件物品的费用 (i从1开始)int W[N+1]={0,2,2,1}; //W[i]表示第i件物品的价值int dp[V+1];int sum[N+1]={0};//辅助数组,用于求C[N]+C[N-1]……int ZeroOnePack(){//常数优化sum[N]=C[N];for(int i=N-1;i>=1;i--)sum[i]+=sum[i+1]+C[i];//endfor(int i=1;i<=N;i++){for(int v=V;v>=max(V-sum[i],C[i]);v--)dp[v]=max(dp[v],dp[v-C[i]]+W[i]);}return dp[V];}int main(){memset(dp,0,sizeof(dp));//先对dp初始化cout<<ZeroOnePack();return 0;}

(四)状态方程深入剖析
适才我们从最终的dp[V]向前讨论,怎么那么像递推的思想呢,由于只需求dp[V]的解,那么递归能否解决呢
还是从这个方程开始dp[v]=max(dp[v],dp[v-C[i]]+W[i]),max中前者指的是不取i获得的值,后者指的是取i获得的值,由此可以写递归代码了
#include <iostream>#include <algorithm>using namespace std;#define N 3//物品个数#define V 10//背包容量int C[N+1]={0,10,5,5};//Ci表示第i件物品的费用 (i从1开始)int W[N+1]={0,2,2,1};//W[i]表示第i件物品的价值int ZeroOnePack(int i,int v){if(i<=0||v<=0)return 0;//不选第i件物品int a=ZeroOnePack(i-1,v);//必须得保证剩下的容量v能够有C[i]的容量,才能选择第i件物品int b=v>=C[i]?ZeroOnePack(i-1,v-C[i])+W[i]:0;return max(a,b);}int main(){cout<<ZeroOnePack(N,V);    return 0;}

(五)变形问题
将题目略微改动一下,附加一个条件,即求恰好装满容量为v的背包所获得的最大值,那么我们该如何求解呢?
我们还是从状态方程开始,从最初的方程说吧,dp[i][v]=max(dp[i-1][v],dp[i-1][v-C[i]]+W[i]),这里的v就要指恰装满v的背包了,我们来看从上一状态i-1怎么转移到当前状态的,dp[i-1][v]和dp[i-1][v-C[i]]两个值,因为这里要求装满背包,我们无法保证dp[i-1][v]和dp[i-1][v-C[i]]是存在的,即前i-1件物品不一定装满容量v的背包或者是容量v-C[i]的背包,但是如果其中一个是能装满的,另一个不能装满,则一定是选那个能装满的,再看是取这两个值得max值,那么我们就有法了,将初态除dp[0][0]=0外,dp[0][1……V]=负无穷,即1……V的背包起始状态是无效态
#include <iostream>#include <algorithm>#include <vector>using namespace std;#define INF -0x7ffffff#define N 3//物品个数#define V 10//背包容量int C[N+1]={0,10,4,5};//Ci表示第i件物品的费用 (i从1开始)int W[N+1]={0,2,2,1};//W[i]表示第i件物品的价值vector<int> dp;int ZeroOnePack(){for(int i=1;i<=N;i++){for(int v=V;v>=C[i];v--)dp[v]=max(dp[v],dp[v-C[i]]+W[i]);}return dp[V];}int main(){dp.assign(V+1,INF);//dp初始化为负无穷dp[0]=0;cout<<ZeroOnePack();    return 0;}

递归代码实现也只要稍微改一改条件就行了
#include <iostream>  #include <algorithm>  using namespace std;    #define INF -0x7fffffff#define N 3//物品个数  #define V 10//背包容量    int C[N+1]={0,10,4,5};  //Ci表示第i件物品的费用   (i从1开始)  int W[N+1]={0,2,2,1};   //W[i]表示第i件物品的价值    int ZeroOnePack(int i,int v){  if(v==0)return 0;if(i==0)return INF;    //不选第i件物品      int a=ZeroOnePack(i-1,v);      //必须得保证剩下的容量v能够有C[i]的容量,才能选择第i件物品      int b=v>=C[i]?ZeroOnePack(i-1,v-C[i])+W[i]:INF;      return max(a,b);  }  int main()  {      cout<<ZeroOnePack(N,V);      return 0;  } 



王川
2014/02/18



1 0