文章目录
背包九讲
博客介绍
我所写例题均来自**https://www.acwing.com/problem/**,如果有疑问也可去这个网站听yxc讲解课。
一、 01背包
问题描述:
有N件物品,容量是V的背包,每件物品只能使用一次,根据给出物品的体积与价值,求出使总体积不超过背包容量的最大价值
求解思路:
- f[i] [j]为在前i个物品中,背包容量为j时的最优解。因此,我们可以将f[0] [0]赋值为0.
- 分析每件物品时,我们都有两种决策方案:选与不选
- 当此时背包的容量不可以容纳这件物品时,我们不可以选择此物品,则价值等于有i-个物品时,f[i] [j] = f[i-1] [j]
- 当满足此时背包的容量可以容纳这件物品,即 j < v[i].那么此时有两种决策方案:选与不选
- 如果不选,则和第3点一样,如果选,背包容量由 j —>j-v[i] ,此时价值为则f[i] [j]=f[i-v[i]] [j] +w[i]
- 我们是否选择要根据题意所求判断,我们需要最大价值,因此f[i] [j]应取两者最大值
二维代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v[1005],w[1005];
int f[1005][1005];
int main()
{
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++)
{
if(v[i]>j)//不选该物品
f[i][j]=f[i-1][j];
else//是否选该物品,
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
}
cout<<f[n][m]<<endl;
return 0;
优化
f[j] 为N件物品,背包容量为j时的最优解。我们从代码中可以发现f[i] [j]的状态与前一个状态有关,我们简化为一维,可以直接去掉一维,我们可以发现这样是错误的。
为什么呢?因为二维时f[i] [j]的状态由f[i-1] [j]得到的,当优化为一维时,只剩一个变量,我们无法去保证用的是i-1轮的状态。在循环时,我们可能会使需要用的上一轮状态被改变,即当前物品被选多次。
如何解决这个问题呢?我们可以采取逆序,当前物品不会影响下一个容量。
一维代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005];
int f[1005];
int main()
{
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]<<endl;
return 0;
}
二、完全背包
简单介绍
这道题与01背包的区别在于:完全背包里的物品可以无限次使用
思路
我们在选择物品时则转化为了
f[i][j] =max(f[i][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,...f[i-1][j-k*v]+k*w);
最初我想添加一层循环来得到f[i] [j]的最大值,听了yxc的课发现没必要
f[i][j] =max(f[i][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,...f[i-1][j-k*v]+k*w);
f[i][j-v]=max( ,f[i-1][j-v]+w,f[i-1][j-2v]+2w,...f[i-1][j-k*v]+k*w);
得到递推关系
f[i][j]=max[f[i][j],f[i][j-v[i]]+w[i]];
这与01背包类似,不同点在于01背包用的是i-1层的状态,而完全背包则使用的是i层的状态,也正因如此,当优化为一维时可以正序,无需倒序,以下是优化前后的代码
二维代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005];
int f[1005][1005];
int main()
{
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++)
{
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>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005];
int f[1005];
int main()
{
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]<<endl;
return 0;
}
三、多重背包
简单介绍:
这个题与01背包的区别在于每件物品有固定的数量
思路:
我们思路与完全背包类似,在选取物品时我们需要多加一层循环
f[i][j] =max(f[i][j],f[i-1][j-k*v]+k*w);
时间复杂度O(n * v * s)
二维代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005],s[1005];
int f[1005][1005];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>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++)
{
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;
}
一维代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 50000
using namespace std;
int n,m;
int v[1005],w[1005],s[1005];
int f[1005];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>v[i]>>w[i]>>s[i];
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
{
for(int k=0;k<=s[i];k++)
{
if(j>=k*v[i])
{
f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
}
}
}
}
cout<<f[m]<<endl;
return 0;
}
优化1(二进制优化):
-
我们对于这个问题需要考虑每组商品需要选几个,我们可以使用二进制进行优化。
-
例如8,我们可以使用1,2,4,8组合出0-8所有的情况,但是他也会组合出超出8的情况,因此我们应进行限制。
-
我们将所有组合出来的体积价值放入容器中,此时问题又转化为01背包,我们进行求解即可
优化代码1
#include<bits/stdc++.h>
using namespace std;
#define N 2010
int f[N];
struct Good
{
int v,w;
};
vector<Good>good;
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int v,w,s;
cin>>v>>w>>s;
for(int k=1;k<=s;k*=2)
{
s-=k;
good.push_back({k*v,w*k});
}
if(s>0)
good.push_back({s*v,s*w});
}
for(auto t:good)
{
for(int j=m;j>=t.v;j--)
{
f[j]=max(f[j],f[j-t.v]+t.w);
}
}
cout<<f[m]<<endl;
return 0;
}
优化2(单调队列优化):
-
我们可以根据余数来优化,把余数相同的归为一类,余数不同无法相互干涉
dp[0], dp[v], dp[2*v],...dp[k*v] dp[1], dp[v+1], dp[2*v+1],...dp[k*v+1] dp[2], dp[v+2], dp[2*v+2],...dp[k*v+2] ... dp[j], dp[v+j], dp[2*v+j],...dp[k*v+j]
-
因此我们需要得到(dp[j], dp[v+j], dp[2 * v+j], … dp[k * v+j])中的最大值。我们可以将其通过单调队列来求取最大值
-
dp[j] = dp[j] dp[j+v] = max(dp[j] + w, dp[j+v]) dp[j+2v] = max(dp[j] + 2w, dp[j+v] + w, dp[j+2v]) dp[j+3v] = max(dp[j] + 3w, dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
由于每次dp[j]所加数都会改变,因此我们可以做修改:
dp[j] = dp[j] dp[j+v] = max(dp[j], dp[j+v] - w) + w dp[j+2v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w) + 2w dp[j+3v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w, dp[j+3v] - 3w) + 3w
此时每次入队列的都为
dp[j+k*v]-k*w
-
单调队列问题中 维持单调队列长度与单调性
优化代码2
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 20010
int f[N],q[N],g[N];
signed main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int v,w,s;
cin>>v>>w>>s;
memcpy(g,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&&q[hh]<k-s*v) //维持单调队列长度
hh++;
if(hh<=tt)
f[k]=max(f[k],g[q[hh]]+(k-q[hh])/v*w);
while(hh<=tt&&g[q[tt]]-(q[tt]-j)/v*w<=g[k]-(k-j)/v*w)//维持单调性
tt--;
q[++tt]=k;
}
}
}
cout<<f[m]<<endl;
return 0;
}
四、混合背包
简单介绍:
混合背包,顾名思义,将多种类型混合在一起。每次输入的s来区别它的类型,-1为01背包,0为完全背包,>0则为多重背包。
思路:
-
多重背包我们经过二进制优化,放进容器中可以转化为01背包,此时背包里只剩01背包与完全背包
-
在一维中01背包和完全背包的区别在于状态转移方程一个倒序,一个正序,因此我们需要分开写。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 20010;
int f[N];
struct Good
{
int kind;
int v,w;
};
vector<Good>good;
int main() {
int n,m;
cin >> n >> m;
for (int i = 0; i < n; ++i) {
int v, w, s;
cin >> v >> w >> s;
if(s==-1)
good.push_back({-1,v,w});
else if(s==0)
good.push_back({0,v,w});
else
{
for(int k=1;k<=s;k*=2)
{
s-=k;
good.push_back({-1,v*k,w*k});
}
if(s>0) good.push_back({-1,v*s,w*s});
}
}
for(auto t:good)
{
if(t.kind==-1)
{
for(int i=m;i>=t.v;i--)
{
f[i]=max(f[i],f[i-t.v]+t.w);
}
}
else
{
for(int i=t.v;i<=m;i++)
f[i]=max(f[i],f[i-t.v]+t.w);
}
}
cout << f[m] << endl;
return 0;
}
五、二维背包的费用问题
简单介绍:
与01背包类似,不过问题转化为二维了。在01背包中限制条件只有体积,而在这个题中又加了重量这一限制条件。
思路:
01背包状态转移方程为
f[i][j]=max(f[i-1][j],f[i-1][ j-v[i] ]+w);
f[i][j]表示在前i个物品中,当背容量为j时的最优解
多了重量这一限制条件,那么我们就多加一维:
f[i][j][k]=max(f[i-1][j][k],f[i-1][ j-v[i] ][ k-m[i] ]+w[i]);
f[i][j][k]表示在前i个物品中,当背包容量为j,可承受重量为k时的最优解
我们依然可以类似01背包操作,将其压缩为二维,当优化为二维时,不包含变量i,我们无法去保证用的是i-1轮的状态。在循环时,我们可能会使需要用的上一轮状态被改变,即当前物品被选多次。我们可以采取逆序解决这一问题,使得当前物品不会影响下一个容量
f[j][k]=max(f[j][k],f[ j-v[i] ][ k-m[i] ]+w[i]);
代码:
#include <bits/stdc++.h>
using namespace std;
int t[1010],z[1010],jz[1010];
int f[1010][110];
int main() {
int n,v,m;
cin>>n>>v>>m;
for(int i=0;i<n;i++)
{
cin>>t[i]>>z[i]>>jz[i];
}
for(int i=0;i<n;i++)
{
for(int j=v;j>=t[i];j--)
{
for(int k=m;k>=z[i];k--)
{
f[j][k]=max(f[j][k],f[j-t[i]][k-z[i]]+jz[i]);
}
}
}
cout<<f[v][m]<<endl;
return 0;
}
六、分组背包
简单介绍:
这道题是n组物品,每组物品有若干个,在同一组只能选择一个,体积不超过背包容量,求最大价值
思路:
- 若一组中有5件物品,则有六种决策:不选和选择任意这五个物品中的任意一个
- 我们加一层循环,在这个循环中找到这个组内的最佳决策
- 此时这道题又一次转换为01背包,求解即可
代码
#include <bits/stdc++.h>
using namespace std;
#define N 110
int v[N][N],w[N][N],p[N];
int f[N][N];
int main() {
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>p[i];
for(int j=1;j<=p[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<=p[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]<<endl;
return 0;
}
优化:
我们依然可以类似于01背包的优化,将其压缩为一维,记得倒序,原因在01背包中解释过了
优化代码:
#include <bits/stdc++.h>
using namespace std;
#define N 110
int v[N][N],w[N][N],p[N];
int f[N];
int main() {
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>p[i];
for(int j=1;j<=p[i];j++)
{
cin>>v[i][j]>>w[i][j];
}
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)
{
for(int k=1;k<=p[i];k++)
{
if(j>=v[i][k])
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
}
}
}
cout<<f[m]<<endl;
return 0;
}
七、有依赖的背包问题
简单介绍:
物品间有依赖关系,如果选取一个物品则必须选取它的父节点,求最大价值
思路:
1.我们在遍历到根节点x时,考虑选取这个节点,此时我们可以赋值:
for(int i=v[x];i<=m;i++)
f[x][i]=w[x];
2.由于背包选取了x,因此j一定大于v[x],我们枚举j时,j的范围时j—v[x] (倒着枚举)
3.此时在加一层循环,用来枚举这个子树可分得的空间,范围为[0, j-v[x] ]
代码
#include<bits/stdc++.h>
using namespace std;
int f[110][110];//f[x][v]表达选择以x为子树的物品,在容量不超过v时所获得的最大价值
vector<int> g[110];
int v[110],w[110];
int n,m,root;
int dfs(int x,int m)
{
for(int i=v[x];i<=m;i++) f[x][i]=w[x];//选取点x
for(int i=0;i<g[x].size();i++)
{
int y=g[x][i];
dfs(y,m-v[x]);
for(int j=m;j>=v[x];j--)//j的范围为v[x]~m, 小于v[x]无法选择以x为子树的物品
{
for(int k=0;k<=j-v[x];k++)//分给子树y的空间不能大于j-v[x],不然都无法选根物品x
{
f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
}
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int fa;
cin>>v[i]>>w[i]>>fa;
if(fa==-1)
root=i;
else
g[fa].push_back(i);
}
dfs(root,m);
cout<<f[root][m];
return 0;
}
八、 背包问题求方案数
简单介绍:
题目条件大致与01背包类似,但所求不同,这道题让求出最优方案选择数,结果对1e9+7取模
思路:
if(f[i][j]==f[i-1][j]&&f[i][j]==f[i-1][j-v]+w);
则
g[i][j]=g[i-1][j]+g[i-1][j-v];
if(f[i][j]==f[i-1][j]&&f[i][j]!=f[i-1][j-v]+w)
g[i][j]=g[i-1][j]
if(f[i][j]!!=f[i-1][j]&&f[i][j]==f[i-1][j-v]+w)
g[i][j]=g[i-1][j-v]
- 根据以上三种判断可以得出结果,由于当物品为0,背包容量为0时,什么都不选也为1种方案,因此g[0] [0]=1.
- 最优方案为f[n] [m],因此我们将所有与其相等的方案数相加得到的结果对1e9+7取模即为所求
代码
#include<bits/stdc++.h>
using namespace std;
#define N 1010
#define mod 1000000007
int f[N][N],v[N],w[N],g[N][N];
int n,m;
int main()
{
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]<<endl;
g[0][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
if(f[i][j]==f[i-1][j])
g[i][j]=(g[i][j]+g[i-1][j])%mod;
if(j>=v[i]&&f[i][j]==f[i-1][j-v[i]]+w[i])
g[i][j]=(g[i][j]+g[i-1][j-v[i]])%mod;
}
}
int res=0;
for (int j = 0; j <= m; ++ j)
{
if (f[n][j] == f[n][m])
{
res = (res + g[n][j]) % mod;
}
}
cout << res << endl;
return 0;
}
优化代码:
我们依然可以将其压缩为一维
#include<bits/stdc++.h>
using namespace std;
#define N 1010
#define mod 1000000007
int v[N],w[N],f[N],g[N];
int main()
{
int n,m;
cin>>n>>m;
g[0]=1;
for(int i=1;i<=n;i++)
{
cin>>v[i]>>w[i];
}
int t;
int c;
for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
{
t=max(f[j],f[j-v[i]]+w[i]),c=0;
if(t==f[j]) c=(c+g[j])%mod;
if(t==f[j-v[i]]+w[i]) c=(c+g[j-v[i]])%mod;
f[j]=t;
g[j]=c;
}
}
int res=0;
for(int j=0;j<=m;j++)
{
if(f[j]==f[m])
res=(res+g[j])%mod;
}
cout<<res<<endl;
return 0;
}
九、背包问题求具体方案
简单介绍:
与01背包所给条件类似,其所求为输出最优解中所选编号的序列,并且该编号序列的字典序最小
思路:
-
题目要得到的答案为字典序最小,我们需要采取由结果反推。
-
为什么使用反推可以达到预期效果?在dp过程中状态是不断变化的,如果在一个问题中编号2,3同时可以使问题达到最优解,在后续递推过程中会记录编号3,和我们想得到的预期结果不同,因此选择从结果反推
-
if(f[i][j]==f[i+1][j])
则说明不选第i个物品为最优解
if(f[i][j]==f[i+1][j-v[i]]+w[i])
则说明选第i个物品为最优解
if(f[i][j]==f[i+1][j]&&f[i][j]==f[i+1][j-v[i]]+w[i])
则说明选不选第i个物品都可以得到最优解,但由于我们结果要求字典序最小,因此我们需要选择该物品。
-
汇总可得当f[i][j]==f[i+1] [j-v[i]]+w[i]时,必选该物品
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1010
int v[N],w[N],f[N][N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>v[i]>>w[i];
}
for(int i=n;i>=1;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]);
}
}
int i=1;
int j=m;
while(i<=n)
{
if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
{
cout<<i<<" ";
j-=v[i];
i++;
}
else
i++;
}
return 0;
}