背包问题
一:01背包
题目:有一个容量为T的背包,现有n个物品,每个物品有都有一个体积w[ i ],和自身价值v[ i ],现在要求求出背包能够装的物品的价值最大。每个物品只可以装一次。
基本思路:01背包是背包中的最基础的问题,后面很多背包问题都是01背包和完全背包延伸出来的。01背包的特点是:每一个物品只可以放一次,可以选择放或者不放。我们用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 个物品的时候容量为 j 。他的价值就为f[ i-1 ][ j ]。当我们选择第 i 件物品的时候。那么这个决策的的前一个转态就是前 i-1 个物品此时的容量为 j 个容量减去第 i 个物品的容量,那么前 i 个物品的的最大价值为f[ i-1 ][ j-w[ i ] ]+v[ i ]。不理解的可以列一个表格来找到其中的规律。
核心代码:
#include<iostream>
using namespace std;
int v[101],w[101];
int f[1005][1005];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=m;i++)
for(int j=n;j>=0;j--)
if(j>=w[i])
f[i][j]=max(f[i-1][j-w[i]]+v[i],f[i-1][j]);
cout<<f[m][n];
return 0;
}
第一层循环表示第 i 个物品,第二层循环表示背包的容量从最大一直循环到最小,从T——0这样可以避免每个物品被多次装入背包的情况。
空间优化:01背包的时间我们已经不能再优化了,时间复杂度为O(T*n),但是空间可以优化。从上面的循环我们可以看出二维数组中的 i 表示第 i 个物品,但我们每次更新的是容量 j 和本次循环的所对应的最大价值,因此我们可以用一个一维数组来表示f[ j ]=max(f[ j ],f[ j-w[ i ] ]+v[ i ]。
for(int i=1;i<=n;i++)
for(int j=T;j>=0;j--)
if(j>=w[i])
f[j]=max(f[j],f[j-w[i]]+v[i])
在很多的背包问题中,我们可以发现问题有两种不同的问法,第一种是要求“背包装满时的价值最大”,第二种就是“背包可以不用装满”。对于这两种不同的问法,其实是f[]数组的初始化问题。
当题目要求我们将背包恰好装满是,我们只需要将f[0]=0,数组中的其他元素初始化为−∞。当题目不需要将背包装满,而只是求最大价值的时候,我们将f[]数组中的元素全都初始化为0。
典型例题:https://luogu.org/problem/P1048
二:完全背包
题目:有一个背包容量为T的背包,有n个物品,每个物品有对于的体积w[ i ],相应的价值v[ i ]。现要求求出背包可以装下的最大价值,每个物品可以装多次。
基本思路:完全背包类似与01背包,不同的是完全背包中的物品可以被多次转,那么就这不是简单的选与不选了,二十每个物品可以选0次,选一次,选两次。。。。。。
类似于01背包,我们同样要找出转态转移方程式,我们任然可以这样处理记f[ i ] [ j ]来表示背包容量为 j 时前 i 个物品所对应的价值,第 i 个物品不选,那么那么对应的价值为f[ i-1][ j ],
第 i 个选,对应的价值为f[ i ] [ j -w[ i ]]+v[ i ](注意这里的方程式与01背包方程式的不同)。那么对应的状态转移方程式为max(f[ i-1][ j ],f[ i ] [ j -w[ i ]]+v[ i ])。
转化为01背包解决:
在解决01背包的时候,我们的背包容量(即第二层循环)是从V-0的,而不是从0-V。事实上,背包容量从V-0的目的是保证f[ i ] [ j ]是 f[ i-1 ][ j-w[i]]递推来的,这样就可以保证每一个物品只是被装了一次。相反的,要是我们想一个物品多次被装的话,那么背包容量就要从0-V。那么我们就可以保证f[ i ][ j ] 是从f[ i ][ j-w[ i ]]递推来的,这也是为什么这里是 i ,而不是 i-1的原因。
可以列两个表来辅助理解。
核心代码:
#include<iostream>
using namespace std;
int f[100000][100000];
int n,m;
int w[10001],v[10001];
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=m;i++)
for(int j=w[i];j<=n;j++)
f[i][j]=max(f[i][j],f[i][j-w[i]]+v[i]);
cout<<f[m][n];
return 0;
}
同样的,这个二维数组也可以优化为一维数组。
for(int i=1;i<=n;i++)
for(int j=0;j<=V;j++)
if(j>=w[i])
f[j]=max(f[j],f[j-w[i]]+w[i]);
典型例题 :https://www.luogu.org/problem/P1616
三:多重背包
题目:有一个容量为T的背包,现有n个物品,每个物品有对应的价值v[ i ],对应的体积我w[ i ],并且每个物品有 s[ i ]个,即每个物品有有限个。现要求装这些物品使价值最大。
基本思路:我们可以看出在这个题目中,每一个物品可以装0次,1次,2次。。。。。。这种装法与完全背包的策略略微相同。差别在于每一个物品是有限个,只能去k次(k<=s[ i ]),而不是像完全背包那样可以无限次的取。那么我们的所考虑的就是每一个物品要在这有限的个数中取多少个才能使背包所装的价值最大。在这里我们同样可以转化为01背包来解决。我们将每种物品看做01中的每一个物品。然后再在每种物品中去判断取0次,1次,2次。。。。。。。哪一种决策可以使价值最大。那么转态转移方程式就可以这样来写:
f[ i ][ j ]=max(f[ i-1 ][ j ],f[ i-1 ][j-k*w[ i ]]+k*v[ i ]);同样这个方程式与01背包的方程式类似,只是我们判断了取k次的结果与前一个转态的相比较。要判断每种物品要装多少次才能使背包所装价值最大,只需要加一个循环,k从0—s[ i ]。
代码:
#include<iostream> using namespace std; int N,V; int w[200],v[200],s[200]; int f[200][200]; int main() { cin>>N>>V; for(int i=1;i<=N;i++) cin>>w[i]>>v[i]>>s[i]; for(int i=1;i<=N;i++) for(int j=V;j>=0;j--) for(int k=0;k<=s[i];k++) if(j>=k*w[i]) f[i][j]=max(f[i-1][j],f[i-1][j-(k*w[i])]+k*v[i]); cout<<f[V]; return 0; }
同样,这个二维数组也可以优化为一维数组。
for(int i=1;i<=N;i++) for(int j=V;j>=0;j--) { for(int k=0;k<=s[i];k++) { if(j>=k*w[i]) f[j]=max(f[j],f[j-(k*w[i])]+k*v[i]); } }
典型例题:https://www.acwing.com/problem/content/4/
四:混合背包
题目:混合背包就是01背包,完全背包,多重背包的混合。现有一个容量为V的背包,有n个物品,其中有一部分物品只可以取一次,一部分物品可以取无限次,还有一部分物品只能取有限次,现要求求背包可以装的最大价值。
基本思路:几种背包混合,我们可以拆分开来就可以了,拆分成01背包,完全背包,多重背包,然后分开来计算就可以了。为了方便起见在这里用s[ i ]=1表示物品只取一次,s[ i ]=0表示物品可以无限次的取,是s[ i ]是一个非零的整数表示物品可以取有限次。利用这样的方法那么背包就被拆分成了其他三种背包了,在计算的时候,就只需要判断s[ i ]的值了。如果s[ i ]=0,即完全背包,第二层循环从0-v,如果s[ i ]!=0,即01背包和多重背包,由于这两种背包第二层循环都是从V-0,所以这两种背包可以放在一起处理。
代码:
#include<iostream> using namespace std; int w[100],v[100],s[100]; int f[1001]; int main() { int N,V; cin>>V>>N; for(int i=1;i<=N;i++) cin>>w[i]>>v[i]>>s[i]; for(int i=1;i<=N;i++) { if(s[i]!=0)//01背包和多重背包 for(int j=V;j>=0;j--) { for(int k=0;k<=s[i];k++) if(j>=k*w[i]) f[j]=max(f[j],f[j-k*w[i]]+k*v[i]); } else if(s[i]==0)//完全背包 for(int j=w[i];j<=V;j++) f[j]=max(f[j],f[j-w[i]]+v[i]); } cout<<f[V]; return 0; }
典型例题:https://www.acwing.com/problem/content/7/
五:二维费用背包
题目:有一个容量为V的背包,并且背包能够有一个最大的承载重量M,现在有n个物品,每一个物品有对应的体积w[ i ],重量m[ i ],价值v[ i ],每个物品只能够装一次,现在要求背包能够装下的最大价值。
基本思路:二维背包与01背包类似,但是加了背包的限定条件,与前面对比,我们可以用二维数组来表示一维背包所能够装的价值,同样的,二维背包在一维背包的基础上加了一维,那么我们也就可以用三维数组来表示二维背包,f[ i ] [ j ] [ k ],其中 i 表示第 i 个物品,j 表示体积,k 用来表示重量。同理于01背包,用于每个物品只可以取一次,那么在循环的时候体积从V-0,重量从M-0。那么二维背包的状态转移方程式就可以写成这样:f[ i ] [ j ] [ k ]=max(f[ i-1 ] [ j ] [ k ],f [ i-1 ] [ j-w[ i ]] [ k-m[ i ]]+v[ i ])。类似于01背包,三维数组同样可以优化为二维数组
f [ j ] [ k ]=max(f [ j ] [ k ],f [ j-w[ i ]] [ k-m[ i ]]+v[ i ])。
代码:
#include<iostream>
using namespace std;
int w[1001],m[1001],v[1001];
int f[1001][1001];
int main()
{
int N,V,M;
cin>>N>>V>>M;
for(int i=1;i<=N;i++)
cin>>w[i]>>m[i]>>v[i];
for(int i=1;i<=N;i++)
for(int j=V;j>=0;j--)
for(int k=M;k>=0;k--)
{
if(j>=w[i]&&k>=m[i])
f[j][k]=max(f[j][k],f[j-w[i]][k-m[i]]+v[i]);
}
cout<<f[V][M];
return 0;
}
二维背包与其他背包混合:前面提到的是为二维01背包,二维背包还可以与其他背包混合在一起,如每个物品可以取无限次,那么就是二维完全背包,每个物品只能取有限次,那么就是二维多重背包。。。。。。,可以像一维背包那样改变循环来处理。不管是哪一种背包类类型,只要将一维背包中的几种背包弄懂了,二维背包也就不是问题。
典型例题:https://www.acwing.com/problem/content/8/
六:分组背包
题目:有一个背包容量为V的背包,有n个物品,第 i 个物品的体积为w[ i ],价值为v[ i ],现在将些物品分为k组。问在每组中最多取一件物品的情况下,背包能够装的最大价值是多少。
基本思路:现在将这些物品分为了k组,那我们首先想到的就是在加一层循环,用来遍历每一组,再在每一组来遍历每一个物品,来决策装哪一个物品才能使价值最大。那么第一层循环就可以从1——k;由于每一组中的物品只可以取一件,类比01背包,那么第二层循环就可以从V——0,;第三层循环用来遍历每一组中的每一个物品。那么分组背包的状态转移方程式就可以这样来写:f[ j ]=max(f[ j ],f[ j-w[ i ]]+v[ i ]。
代码:
#include<iostream>
using namespace std;
int f[1001],k_w[1001][1001],k_v[1001][1001];//K_w//表示第k组的第i个物品的体积,k_v同理
int main()
{
int K,V;
cin>>K>>V;
for(int i=1;i<=K;i++)
{
int n;
cin>>n;
k_w[i][0]=n;//k_w[0]用来表示每组有多少个物品
for(int j=1;j<=n;j++)
cin>>k_w[i][j]>>k_v[i][j];
}
for(int i=1;i<=K;i++)//第一层循环,遍历每一组
for(int j=V;j>=0;j--)//第二层循环,与01背包功能一样
for(int k=1;k<=k_w[i][0];k++)//第三层循环,遍历每一组中的每一个物品
if(j>=k_w[i][k])
f[j]=max(f[j],f[j-k_w[i][k]]+k_v[i][k]);
cout<<f[V];
return 0;
}
典型例题:https://www.acwing.com/problem/content/9/
七:有依赖的背包
题目:这种背包题目与前面的背包问题类似,但是现在在原来的背包问题之上加了一个条件:每一个物品(主件)现在有一些附件,现在的规则就是,你要买附件,就必须买主件,也可以只买主件。要求求背包可以装下的最大价值。
基本思路:对于第 k 个物品组中的 物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,可以对主件 k 的 “附件集合”先进行一次 01 背包,得到费用依次为 0...V −Ck 所有这些值时相应的最 大价值 Fk[0...V −Ck]。那么,这个主件及它的附件集合相当于 V −Ck + 1 个物品的 物品组,其中费用为 v 的物品的价值为 Fk[v−Ck]+Wk,v 的取值范围是 Ck ≤ v ≤ V 。 也就是说,原来指数级的策略中,有很多策略都是冗余的,通过一次 01 背包后, 将主件 k 及其附件转化为 V −Ck + 1 个物品的物品组,就可以直接应用分组背包的算法就可以了。