背包问题总结
背包问题总结
_ _ _ _ _ _ __ _ _ _ _ _ _ 复赛前的挣扎(望csp-s复赛 rp++)
这里整理了6种背包问题(虽说还有好几种,但是本人太菜了~~~~)
- 01背包
题目
有N 件物品和一个容量为V 的背包。放入第i 件物品花费的费用是w[ i ] ,得到的价值是v[ i ] ,求将哪些物品装入背包可使价值总和最大。
基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 用子问题定义状态:即f [ i ] [ j ] 表示前 i 件物品恰放入一个容量为 j 的背包可以获得的最大价值,则其状态转移方程便是:
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i])
解释一下: “将前 i 件物品放入容量为 j 的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只牵扯前 i − 1 件物品的问题。如果不放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入容量为 j 的背包中”,最大价值为 f [ i − 1 ] [ j ] ;如果放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入剩下的容量为j − w[ i ] 的背包中”,此时能获得的最大价值就是f [ i − 1 ] [ j − w[ i ] ] 再加上通过放入第 i 件物品获得的价值 w [ i ] 。
空间复杂度优化
以上方法的时间和空间复杂度均为Θ ( V ∗ N ) 其中时间复杂度已经不能再优化了,但空间复杂度却可以优化到Θ ( N )。 代码如下:
for(int i=1;i<=n;i++)
for(int j=V;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
你以为这就完了?注意看下面:
一个常数优化
前面的代码中有f o r ( j = V . . . w [ i ] ) ,还可以将这个循环的下限进行改进。 假设当前物品为i ,即使在后面的循环中i + 1... n 的所有物体都要取,该下限也能保证最优解f [ V ] 能被更新到,该方法就是避免了这种情况,相当于舍弃了一些无用的状态,减少循环次数,但相应也降低了数组的完整性。f [ j − w [ i ] ]中的j jj减去后面所有的w [ i ] 才能达到这个bound,所以bound以下的状态是对答案没有贡献的。代码可以改成:
for(int i=1;i<=n;i++){
int bound=max(V-sum{w[i+1]...w[n],w[i]);
}
for(int j=V;j>=bound;j--){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
对于求sum可以用前缀和,这对于V 比较大时是有用的。 具体代码:
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i],sum[i]=sum[i-1]+w[i];
for(int i=1;i<=n;i++){
int bound=max(w[i],V-(sum[n]-sum[i]));
for(int j=V;j>=bound;j--){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
- 完全背包
题目
有N 种物品和一个容量为V 的背包,每种物品都有无限件可用。第i 种物品的费用是w[ i ] ,价值是v[ i ] 。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0 件、取1 件、取2 件……等很多种。如果仍然按照解01背包时的思路,令f [ i ] [ j ] 表示前i 种物品恰放入一个容量为V 的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程:
f[i][j]=max(f[i-1][j-k*w[i]]+k*v[i]))∣0≤k∗w[i]<=j
但是其复杂度为Θ ( N ∗ Σ ( V / w[ i ] ) ) 是比较大的,所以我们可以将其转化成01背包求解,变为Θ(V∗N):
f[i][j]=max(f[i−1][j],f[i][j−c[i]]+w[i])
同理,将其滚掉一维后变为:
for(int i=1;i<=n;i++)
for(int j=w[i];j<=V;j++)
f[j]=max(f[j],f[j-w[i]]+v[i]);
细心的读者会发现,这个代码与01背包的代码只有 j 的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么01背包中要按照 j = V . . . 0 的逆序来循环。这是因为要保证第i 次循环中的状态 f [ i ] [ j ] 是由状态 f [ i − 1 ] [ j − w[ i ] ] 递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第 i 件物品”这件策略时,依据的是一个绝无已经选入第 i 件物品的子结果 f [ i − 1 ] [ j − w[ i ] ] 。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i 种物品”这种策略时,却正需要一个可能已选入第i 种物品的子结果 f [ i ] [ j − w[ i ] ] ,所以就可以并且必须采用 j = 0... V 的顺序循环。
若还未明白正序倒序问题可参考此篇博客~~~~
- 多重背包
题目
有 N 种物品和一个容量为 V的背包。第 i 种物品最多有 p [ i ] 件可用,每件费用是 c [ i ] ,价值是 w [ i ] 。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本算法
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第 i 种物品有 p [ i ] + 1 种策略:取0 件,取1 件……取 p [ i ] 件。令 f [ i ] [ j ] 表示前 i 种物品恰放入一个容量为 j 的背包的最大价值,则有状态转移方程:
f[i][j]=max(f[i-1][j-k*w[i]]+k*v[i])∣0≤k≤p[i]
实际代码实现中再加一重 0... p [ i ] 的循环即可,复杂度是Θ ( V ∗ ∑ p [ i ] ) ),如下:
for(int i=1;i<=n;i++)
for(int j=V;j>=w[i];j--)
for(int k=1;k<=p[i];k++)
f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
当然,这也可以转化为时间复杂度为Θ ( N ∗ l o g ( p ) ∗ V ) )的01背包。二分制拆分代码如下:
/*方法一*/
int p[N];
int prime3(int n,int V){
for(int i=1;i<=n;i++){
int num=min(p[i],V/w[i]);
for(int k=1;num>0;k<<=1){
if(k>num) k=num;
num-=k;
for(int j=V;j>=w[i]*k;j++){
f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
}
}
}
return f[V];
}
/*方法二*/
int numv[N],numw[N],cnt;
int prime4(int n,int V){
for(int i=1;i<=n;i++){
for(int j=1;j<=p[i];j<<=1){
numv[cnt]=j*v[i];
numw[++cnt]=j*w[i];
p[i]-=j;
}
if(p[i]>0){
numv[cnt]=p[i]*v[i];
numv[++cnt]=p[i]*w[i];
}
}
for(int i=1;i<=cnt;i++)
for(int j=V;j>=numw[i];j--)
f[j]=max(f[j],f[j-numw[i]]+numv[i]);
return f[V];
}
- 混合背包
问题
如果将前面三个背包混合起来,也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包),应该怎么求解呢?
代码如下:
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i]>>p[i];
if(p[i]==0)//完全背包
for(int j=w[i];j<=V;j++)
f[j]=max(f[j],f[j-w[i]]+v[i]);
else if(p[i]==1)//01背包
for(int j=V;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
else {//二分制拆分优化多重背包
int num=min(p[i],V/w[i]);
for(int k=1;num>0;k<<=1){
if(num>=k) num=k;
num-=k;
for(int j=V;j>=w[i]*k;j--)
f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
}
}
}
- 二维费用背包
问题
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设第 i 品所需的两种代价分别为 w[ i ] 和 g[ i ] 。两种代价可付出的最大值(两种背包容量)分别为 V和 M。物品的价值为 v[ i ] 。
算法
费用加了一维,只需状态也加一维即可。设f [ i ] [ j ] [ k ] 表示前i 件物品付出两种代价分别最大为j 和k 获得的最大价值。状态转移方程就是:
f[i][j][k]=max(f[i−1][j][k],f[i−1][j−w[i]][k-g[i]]+v[i])
滚掉一维后:
for(int i=1;i<=n;i++)
for(int j=T;j>=w[i];j--)
for(int k=V;k>=g[i];k--)
dp[j][k]=max(dp[j][k],dp[j-w[i]][k-g[i]]+v[i]);
这里要注意一点:有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取T件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1 ,可以付出的最大件数费用为T。换句话说,设f [ i ] [ j ]表示付出费用i 、最多选j 件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新。
- 分组背包
问题
有N 件物品和一个容量为V 的背包。第i ii件物品的费用是w[ i ] ,价值是v[ i ] 。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
算法
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f [ k ] [ j ] 表示前k kk组物品花费费用j 能取得的最大价值,则有:
f[k][j]=max(f[k-1][j],f[k-1][j-w[i]]+v[i]∣物品i⊆组k)
同样可用一维数组:
/*方法一*/
int a[1010][1010];
int prime7(){
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
for(int k=1;k<=j;k++)
f[j]=max(f[j],f[j-k]+a[i][k]);
return f[m];
}
/*方法二*/
int s;
int c[N];
inline void prime8(){
for(int i=1;i<=n;i++){
cin>>s;//物品组数
}
for(int j=1;j<=s;j++) cin>>c[j]>>w[j];
for(int j=V;j>0;j--)
for(int k=1;k<=s;k++)
if(j>=c[k])
f[j]=max(f[j],f[j-c[k]]+c[k]);
}
好了,到这里就结束了。。。
最后附上背包代码全家桶:
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
int f[N],w[N],v[N];
/*---------0-1背包---------*/
inline int prime1(int n,int V){
memset(f,0x3f3f3f,sizeof(f)); f[0]=0;//需要装满
memset(f,0,sizeof(f));//不需要装满
for(int i=1;i<=n;i++)
for(int j=V;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
return f[V];
}
/*---------完全背包---------*/
int prime2(int n,int V){
for(int i=1;i<=n;i++)
for(int j=w[i];j<=V;j++)
f[j]=max(f[j],f[j-w[i]]+v[i]);
return f[V];
}
/*----------多重背包二进制拆分--------*/
/*方法一*/
int p[N];
int prime3(int n,int V){
for(int i=1;i<=n;i++){
int num=min(p[i],V/w[i]);
for(int k=1;num>0;k<<=1){
if(k>num) k=num;
num-=k;
for(int j=V;j>=w[i]*k;j++){
f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
}
}
}
return f[V];
}
/*方法二*/
int numv[N],numw[N],cnt;
int prime4(int n,int V){
for(int i=1;i<=n;i++){
for(int j=1;j<=p[i];j<<=1){
numv[cnt]=j*v[i];
numw[++cnt]=j*w[i];
p[i]-=j;
}
if(p[i]>0){
numv[cnt]=p[i]*v[i];
numv[++cnt]=p[i]*w[i];
}
}
for(int i=1;i<=cnt;i++)
for(int j=V;j>=numw[i];j--)
f[j]=max(f[j],f[j-numw[i]]+numv[i]);
return f[V];
}
/*------------二维费用背包----------*/
int t[N],g[N],dp[1010][1010];
int n,m;
int prime6(int n,int V,int T){
for(int i=1;i<=n;i++)
for(int j=T;j>=w[i];j--)
for(int k=V;k>=g[i];k--)
dp[j][k]=max(dp[j][k],dp[j-w[i]][k-g[i]]+v[i]);
return dp[T][V];
}
/*------------分组背包-----------*/
/*方法一*/
int a[1010][1010];
int prime7(){
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
for(int k=1;k<=j;k++)
f[j]=max(f[j],f[j-k]+a[i][k]);
return f[m];
}
/*方法二*/
int s;
int c[N];
inline void prime8(){
for(int i=1;i<=n;i++){
cin>>s;//物品组数
}
for(int j=1;j<=s;j++) cin>>c[j]>>w[j];
for(int j=c[j];j>0;j--)
for(int k=1;k<=s;k++)
if(j>=c[k])
f[j]=max(f[j],f[j-c[k]]+c[k]);
}
完结撒花~~~~