一、01背包问题
1.1 01背包求最大价值
题目描述
题目链接:2. 01背包问题 - AcWing题库
思路
对于01背包,我们需要考虑2个方面,我们需要考虑选择哪些物品使价值最大,同时也要考虑体积问题。我们可以用一个二维数组来维护。对于f[i][j] ,i维持物品的种类,j维持空间的大小,值存的就是当前最大的价值。
对于集合f,我们要考虑条件,也就是题目上的限制,对于这个模板题,我们要从前i个物品中选,总体积小于j的。
对于属性,一般有max、min、数量……(即f存的值存要求的属性),这道题里找的最大值
状态计算本质就是集合的划分
代码
#include<iostream>
using namespace std;
const int N=1e4+10;
int n,m;
int v[N],w[N];
int f[N][N];
int main()
{
scanf("%d %d",&n,&m);//n种物品,背包总体积为m
for(int i=1;i<=n;i++)//输入n种物品,和每种的价值
scanf("%d %d",&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];//不选物品i
if(j>=v[i])//当体积大于v[i]时才有能装i的机会
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);//选物品i
}
printf("%d",f[n][m]);
return 0;
}
一维优化
我们先举个例子
由于我们计算f[i]层时,只用到的f[i-1]层的数据
所以对于
f[i][j]=f[i-1][j];
我们可以删除第i维,就变成
f[j]=f[j]
所以这一行可以直接删去.
对于j,我们从0~m遍历,此时循环里就一个if循环,
if(j>=v[i])//当体积大于v[i]时才有能装i的机会
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
我们在j大于v[i]时才进行操作,不如直接让j从v[i]枚举,这样我们就可以省去if循环,变成
for(int j=v[i];j<=m;j++)
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
此时循环里的语句也要删去i维,如果我们直接删去i维,即
f[j]=max(f[j],f[j-v[i]]+w[i])
由于j是从小到大枚举,且j-v[i]<j,所以我们在枚举到j时,j-v[i]在之前肯定算过了
为什么一维情况下枚举背包容量需要逆序?
在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]与f[i - 1][j]是独立的。
而优化到一维后,如果我们还是正序,则有f[较小体积] 更新到 f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。
例如,一维状态第i轮对体积为3的物品进行决策,则f[7]由f[4]更新而来,这里的f[4]正确应该是f[i - 1][4],
但从小到大枚举j这里的f[4]在第i轮计算却变成了f[i][4]。
当逆序枚举背包容量j时,我们求f[7]同样由f[4]更新,但由于是逆序,这里的f[4]还没有在第i轮计算,所以此时实际计算的f[4]仍然是f[i - 1][4]。
简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「使用过」,逆序则不会有这样的问题。所以对于j我们要从大到小遍历
代码
#include<iostream>
using namespace std;
const int N=1e4+10;
int n,m;
int v[N],w[N];
int f[N];
int main()
{
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++)//枚举物品i
for(int j=m;j>=v[i];j--)//从大到小枚举
f[j]=max(f[j],f[j-v[i]]+w[i]);
printf("%d",f[m]);
return 0;
}
1.2 01背包求方案数
题目描述
题目链接:278. 数字组合 - AcWing题库
思路
和上述求最大值的思路基本相同,
对于本题我们可以把每个 正整数 看作是一个 物品
正整数的值就是物品的 体积
我们方案选择的目标是最终 体积 恰好为 m 时的方案数
注意要初始化,这里需要初始化f[0][0]=1,即在0个物品,值恰好为0时的方案数为1
二维代码
#include<iostream>
using namespace std;
const int N=110;
int n,m,f[N][100005];
int v[N];
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&v[i]);
f[0][0]=1;//当可选数字为0,和为0时也是一种
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)//和恰好为j时
{
f[i][j]=f[i-1][j];//不选时
if(v[i]<=j)
f[i][j]+=f[i-1][j-v[i]];//选第i个时直接加上,选不选都是符合的方案
}
}
printf("%d",f[n][m]);
return 0;
}
一维代码
优化过程和01背包一样
#include<iostream>
using namespace std;
const int N=1e4+10;
int n,m,f[N],v[N];
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&v[i]);
f[0]=1;//当可选数字为0,和为0时也是一种
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
f[j]+=f[j-v[i]];
printf("%d",f[m]);
return 0;
}
1.3 01背包——二维费用
题目描述
题目链接
思路
每件物品只能 用一次 ,所以这是一个01背包模型
费用一共有两个,一个是 体积V,一个是 重量M,我们用2个维度来维护
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int n,m1,m2;//1是体积 2是重量
int f[N][N];//在前i种物品中,第一费用体积不超过j,第二费用重量不超过k的所有方案
int w[N],v1[N],v2[N];
int main()
{
scanf("%d %d %d",&n,&m1,&m2);
for(int i=1;i<=n;i++)
scanf("%d %d %d",&v1[i],&v2[i],&w[i]);
for(int i=1;i<=n;i++)
for(int j=m1;j>=v1[i];j--)//枚举体积
for(int k=m2;k>=v2[i];k--)//枚举重量
f[j][k]=max(f[j][k],f[j-v1[i]][k-v2[i]]+w[i]);
printf("%d",f[m1][m2]);
return 0;
}
二、完全背包问题
2.1 完全背包求最大价值
题目描述
题目链接:3. 完全背包问题 - AcWing题库
思路
与01背包稍有不同,这里每个物品我们都可以选任意个,
朴素版代码
#include<iostream>
using namespace std;
const int N=1e4+10;
int n,m;
int v[N],w[N];
int f[N][N];
int main()
{
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=0;j<=m;j++)
for(int k=0;k*v[i]<=j;k++)//体积不能大于j
f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
printf("%d",f[n][m]);
return 0;
}
优化过程
我们观察 不选物品i的状态f[i.j]和 选1个物品i的状态f[i.j-v],看看它们都代表上一维中哪部分的最大值
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , ....., f[i-1,j-k*v]+k*w )
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , ....., f[i-1,j-(k-1)*v]+(k-1)*w )
我们可以看出只有f[i][j]中的f[i-1][j]特殊,后面所有项,都可以通过f[i][j-v]+w实现。
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
所以不需要枚举数量k,优化后如下
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]>=0)
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
此时和01背包的代码基本相同
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]>=0)
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
只有1句不同
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题
那么可以像01背包那样优化:
for(int i=1;i<=n;i++)
for(int j=v[i];j<=m;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
f[j]=max(f[j],f[j-v[i]]+w[i]);
这里为什么从小到大,还是有些不太清楚。后续有新的理解会更新。个人当前的理解是:01背包用的是第i-1维状态的值,而完全背包用的是第i维的值,01背包中,从大到小枚举是怕i-1维的值被i维的覆盖了,而完全背包需要的就是第i维的值,所以需要从小到大。
优化后代码
#include<iostream>
using namespace std;
const int N=1e4+10;
int n,m;
int v[N],w[N];
int f[N];
int main()
{
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=v[i];j<=m;j++)//从小到大,与01背包问题不同
f[j]=max(f[j],f[j-v[i]]+w[i]);
printf("%d",f[m]);
return 0;
}
2.2 完全背包求方案数
题目描述
题目链接:1023. 买书 - AcWing题库
思路
每种书我们可以看成1种 物品
书的价格 就是物品的 费用
每种书的 数目任意(不超过总钱数)
由此可知是完全背包问题
注意初始化!f[0][0]=1,不买也是一种方案
因为是完全背包,也能按照完全背包的方法优化
代码
//朴素版
#include<iostream>
using namespace std;
const int N=1e3+10;
int n;
int f[10][N];
int v[5]={0,10,20,50,100};
int main()
{
scanf("%d",&n);
f[0][0]=1;
for(int i=1;i<=4;i++)
for(int j=0;j<=n;j++)//前i种物品不超过j元的所有方案数
for(int k=0;k*v[i]<=j;k++)//k个i
f[i][j]+=f[i-1][j-k*v[i]];
printf("%d",f[4][n]);
return 0;
}
*/
//优化后
#include<iostream>
using namespace std;
const int N=1e3+10;
int n;
int f[N];
int v[5]={0,10,20,50,100};
int main()
{
scanf("%d",&n);
f[0]=1;
for(int i=1;i<=4;i++)
for(int j=v[i];j<=n;j++)
f[j]+=f[j-v[i]];
printf("%d",f[n]);
return 0;
}
2.2 完全背包——二维费用
题目描述
思路
代码
三、多重背包问题
3.1 多重背包求最大价值
题目描述
思路
n种物品,每种我们最多选s个,总体积不超过m。与完全背包基本一样,就是限制了个数。
朴素版代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e4+10;//n*m
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=0;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-v[i]*k]+k*w[i]);
printf("%d",f[n][m]);
return 0;
}
优化过程
由于和完全背包很像,这里我们试试能不能像完全背包一样优化
观察f[i][j]和发f[i][j-v]:
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....f[i-1,j-s*v]+s*w)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....f[i-1,j-s*v]+(s-1)*w,f[i-1,j-(s+1)*v]+s*w)
我们发现由于多了一项f[i-1,j-(s+1)*v]+s*w不符合规律,不能像完全背包那样优化。
我们每次都需要遍历一遍物品i的数量,可以从这方面入手,可以用2进制来优化一下数量表示。
对于s个,我们可以把s硬拆,用2进制表示,
如s=1023,我们可以拆为1,2,4,8,16,……512
每个依次选或者不选,就可以组成0~1023中的每一个数,这是2^n -1类的数
再如一个普通的数s=200,我们可以拆成1,2,4,8,16,32,64,73,应该加起来刚好为s。
这样就可以转化为一个01背包问题,我们对拆开的数字依次选或者不选,也能组成每一个数,并不会有遗漏情况。
这样就把O(n)优化为O(logn)。
优化后代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e4+10;//n*m
int n,m,v[N],w[N],f[N];
int main()
{
scanf("%d %d",&n,&m);
int cnt=0;//cnt写外面就是为了让后面输入的v[i],w[i]不把前面的顶掉
for(int i=1;i<=n;i++)
{
int a,b,s;//体积a,价值b,个数s
scanf("%d %d %d",&a,&b,&s);//s个
int k=1;//记录个数
while(k<=s)//按照1 2 4 8等二进制表示方法将个数s拆分
{
cnt++;
v[cnt]=k*a;//k个物品的体积
w[cnt]=k*b;//k个物品的价值
s-=k;
k*=2;
}
if(s)//如果有剩余,另加一个表示
{
cnt++;
v[cnt]=s*a;
w[cnt]=s*b;
}
}
n=cnt;//现在的个数就是拆分后的个数,拆分完之后就是01背包问题了
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]);
printf("%d",f[m]);
return 0;
}
3.2 多重背包求方案数
题目描述
题目链接:P1077 [NOIP2012 普及组] 摆花 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路
n种花,每种花可以看成一种 物品
每盆花的体积为1
每种花最多选ai盆,有上限
可以看出是多重背包问题
记得初始化:f[0][0]=1
代码
#include<iostream>
using namespace std;
const int N=110;
const int mod=1e6+7;
typedef long long ll;
int n,m,s[N];//s代表个数上限
ll f[N][N];
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&s[i]);
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=min(s[i],j);k++)
f[i][j]=(f[i][j]+f[i-1][j-k])%mod;
printf("%lld",f[n][m]%mod);
return 0;
}
3.1 多重背包——二维费用
题目描述
思路
代码
四、分组背包问题
4.1 分组背包求最大值
题目描述
题目链接:9. 分组背包问题 - AcWing题库
思路
分组背包问题,是我们有一个体积为m的背包,给定了在n组物品中,我们在每组中至多选择一个(也可以不选),求最大价值。
我们只需要用二维数组分别记下体积和价值,选的过程和01背包差不多。只是多一维k表示我们选的第k个
朴素版代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int n,m,s[N];
int v[N][N],w[N][N];
int f[N][N];
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)//n组
{
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=0;j<=m;j++)
{
f[i][j]=f[i-1][j];//不选i
for(int k=0;k<=s[i];k++)//选第k个
if(j>=v[i][k])
f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
}
printf("%d",f[n][m]);
return 0;
}
优化过程
按照01背包逆向枚举体积来优化
优化后代码
#include<iostream>
using namespace std;
const int N=110;
int n,m;
int v[N][N],w[N][N],s[N];
int f[N];
int main()
{
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",f[m]);
return 0;
}