背包问题
动态规划
动态规划问题的思考方式:
- 状态表示
- 代表的集合是什么,一般来说会加一些关于
的限制条件,比如前 个物品,总容量不超过 等等 - 表示的是集合的哪个属性,常见的有 最大值/最小值,元素数量
- 状态表示往往可以维数优化,但一般参照 转移方程 来优化,推荐一开始写全面些
- 代表的集合是什么,一般来说会加一些关于
- 状态计算方法
- 集合划分:将当前状态如何划分为更小的子结构,同时这些子结构都可以被计算出来
- 子结构到当前状态的过程就是转移方程
- 例如
可以按照是否取第 件物品进行划分(注意,划分 最好不重,一定不漏)
- 注意初始化
01 背包
有
- 状态表示:
f[i][j]
表示只取前i
个物品,背包总容量不超过j
的最大价值 - 集合划分:是否取第
i
个物品- 不取:
f[i - 1][j]
- 取:
f[i - 1][j - v[i]] + w[i]
- 这两个子结构的最大值取
max
即可
- 不取:
- 注意初始化,
f[0][0~m] = 0
核心代码:
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]);
}
}
}
- 二重循环,第一重枚举物品,第二重枚举容量大小
- 两个子结构,注意第二个只有 容量足够大 才可行
显然,f[i][j]
构成了一个矩阵图,动态规划不过就是按图打表罢了,打表的方向为 从左到右,从上到下。同时我们注意到转移方程中 f[i][j]
仅与上一行中的 f[i - 1][j]
和 f[i - 1][j - v[i]]
有关,一个在其正上方,一个在其 左上方。
因此我们将其压缩到一维,删除第一维,这样 f[j]
就会直接覆盖旧值。然而,由于打表的方向是从左到右,因此左上方的 f[i - 1][j - v[i]]
在 f[i][j]
更新之前就已经被更新为了 f[i][j - v[i + 1]]
。为了避免这种情况发生,我们可以采取从右往左打表的方式。
从右往左的打表方式自然是没有问题的,因为我们只要在更新本行时,提前准备好上一行的数据即可,无论以什么顺序更新本行都无所谓。这里由于子结构都在左上方,故采取从右往左的更新方式(正上方的恰好用自己的旧值,左上方的用本行中左边尚未更新的值即可)。
核心代码:
for (int i = 1; i <= n; i ++) {
for (int j = m; j >= 0; j --) {
if (j >= v[i]) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
}
这里面的 if
甚至可以优化掉:
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]);
}
}
TODO:滚动数组继续优化
完全背包
有
- 状态表示:
f[i][j]
表示只取前i
个物品,背包总容量不超过j
的最大价值 - 集合划分:取多少个第
i
个物品分成很多类- 选 0 个:
f[i - 1][j]
- 选 1 个:
f[i - 1][j - v[i]] + w[i]
- ...
- 选 k 个:
f[i - 1][j - v[i] * k] + w[i] * k
- 其中
(k+1) * v[i] > j
,多选一个就超了 - 这些子结构的取
max
即可
- 选 0 个:
- 注意初始化,
f[0][0~m] = 0
注意,直接这样写二维方程,时间复杂度较高,三重循环
for (int i = 1; i <= n; i ++) {
for (int j = 0; j <= m; j ++) {
for (int k = 0; k * v[i] <= j; k ++) {
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
}
}
}
就算优化为一维,也只是降低了空间复杂度,并没有降低时间复杂度。因此要考虑如何优化掉这个三重循环。
展开这个转移方程:
f[i][j] = max(f[i-1][j], f[i-1][j-v]+w, f[i-1][j-2v]+2w, f[i-1][j-3v]+3w, ...)
f[i][j-v] = max( f[i-1][j-v], f[i-1][j-2v]+w, f[i-1][j-3v]+2w, ...)
f[i][j] = max(f[i-1][j], f[i][j-v]+w)
可以看出 f[i][j]
中取了第 f[i][j-v] + w
完全替换(每一项都差个 w
,所有项的最大值自然也在这其中,因此最大值也相差一个 w
)。
我们试图直观理解 f[i][j-v] + w
的含义:它表示只在前
因此我们将集合划分为以下两类:对于第
f[i][j]
:在前件物品中任意选择,背包容量为 - 一件第
件物品都不拿: f[i-1][j]
- 至少拿一件第
件物品: f[i][j-v] + w
,剩下还是前件物品中任意选择,背包容量缩小到
于是在二维表格中,f[i][j]
会用到它正上方的 f[i-1][j]
和同一行左边的 f[i][j-v] + w
,因此如果我们想降维到一维的话,搜索顺序要保持从左到右,确保左边的 f[i][j-v] + w
更新后才轮到 f[i][j]
。
二维核心代码:
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][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]);
}
}
时间复杂度降低到了
多重背包
有
- 状态表示:
f[i][j]
表示只取前i
个物品,背包总容量不超过j
的最大价值 - 集合划分:是否取第
i
个物品- 选
个: f[i - 1][j]
- 选
个: f[i - 1][j - v[i]] + w[i]
- ...
- 选
个: f[i - 1][j - v[i] * k] + w[i] * s[i]
- 其中
(k+1) * v[i] > j
,多选一个就超了 - 这些子结构的取
max
即可
- 选
- 注意初始化,
f[0][0~m] = 0
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] + w[i] * k);
}
}
}
当数据范围变成
而我们回顾完全背包问题中 f[i][j-v] + w
的含义:它表示只在前
f(i,j) = Max(f(i-1,j), f(i-1,j-v)+w, f(i-1,j-2v)+2w, ..., f(i-1,j-sv)+sw)
f(i,j-v) = Max( f(i-1,j-v), f(i-1,j-2v)+w, ..., f(i-1,j-sv)+(s-1)w, f(i-1,j-(s+1)v)+sw)
考虑二进制优化方式:
- 假设某个物品有 1023 个,难道我们真的要枚举 1023 次呢?
- 那么我们可以将 s 分为
1, 2, 4, 8, 16, 32, 64, 128, 256, 512
一共十组,也就是打包 - 显然
0~1023
中的任何一个数都可以由上述这些数表示,直接转为二进制即可- 其他进制是否可以?不可以,因为 1023 的二进制表示中 0 表示不选这个打包,1 表示选择这个打包
- 经典的
1, 2, 5, 10, 20, 50
是否可以?不可以,例如9 = 5 + 2 + 2
就不行
- 对于
s = 200
而言:- 首先将其拆分为
1, 2, 4, 8, 16, 32, 64, 200-127
这么多物品 1, 2, 4, 8, 16, 32, 64
可以表示0 ~ 127
- 而
128 ~ 200
首先可以必选一个73
,剩下的55 ~ 127
依然可以用1, 2, 4, 8, 16, 32, 64
表示
- 首先将其拆分为
- 也就是将
S
拆分为的新物品,对新物品做 01 背包即可,也就是一共有 件物品 - 总的时间复杂度为
核心代码:
// ...
for (int i = 1; i <= n; i ++) {
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while (k <= s) {
idx ++;
v[idx] = a * k;
w[idx] = b * k;
s -= k;
k *= 2;
}
if (s > 0) {
idx ++;
v[idx] = a * s;
w[idx] = b * s;
}
}
分组背包问题
有
- 状态表示:
f[i][j]
表示只从前i
组中选物品,背包总容量不超过j
的最大价值 - 集合划分:是否取第
i
组中的物品- 不选组内物品:
f[i - 1][j]
- 选第
个物品: f[i - 1][j - v[i][1]] + w[i][1]
- ...
- 选第
个物品: f[i - 1][j - v[i][s[i]]] + w[i][s[i]]
- 注意要保证
v[i][k] <= j
- 这些子结构的取
max
即可
- 不选组内物品:
- 注意初始化,
f[0][0~m] = 0
还是和多重背包一样,选