#完全背包问题 一个背包总容量为V,现在有N种物品,第i种物品体积为w[i],价值为v[i],每个物品都有无限多件,现在往背包里面装东西,怎么装能使背包的内物品价值最大?
【分析】 完全背包问题和之前的硬币问题如出一辙,都是DAG上的动态规划问题,只需要对硬币问题的代码稍微修改,即可解决完全背包问题。
状态定义:d(i)表示从i状态到小于i的其他所有状态的最长路(价值最大)状态转移:d(i)=min{d(i-w[j])+v[j] | i>=w[j]}值得注意的是d的初始化是0,而不是-INF,如果是-INF,这个是关键,因为d(i)表示的是从i状态到小于等于i的所有状态的最长路,相当于从所有小于i的状态进行递推。
#include<iostream> using namespace std; const int N=5;//物品数目 const int V=10;//背包容量 int main() { int w[10]={-1,3,3,3,3,3}; int v[10]={-1,1,1,1,1,1}; int dp[15]={0}; for(int i=1;i<=N;i++) for(int j=1;j<=V;j++) if(w[i]<=j) dp[j]=max(dp[j],dp[j-w[i]]+v[i]); cout<<dp[V]<<endl; return 0; }【例题 Piggy-Bank HDU - 1114】 裸露的完全背包问题,和上面说的不同的是,这个也是必须从状态V到状态0。
#include<iostream> using namespace std; const int maxn = 600; const int INF = (1<<30); int T; int E,F,S; int n; int v[maxn],w[maxn]; int dp[10005]; int main() { cin>>T; while(T--){ cin>>E>>F; S=F-E; cin>>n; for(int i=1;i<=n;i++){ cin>>v[i]>>w[i]; } for(int i=1;i<=S;i++){ dp[i]=INF; } dp[0]=0; for(int i=1;i<=S;i++){ for(int j=1;j<=n;j++){ if(i>=w[j]){ dp[i]=min(dp[i],dp[i-w[j]]+v[j]); } } } if(dp[S]==INF){ cout<<"This is impossible."<<endl; }else{ cout<<"The minimum amount of money in the piggy-bank is "<<dp[S]<<"."<<endl; } } return 0; }#0-1背包问题 有编号分别为a,b,c的五件物品,它们的重量分别是1,2,3它们的价值分别是11,10,12。现在给你个承重为5的背包,如何让背包里装入的物品具有最大的价值总和?
【问题分析与解决方案】
1)与完全背包对比。0-1背包问题中,(与完全背包相比)每种物品只有一个,无法采用DAG动态规划。 2)与贪心法对比。初看0-1背包问题,笔者首先想到的是贪心策略,但是细细分析一下,方知贪心策略不能解决0-1背包问题。现在就根据原题目说明,如果根据贪心法来做,每次应该选择(价值/重量)最大的物品,那么将选择重量为1,2的物品,得到的价值之和是21。而正确的解是选择重量为1,3的物品,得到价值之和是23。
为什么不能用贪心呢? 因为0-1背包问题的关键就是在于“0-1”,就是说每件物品不可分割,要么选,要么不选。如果把题目稍微改一下,物品可以分割,即可以选择物品的一部分,原题目就可以用贪心策略了,按照价值/重量的先后次序依次选择物品,会选择重量为1,2的,然后再选择重量为3的,由于背包承重只有5,那么会从重量为3的物品中截取重量2(对应价值是8)进行选取,这样最终得到价值和是29。3)0-1背包动态规划方案。
状态:d(i,j)表示在承重量为j的背包中装前i个物品得到的最大总价值状态转移:d(i,j)=max{d(i-1,j), d(i-1,j-w[i])+v[i]},j>=w[i](状态转移是根据第i个物品是否装入写出来的)还需要注意的是,如果j<w[i],即第i个物品的重量比重量为j的背包承重量大,第i个物品不能放,那么显然有d(i,j)=d(i-1,j)边界处理,d(i,j)都初始化为0即可,因为题目没有要求达到背包的最大承重量,0可以表示一个合法的最小值,即承重量为任何值的背包装入的最小价值也是0。如果题目要求必须达到背包的最大承重量,那么不能初始化为0,因为根据题目给的物品,未必能够达到背包的最大承重量,即d(i,j)的值可能无法取到(不是0),下面第4点将展示下这种情况下的代码 #include<iostream> using namespace std; const int N=3;//物品数目 const int V=5;//背包承重量 int main() { int w[4]={-1,1,2,3}; int v[4]={-1,11,10,12}; int dp[10][10]={0}; for(int i=1;i<=N;i++){ for(int j=1;j<=V;j++){ if(j<w[i]){ dp[i][j]=dp[i-1][j]; }else{ dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); } } } cout<<dp[N][V]<<endl; return 0; }可以打印输出,看每次内层for循环结束后对应的第i行的更新结果,得到如下表:
下面笔者把if-else语句合并,可以得到以下代码:
#include<iostream> using namespace std; const int N=3;//物品数目 const int V=5;//背包承重量 int main() { int w[4]={-1,1,2,3}; int v[4]={-1,11,10,12}; int dp[10][10]={0}; for(int i=1;i<=N;i++){ for(int j=1;j<=V;j++){ dp[i][j]=dp[i-1][j]; if(j>w[i]){ dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]]+v[i]); } } } cout<<dp[N][V]<<endl; return 0; } 空间复杂度优化(付出的代价是一次次覆盖掉上一行的结果,中间过程无法保留):分析上面代码及运行结果表,我们不难发现,结果表是一行一行的进行更新的,本行每个元素的更新仅仅受上一行且小于本元素所在列的元素和本行对应的物品的影响,考虑到这一点,我们能不能仅仅用一行来代替所有行呢?答案是可以的。我们完全可以用一个一维数组来代替二维数组,每更新一行,不再多用存储空间,而是覆盖掉原来的值。注意j要从V到1枚举,因为这样对于每一个i来说,利用i-1行的数据来更新当前的dp[j],再更新比j小的对应的dp,由于dp[j]不受比j小的对应的dp的影响,这样可以保证每次元素的更新得到的都是当前状态下的最优解。如下代码:
#include<iostream> #include<cstring> using namespace std; const int N=3;//物品数目 const int V=5;//背包承重量 int dp[V+10]; int main() { int w[4]={-1,1,2,3}; int v[4]={-1,11,10,12}; memset(dp,0,sizeof(dp)); for(int i=1;i<=N;i++){ for(int j=V;j>=1;j--){ if(j>=w[i]){ dp[j]=max(dp[j],dp[j-w[i]]+v[i]); } } } cout<<dp[V]<<endl; return 0; }4)改动题目为恰好装满背包(达到背包的最大承重量)
此时的状态d(i,j)表示用前i个物品装满容量为j的背包获得的最大价值先考虑下上面求解不必装满的情况,dp都初始化为0,这时因为总价值至少也可以达到0,但是改变条件后,必须要装满背包,可能存在无解的情况,dp就不能初始化为0了,那就初始化一个值,用来表示无解,由于最终求解的是最大价值,我们不妨初始化为一个极小值-INF。递推求解策略,就必须要先初始化一批值,用来作为递推的基石,这很类似于数学归纳法中的第一步,那这里我们要初始化的基石是哪些呢?这要结合后状态转移方程和状态定义的实际意义来,具体直接上代码。 #include<iostream> using namespace std; const int INF=(1<<30); const int N=3;//物品数目 const int V=5;//背包承重量 int dp[10][10]; int main() { int w[4]={-1,1,2,3}; int v[4]={-1,11,10,12}; for(int i=0;i<=N;i++) dp[i][0]=0; for(int j=1;j<=V;j++) dp[0][j]=-INF; for(int i=1;i<=N;i++){ for(int j=1;j<=V;j++){ if(j<w[i]){ dp[i][j]=dp[i-1][j]; }else{ dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); } } } cout<<dp[N][V]<<endl; return 0; }打印下动态规划中的那张表看看如下: #多重背包 一个背包,承重量为V。现有n种物品,第i种物品有num[i]个,第i种物品的价值是v[i],重量是w[i],问这个背包能容纳的最大价值是多少? 【分析】 研究过0-1背包后,我们会发现解决多重背包问题的一个明显的思路,就是把多重背包里面的某一种物品的num[i]个物品看成是i种物品,这样问题就是0-1背包问题了,自然顺利解决。直接上代码如下:
#include<iostream> #include<cstring> using namespace std; const int N=3; const int V=8; int main() { int w[N+100]={-1,1,2,2}; int v[N+100]={-1,6,10,20}; int num[N+100]={-1,10,5,2}; int dp[N+100][V+10]; int n=N;//转换成0-1背包后,n将是物品种类数,也是物品个数 for(int i=1;i<=N;i++){ int cnt=num[i]; while(cnt>=2){ n++; w[n]=w[i]; v[n]=v[i]; cnt--; } } memset(dp,0,sizeof(dp)); for(int i=1;i<=n;i++){ for(int j=1;j<=V;j++){ if(j<w[i]){ dp[i][j]=dp[i-1][j]; }else{ dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); } } } cout<<dp[n][V]<<endl; return 0; }