1、0/1背包
将物品划分为两个集合:①从1~i中选不包含i且体积小于等于V的,即f(i-1,j)
②从1~i中选包含i且体积小于等于V的,即f(i-1,j-v[i])+w[i];f(i,j)表示从1~i中选且体积不超过j的物品的最大价值。
为什么需要逆序?
由转移方程f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]]),如果从小到大枚举的话,j递增,j-v[i]一定小于j,当更新本层时j-v[i]就会取到本层前面f[i][j],而不是上一层的。
#include<iostream>
using namespace std;
const int N=1e5+10;
const int M=1e3;
int v[N],w[N],dp[N][M];
int main()
{
int n,m;
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=1;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
if(v[i]<=j)
dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
}
}
//优化,可发现每次在更新i时我们只需要用到前面i-1的值,所以用一维数组来存储,即滚动数组的形式
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
{ //逆序过来,否则因为j-v[i]<=j的,所以当dp[j]=dp[j-v[i]]时,dp[j-v[i]]可能用到的是第i层的dp[i][j-v[i]],而实际要的是dp[i-1][j-v[i]]
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
cout<<dp[n][m]<<endl;//我们要的答案是从n件物品中选体积不大于m的最大价值,所以答案是dp[n][m]。
}
2、完全背包(每件物品可以取无限多个)
状态表示:f[i][j]从1~i中每件物品选无限多个且体积不超过V的最大价值。
状态划分:对每件物品可以取0,1,2...k,状态方程f[i][j]=f[i-1][j-k*v[i]]+k*w[i]。
#include<iostream>
using namespace std;
const int N=1010;
int v[N],w[N],f[N];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&w[i]);
for(int i=1;i<=n;i++)
{
for(int j=1;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]);
}
}
//优化①,f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i],f[i-1][j-2v[i]]+2w[i]]...,f[i-1][j-kv[i]]+kw[i])
// f[i][j-v[i]]=max( f[i-1][j-v[i]],f[i-1][j-2v[i]]+w[i],...,f[i-1][j-kv[i]]+(k-1)w[i])
//f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])
for(int i=1;i<=n;i++)
{
for(int j=1;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]);
}
}
//优化②,一维数组存储
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]);
}
}
printf("%d\n",f[m]);
return 0;
}
3、多重背包(有限多个物品)
(1)多重背包一(朴素法)
与完全背包的朴素算法一样的,只要在多判断一下物品的数量不要超过给定的数量即可。
#include<iostream>
using namespace std;
const int N=110;
int n,m,v[N],w[N],s[N];
int f[N][N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&v[i],&w[i],&s[i]);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
printf("%d\n",f[n][m]);
return 0;
}
(2)多重背包二(二进制优化)
思路:将每种物品进行分包,分为1,2,4,8...这样的每一份。为什么呢?
这样子分的话可以保证最后一定可以凑成原来总的数量,1,2它们可以凑成3;3,4又可以凑成7,1,4又可以凑成5,以此类推,最后这样划分一定可以凑成总的数量。划分完之后就成了0/1背包问题。
#include<iostream>
using namespace std;
const int N=1e6;//范围要注意!!!
int v[N],w[N],s[N];
int f[N],n,m,cnt;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int a,b,s;
scanf("%d%d%d",&a,&b,&s);
int k=1;
while(k<=s)//二进制拆分背包
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0)//没凑完整,剩下的继续凑
{
cnt++;
v[cnt]=s*a;
w[cnt]=s*b;
}
}
for(int i=1;i<=cnt;i++)//0/1背包模型
{
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
printf("%d\n",f[m]);
return 0;
}
(3)AcWing 6. 多重背包问题 III(0<N≤1000,0<V≤20000,0<vi,wi,si≤20000)单调队列优化
时间复杂度=1000*20000*log20000≈1e8
#include<iostream>
#include<cstring>
using namespace std;
const int N=20010;
int f[N],q[N],pre[N];
int n,m;
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
memcpy(pre,f,sizeof(f));
for(int j=0;j<v;j++)
{
int hh=0,tt=-1;
for(int k=j;k<=m;k+=v)
{
if(hh<=tt&&k-q[hh]>s*v)//k-q[hh]>s*v说明超出了物品i的最大数量,所以不合法
hh++;
while(hh<=tt&&pre[q[tt]]-(q[tt]-j)/v*w<pre[k]-(k-j)/v*w)
tt--;
q[++tt]=k;
f[k]=pre[q[hh]]+(k-q[hh])/v*w;
}
}
}
printf("%d\n",f[m]);
return 0;
}
4、分组背包问题(每一组最多选一个)
思路:与0/1背包类似的,它只是把一些背包打包放在一个组里,但是在每一组里要么选一个要么不选,所以就成了0/1背包问题。所以只需要再加一层循环对每一组里的背包来判断选或不选,与0/1背包一样的思路。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int s[N],v[N][N],w[N][N],f[N];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&s[i]);
for(int j=1;j<=s[i];j++)
scanf("%d%d",&v[i][j],&w[i][j]);
}
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
for(int k=0;k<=s[i];k++)
if(j>=v[i][k])
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
printf("%d\n",f[m]);
return 0;
}
5. 混合背包问题
①0/1背包,完全背包,多重背包混合问题。
通过之前的计算可以发现,①多重背包是可以转换为0/1背包的,所以这两类可以一起处理;
②完全背包它在计算的过程是不用逆序的,这是它与0/1背包表达式的区别,所以没办法一起处理,所以它自己要单独处理。
总之,①多重背包转换为0/1背包,之后按0/1背包的方式处理;
②完全背包就正常处理。
#include<iostream>
using namespace std;
const int N=1010,M=5010;
int f[N];
int n,m;
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
if(!s)//完全背包
{
for(int j=v;j<=m;j++)
f[j]=max(f[j],f[j-v]+w);
}
else
{
if(s==-1) s=1;//0/1背包,把s变为1就可以和多重背包一样的处理方式了
for(int k=1;k<=s;k*=2)
{
for(int j=m;j>=k*v;j--)
f[j]=max(f[j],f[j-k*v]+k*w);
s-=k;
}
if(s>0)//二进制分割后,还有剩下的
for(int j=m;j>=s*v;j--)
f[j]=max(f[j],f[j-s*v]+s*w);
}
}
printf("%d\n",f[m]);
return 0;
}
6.二维费用的背包问题(体积最多是j)
题意:有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。每件物品只能用一次。
体积是 vi,重量是 mi,价值是 wi。求解将哪些物品装入背包,可使物品总体积不超过背包容量,
总重量不超过背包可承受的最大重量,且价值总和最大。
思路:f[i][j][k]表示从前i件物品中选且总体积不超过V,总重量不超过M的最大价值。
f[i][j][k]=max(f[i-1][j][k],f[i-1][j-v][k-m]+v)。
#include<iostream>
using namespace std;
const int N=110;
int f[N][N];
int n,v,w;
int main()
{
scanf("%d%d%d",&n,&v,&w);
for(int i=0;i<n;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
for(int j=v;j>=a;j--)
for(int k=w;k>=b;k--)
f[j][k]=max(f[j][k],f[j-a][k-b]+c);
}
printf("%d\n",f[v][w]);
return 0;
}