1.自然语言描述
① 01背包问题:有n个物品,每个物品有相应的体积vi和价值wi,对于一个物品,要么装入背包,要么不装入;有一个体积为V的背包,怎么装物品才能使背包中物品的价值最大?
② 完全背包问题:有n种物品,每种物品有相应的体积vi和价值wi,对于一种物品可以选任意多个装入背包(即每种物品的数量是无穷大的)有一个体积为V的背包,怎么装物品才能使背包中物品的价值最大?
③ 多重背包问题:有n种物品,每种物品有相应的体积vi和价值wi,第i种物品的数量是si个;有一个体积为V的背包,怎么装物品才能使背包中物品的价值最大?
④ 分组背包问题:有n组物品,每组物品中有si个物品,这si个物品中的每一个都有各自的体积v和价值w,每组物品里最多只能选一个;有一个体积为V的背包,怎么装物品才能使背包中物品的价值最大?
背包问题是学习DP的一个基本模型,以上四种问题之间各有差别,各自的DP策略也是不一样的。
yxc 老师将DP问题描述为:有限集合内求最优解的问题。yxc 老师总结了闫氏DP分析法,这个方法的核心在于从集合的角度思考问题;这是一个思考方式的模板,方法的具体讲解请点这儿!
用闫氏DP分析法来看上述的4个问题:
对于①:状态表示f(i,j)含义是对于前i个物品背包容量为j时价值的最大值,f的属性是最大值;f(i,j)状态的集合划分,分为选第i件物品和不选第i件物品两种状态;对于前一种情况,f(i,j)=f(i-1,j),对于后者,f(i,j)=f(i-1,j-vi)+wi;二者中取最大值f(i,j)=max( f(i-1,j) , f(i-1,j-vi)+wi );得到状态转移方程后,从f(1,0)开始迭代计算得到f(n,v)
对于②:状态表示f(i,j)含义是对于前i种物品背包容量为j时价值的最大值,f的属性是最大值;f(i,j)状态的集合划分,分为选第i种物品的0,1,2,…,k件,f(i,j)=max(f(i-1,j),f(i-1,j-vi)+wi,f(i-1,j-2vi)+2wi,…,f(i-1,j-kvi)+kwi),简化方程,因为f(i,j-vi)=max(f(i-1,j-vi),f(i-1,j-2vi)+wi,…,f(i-1,j-kvi)+(k-1)wi),将后者代入前者得到状态转移方程f(i,j)=max(f(i-1,j),f(i,j-vi)+wi);得到状态转移方程后,从f(1,0)开始迭代计算得到f(n,v)
对于③:状态表示f(i,j)含义是对于前i种物品背包容量为j时价值的最大值,f的属性是最大值;f(i,j)状态的集合划分,分为选第i种物品的0,1,2,…,si件;参考②的推理方法,得出状态转移方程f(i,j)=max(f(i-1,j),f(i-1,j-vi)+wi,f(i-1,j-2vi)+2wi,…,f(i-1,j-sivi)+siwi);得到状态转移方程后,从f(1,0)开始迭代计算得到f(n,v)
对于④:状态表示f(i,j)含义是对于前i组物品背包容量为j时价值的最大值,f的属性是最大值;f(i,j)状态的集合划分,分为选第i组物品的第k件,参考②的推理方法,得出状态转移方程f(i,j)=max(f(i-1,j),f(i-1,j-vk)+wk);得到状态转移方程后,从f(1,0)开始迭代计算得到f(n,v)
上述分析过程得到的状态转移方程要在实际程序中写成合理的形式。
2020.9.6更新
混合背包问题:将01背包、完全背包、多重背包混合在一起;可以通过倍增打包的方式将多重背包问题转化为01背包问题,而完全背包问题和01背包问题的状态转移方程在一维情况下只是枚举的顺序相反,而状态转移方程的代码完全相同,整个问题得到简化。
2.代码描述
题目:Acwing.2 01背包问题题目链接
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN=1010;
int n,m,f[MAXN];
int v[MAXN],w[MAXN];
int main(void)
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--){//为什么要逆序求f:因为要做到代码的等价替换,二维形式下是f[i][j]是从f[i-1][j]和f[i-1][j-vi]+wi中找最大值
//一维数组中只能保存上一轮(i-1)中的状态值;更新本轮(i)状态值需要用到上一轮中体积较小的状态值,所以只能逆序处理;
//如果正序,会导致上一轮体积较小的状态值被更新,方程意义就和二维形式下的不一样了,变成错误的、意义不明的状态值
if(j>=v[i])//采用一维写法,是二维写法代码的等价替换,其实际意义仍参考二维代码
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[m]<<endl;
return 0;
}
题目:Acwing.3 完全背包问题题目链接
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN=1010;
int n,m,f[MAXN][MAXN];
int v[MAXN],w[MAXN];
int main(void)
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++){//完全背包也可以转为一维形式,而且这里是正序,参考其二维形式也就不难理解为什么是正序了
f[i][j]=f[i-1][j];
if(j>=v[i])
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
cout<<f[n][m]<<endl;
return 0;
}
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN=1010;
int v[MAXN],w[MAXN],n,m,f[MAXN];
int main(void)
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++)
if(j>=v[i])//这是完全背包的一维优化形式,不难看出,问题的关键在于删去一维后,第i次循环中计算需要的到底是第i次的值(例如这里)还是第i-1次的值(例如01背包)若是前者,则j不需要逆序,若为后者,j需要逆序。
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[m]<<endl;
return 0;
}
题目:Acwing.4 多重背包问题I题目链接
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN=110;
int n,m,f[MAXN][MAXN];
int v[MAXN],w[MAXN],cnt[MAXN];
int main(void)
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i]>>cnt[i];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
for(int k=1;k<=cnt[i];k++)
if(j>=k*v[i])
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
cout<<f[n][m]<<endl;
return 0;
}
题目:Acwing.5 多重背包问题II题目链接
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN=13000,MAXM=2010;//二进制打包,利用倍增的思想,将多重背包问题转化为01背包问题
int n,m,f[MAXN];
int v[MAXN],w[MAXN];
int main(void)
{
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++){
int a,b,c;
cin>>a>>b>>c;
int k=1;
while(k<=c){//k=1,2,4,8,...,2^n (c<2^(n+1))
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
c-=k;
k*=2;
}
if(c>0){//不够2的整数次幂的剩余物品打成最后一个包
cnt++;
v[cnt]=a*c;
w[cnt]=b*c;
}
}
n=cnt;
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
if(j>=v[i])
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m]<<endl;
return 0;
}
题目:Acwing.9 分组背包问题题目链接
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN=110;
int n,m,f[MAXN][MAXN],v[MAXN][MAXN],w[MAXN][MAXN],cnt[MAXN];
int main(void)
{
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>cnt[i];
for(int j=1;j<=cnt[i];j++)
cin>>v[i][j]>>w[i][j];
}
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--){
f[i][j]=f[i-1][j];
for(int k=1;k<=cnt[i];k++)
if(j>=v[i][k])
f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
cout<<f[n][m];
return 0;
}
题目:Acwing.7 混合背包问题题目链接
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int MAXN=1010;
int n,m,f[MAXN];
struct Obj{
int k,v,w;
};
vector<Obj> things;
int main(void)
{
cin>>n>>m;
for(int i=0;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
if(c==-1)
things.push_back({-1,a,b});
else if(c==0)
things.push_back({0,a,b});
else{
for(int k=1;k<=c;k<<=1){//将多重背包问题通过倍增打包转化为01背包问题
c-=k;
things.push_back({-1,a*k,b*k});
}
if(c>0)
things.push_back({-1,a*c,b*c});
}
}
for(auto it:things){
if(it.k<0){//01背包
for(int j=m;j>=it.v;j--)
f[j]=max(f[j],f[j-it.v]+it.w);
}else{//完全背包
for(int j=it.v;j<=m;j++)
f[j]=max(f[j],f[j-it.v]+it.w);
}
}
cout<<f[m]<<endl;
return 0;
}