背包问题
背包
问题:有n个物品,每个重量为vi,权值为wi,每个物品仅用一次,问在背包容量为W里能装的最大价值
01背包
核心特点:每件物品最多只能用一次
集合条件:
核心:不漏下任何一个
1.只从前i个物品中选
2.总体积\(\le\)V
属性:Max
\(f_{i,j}\)据上面的假设,意义应该是在前i个物品,当前已选的物品的最大价值
把\(f_{i,j}\)表示的所有选法分为两大类:
1.不选第i个物品
2.选第i个物品
计算:
\(f_{i,j}\)
如果是第1类的话显然
第2类的话我们考虑:
\(f_{i,j}\)应该可以从上一个位置推过来
那么上一个位置的背包容量应该就是 j-v[i]
同时由于第i位的东西必须选
所以说这个位置的转移方程应该是
综上:我们要的应该是两个集合的最大值 所以说:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e3+10;
int n,m;
int v[N],w[N],f[N][N];
int main(){
ios::sync_with_stdio(false);
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-1][j-v[i]]+w[i]);
}
}
cout<<f[n][m];
return 0;
}
此时我们还可以考虑到一个问题我们发现第i个物品一定是从上一个物品的某一个体积的背包里整过来的
那么我们就可以考虑空间的优化
\(f_{j}\)让它表示背包容量为j时的最大容积即可
那么我们看一下第二重循环
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-1][j-v[i]]+w[i]);
}
这个我们直观考虑应该写成这样
for(int j=0;j<=m;j++){
f[j]=[j];
if(j>=v[i]) f[j]=max(f[j],f[j-v[i]]+w[i]);
}
首先我们看f[j]=f[j]显然是一句废话,删掉就行
\(f[j]\)的转移看着也没太大毛病
等等,还有点问题
首先,由于我们枚举的是体积,那么问题应该在于当j<v[i]时,这个转移显然时废掉了(j-v[i]此时显然小于0)
所以我们可以从v[i]开始枚举
其次,我们考虑一下,我们的第i种状态是不是都是从i-1转移而来的
所以我们的此时是不是也应该做到我们每次取max的两个数也要是上一重循环的数,而不能是这次已经覆盖过的,因此,我们要从m枚举到v[i]
综上所述:
代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e4+10;
int n,m;
int v[N],w[N],f[N][N];
int main(){
ios::sync_with_stdio(false);
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>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m];
return 0;
}
完全背包
核心特点:每件物品可以装无限次
假设每一个物品背包装满时最多选k个
那么转移方程很显然
那么很容易得到朴素做法:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e3+10;
int v[N],w[N],f[N][N];
int n,m;
int main(){
ios::sync_with_stdio(false);
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++){
for(int k=0;k*v[i]<=j;k++){
f[i][j]=max(f[i][j],f[i-1][j - k * v[i]]+ k * w[i]);
}
}
}
cout<<f[n][m];
return 0;
}
但是这样是三重循环预计连1000 1000的数据都过不了,考虑优化
我们可以把右边玩意们列出来:
然后我们再写一个式子
观察相似之处,我们发现第一个的那个式子除了第一个以外,其余的显然都是等于\(f_{i,j-v[i]}+w[i]\)的
所以原式就可以轻易的化为
瞬间干掉了k所在的那个循环
而且我们借用上一个题的那个思想,空间方面i的这一层显然也可以干掉
最终式子
而且关于最后一步和上一个题又不太一样我们发现这一次的每一个转移都是从第i个物品转移到第i个物品,所以我们枚举体积可以直接顺序枚举,不需要倒着来
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e3+10;
int v[N],w[N],f[N];
int n,m;
int main(){
ios::sync_with_stdio(false);
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=v[i];j<=m;j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m];
return 0;
}
多重背包
核心特点:每件物品最多有Si个
朴素做法的话和刚才的朴素写法一样
我们先把式子写出来
核心优化思想:二进制优化
前置知识,一个数一定可以用\(2^n\)的数拼凑出来
原因:1,2 可以凑出来1~3
13分别加上4就是17
同理也可以加上8
证毕(其实很显然啊喂)
然后我们就可以将1,2,4,8,16,32,64,128,256,c……分别打包成一个区间
(\(1+2+4……2^k+c=s\),\(2^k<c<2^k+1\))
然后很显然我们就可以把它转化成01背包问题了
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
const int N=2e4+10;
int w[N],v[N],f[N];
int n,m,cnt;
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++){
int a,b,s,k=1;
cin>>a>>b>>s;
while(k<=s){
v[++cnt]=a*k;
w[cnt]=b*k;
s-=k; k*=2;
}
if(s) v[++cnt]=a*s,w[cnt]=b*s;
}
n=cnt;
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--) f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m];
return 0;
}
分组背包
核心特点:有若干组,每组里面最多选一个物品(假如说有个瓜摊,选了生瓜蛋子就不能选熟瓜了)
状态转移:
若选这一组的东西 \(f_{i,j}=f_{i-1,j-v[i][k]}+w[i][k]\)
若不选的话: \(f_{i,j}=f_{i-1,j}\)
求两个集合的最大值:取Max
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1010;
int n,m;
int v[N][N],s[N],w[N][N],f[N][N];
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
for(int j=1;j<=s[i];j++) cin>>v[i][j]>>w[i][j];
}
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<=s[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;
}