0/1背包
最基础的背包问题
有$n$件物品和一个容量为$m$的背包。第$i$件物品的重量是$w_i$,价值是$v_i$。求解将哪些物品装入背包可在总重量不超过$m$的前提下使价值总和最大。
$f(i,j)$表示前$i$件物品,背包容量为$j$时最大价值
那么就需要考虑第$i$件物品装/不装
- 第$i$件物品无法装进背包:$f(i,j)=f(i-1,j)$,即可获得的最大值不变
- 第$i$件物品装入背包:$f(i,j)=f(i-1,j-w_i)+v_i$,即前$i-1$件物品装入容量为$j-w_i$的背包(留下$w_i$的空间来装第$i$件物品)的价值加上第$i$件物品的价值
- 第$i$件物品不装入背包:$f(i,j)=f(i-1,j)$,即可获得的最大值不变
若第$i$件物品可以装入背包,那么当前可获得的最大价值为第2、3种情况的最大值
1
2
3
4
5
6
7
8int f[n+1][m+1];
for(int i = 1;i<=n;i++){
for(int j = 0;j<=m;j++){
if(j < w[i]) f[i][j] = f[i-1][j];
else f[i][j] = max(f[i-1][j],f[i-1][j-w[i]]+v[i])
}
}
int ans = f[n][m];例题
将总时间$T$视为容量$m$,将总草药数$M$视为物品件数$n$
将每株草药的采集时间视为重量$w$,将草药价值视为$v$
则可以直接套上面的板子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using namespace std;
const int N = 1005;
int t,m;
int dp[N][N];
int v[N];
int T[N];
int main(){
scanf("%d%d",&t,&m);
for(int i = 1;i<=m;i++) scanf("%d%d",&T[i],&v[i]);
for(int i = 1;i<=m;i++){
for(int j = t;j>=0;j--){
if(j < T[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j],dp[i-1][j-T[i]] + v[i]);
}
}
printf("%d",dp[m][t]);
return 0;
}
完全背包
有$n$件物品和一个容量为$m$的背包。第$i$件物品的重量是$w_i$,价值是$v_i$。每件物品可以选无数份。求解将哪些物品装入背包可在总重量不超过$m$的前提下使价值总和最大。
与0/1背包的区别就在于每份物品的数量无限
$f(i,j)$表示前$i$件物品,背包容量为$j$时最大价值
那么就需要考虑第$i$件物品装/不装
- 第$i$件物品无法装进背包:$f(i,j)=f(i-1,j)$,即可获得的最大值不变
- 第$i$件物品装入背包:$f(i,j)=f(i,j-w_i)+v_i$,即前$i$件物品装入容量为$j-w_i$的背包(留下$w_i$的空间来装第$i$件物品)的价值加上第$i$件物品的价值
- 第$i$件物品不装入背包:$f(i,j)=f(i-1,j)$,即可获得的最大值不变
若第$i$件物品可以装入背包,那么当前可获得的最大价值为第2、3种情况的最大值
与0/1背包的区别在选第$i$件物品的时候,是将前$i$件物品装包,而不是前$i-1$件
因为每件物品可以选无数份,所以选更新过的值$f(i,j-w_i)$+$v_i$
模板
1
2
3
4
5
6
7
8int f[n+1][m+1];
for(int i = 1;i<=n;i++){
for(int j = 0;j<=m;j++){
if(j < w[i]) f[i][j] = f[i-1][j];
else f[i][j] = max(f[i-1][j],f[i][j-w[i]]+v[i])
}
}
int ans = f[n][m];在第5行进行了修改
例题
-
在本题使用了滚动数组降维否则爆空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using namespace std;
const int N = (1e7+10);
int t,m;
long long f[N];
int v[10010];
int T[10010];
int main(){
scanf("%d%d",&t,&m);
for(int i = 1;i<=m;i++) scanf("%d%d",&T[i],&v[i]);
for(int i = 1;i<=m;i++){
for(int j = T[i];j<=t;j++){
f[j] = max(f[j],f[j-T[i]]+v[i]);
}
}
printf("%lld",f[t]);
return 0;
}
滚动数组
通过不断覆盖无用的旧数据从而节约空间的操作
易知$\text{dp}$的第一维$i$仅与$i-1$有关,在操作$i$的时候旧数据$i-1$仍在数组中,因此可以压掉第一维
从前文的两种模型的状态转移方程中可以看出,
0/1背包需要上一层的数据,而完全背包不需要,且由于$i$与$j$是两个互不干扰的维度,降维对$j$无影响
因此
0/1背包使用滚动数组的时候需要倒着枚举
完全背包使用滚动数组的时候需要正着枚举
另外,如果第$i$件物品无法装入背包,则状态不需要覆盖
所以开始覆盖旧数据的最小重量为$w_i$
因此枚举时可以以$w_i$开始/结束
模板
1
2
3
4
5
6
7
8
9
10
11
12
13int dp[m+1];
// 01背包
for(int i = 1;i<=n;i++){
for(int j = m;j>=w[i];j--){
dp[j] = max(dp[i],dp[j-w[i]]+v[i]);
}
}
// 完全背包
for(int i = 1;i<=n;i++){
for(int j = w[i];j<=m;j++){
dp[j] = max(dp[i],dp[j-w[i]]+v[i]);
}
}
多重背包
$n$个物品,每个物品可选至多$n_i$个,可不选,每个体积为$v_i$,价值为$w_i$。
求总体积不超过$m$的情况下能拿走物品总价值的最大值。将至多$n_i$个物品全部视为独立的可选取$n_i$个物品,使用0/1背包解决
优化
任何一个数都可以被拆分为 $2^0,2^1,\dots,2^{k-1},d$ 的形式
因此将每个$n_i$进行二进制拆分后,一定可以凑出$\leq n_i$的所有数字
将拆分出来的数$k_j$与原价值、原体积相乘,得到一个新的物品
模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//这里的n,W分别代表n种物品,最大重量为W
scanf("%d%d",&n,&W);
int num = 1;
for(int i = 1;i<=n;i++){
//v为价值,w为重量,m为个数
int v,w,m;
scanf("%d%d%d",&v,&w,&m);
//进行二进制拆分
for(int j = 1;j<=m;j<<=1){
Val[++num] = j*v;
Wei[num] = j*w;
m -= j;
}
//此时的m相当于拆分后剩下的d
if(m) Val[++num] = v*m,Wei[num]=w*m;
}
//01背包滚动数组
for(int i = 1;i<=num;i++){
for(int j = W;j>=Wei[i];j--){
f[j] = max(f[j],f[j-Wei[i]]+Val[i]);
}
}
分组背包
$n$个物品,体积为$w_i$,价值为$v_i$。
所有物品被分为若干组,同组物品最多选一个。
求总体积不超过$m$的情况下能拿走物品总价值的最大值。把每个组当成一个物品,选的时候枚举选哪个物品
1
2
3
4for 所有的组k
for j=W..0
for 所有的i属于组k
f[j]=max(f[j],f[j-w[i]]+v[i])