背包九讲
MoCaRabbit
·
2023-07-11 17:42:16
·
个人记录
01背包
例题:采药.
我们可以用搜索完成,\text O(2^n) ,会T的飞起.
搜索中,一般会带进这些参数: 正在选的数与已选数的背包容积。
我们可以对其记忆化,其时间复杂度达到 O(nm) ,够快了。
我们继续把它用递推形式写下来。
因为递推习惯从小向大递推状态,所以改一个顺序递推,得到以下代码:
#include<bits/stdc++.h>
using namespace std;
int t,m;
int dp[110][1010],t1[1010],m1[110];
int main() {
scanf("%d%d",&t,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&t1[i],&m1[i]);
}
for(int i=1;i<=m;i++){
for(int j=1;j<=t;j++){
dp[i][j]=(j>=t1[i] ? max(dp[i-1][j],dp[i-1][j-t1[i]]+m1[i]) : dp[i-1][j]);
}
}
cout<<dp[m][t];
return 0;
}
背包问题算一种动态规划(dp ),所以数组叫 dp .
但这样,我们就罢休了吗?
不,因为 dp[i][x] 只用到了 dp[i-1] 层的东西,所以我们只需要记录 dp[i-1] 层的数组。
这个叫做滚动数组优化。
因为递推是从下自上推的,所以为了满足 二维数组的递推式,我们写内层循环时需要从大到小枚举。
代码:
#include<bits/stdc++.h>
using namespace std;
int t,m;
int dp[1010],t1[1010],m1[110];
int main() {
scanf("%d%d",&t,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&t1[i],&m1[i]);
}
for(int i=1;i<=m;i++)
for(int j=t;j>=t1[i];j--)
dp[j]=max(dp[j],dp[j-t1[i]]+m1[i]);
cout<<dp[t];
return 0;
}
完全背包
例题:疯狂的采药.
此题当然可以三层循环高时间复杂度枚举 + 滚动数组优化。
当然,这过不了。
要想过此题,需要用 \text O(mt) 的时间复杂度。
我们其实可以将 01背包 的倒序循环改成正序就行了。
因为 01背包 的倒序循环是为了防止重复选择,但是 完全背包 又 允许重复,所以可以这样做。
代码:
#include<bits/stdc++.h>
using namespace std;
long long dp[int(1e7)+10];
int m,t;
long long w[int(1e4)+10],v[int(1e4)+10];
int main() {
cin>>t>>m;
for(int i=1;i<=m;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=m;i++)
for(int j=w[i];j<=t;j++)
dp[j]=max(dp[j-w[i]]+v[i],dp[j]);
cout<<dp[t];
return 0;
}
多重背包
例题:宝物筛选
此题的背包有个特点,每个物品虽然可以重复选择,但是每个物品选择的数是有限的。
此题可以将它拆成 01 背包求解。
通过三层循环,列出状态转移方程 dp_{i,j}=\min(dp_{i-1,j},dp_{i-1,j-k \times w_i}+k \times v_i) 。
时间复杂度 $\text O(v \sum m)$。
当然,这过不了。
因为 $\{2^0,2^1,2^2...,2^n,k(k \le 2^n)\}$ 这个集合的子集之和是可以表示任何数的。
所以我们可以将每个物品的数量拆分成二进制形式进行枚举。
排除了重复的组合,效率更高了。
时间复杂度 $\text O(v \sum \log m)$。
可以通过。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,m;
int dp[int(4e4)+10];
int v[110],w[110],num[110];
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i]>>num[i];
for(int i=1;i<=n;i++){
int d=num[i];
for(int j=1;j<=d;j<<=1){
for(int k=m;k>=w[i]*j;k--)
dp[k]=max(dp[k],dp[k-w[i]*j]+v[i]*j);
d-=j;
}
for(int k=m;k>=w[i]*d;k--)
dp[k]=max(dp[k],dp[k-w[i]*d]+v[i]*d);
}
cout<<dp[m];
return 0;
}
```
# 混合背包
没啥意思,就是混合三种背包来求解。
伪代码:
```cpp
for(int i=1;i<=n;i++){
if(完全背包){
for(int j=c[i];j<=V;j++)
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
else if(01背包){
for(int j=V;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
else{//否则就是多重背包了
for(int j=V;j>=0;j--)
for(int k=1;k<=num[i];k++)
if(j-k*c[i]>=0)
f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
}
}
```
# 分组背包
例题:[通天之分组背包](https://www.luogu.com.cn/problem/P1757).
还是用滚动数组。
此题我们可以将每个组变成一个物品,跑 01 背包,怎么计算每个组的贡献呢。
就是每个物品依次更新一下 dp 数组。
代码。
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,m;
int c[1010];
unordered_map<int,int>mp;
struct node{
int a,b;
};
vector<node>v[1010];
int cnt;
int dp[1010];
int main() {
cin>>m>>n;
for(int i=1;i<=n;i++){
int a,b,g;
cin>>a>>b>>g;
if(!mp[g]) mp[g]=++cnt,v[cnt].push_back((node){a,b});
else v[mp[g]].push_back((node){a,b});
}
for(int i=1;i<=cnt;i++)
for(int k=m;k>=0;k--)
for(int j=0;j<v[i].size();j++)
if(k>=v[i][j].a)
dp[k]=max(dp[k],dp[k-v[i][j].a]+v[i][j].b);
cout<<dp[m];
return 0;
}
```
# 二维费用背包
例题:[NASA的食物计划](https://www.luogu.com.cn/problem/P1507).
通过类比,既然时两维都满足是吧,那么我们在枚举价值的时候就用两层循环。
这样就可以满足两维的费用了。
代码实现很简单。
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,h,m;
int a[110],b[110],c[110];
int dp[510][510];
int main() {
cin>>h>>m>>n;
for(int i=1;i<=n;i++)
cin>>a[i]>>b[i]>>c[i];
for(int i=1;i<=n;i++)
for(int j=h;j>=a[i];j--)
for(int k=m;k>=b[i];k--)
dp[j][k]=max(dp[j][k],dp[j-a[i]][k-b[i]]+c[i]);
cout<<dp[h][m];
return 0;
}
```
# 有依赖的背包问题