背包问题
# 背包问题
# 01背包
有 个物品和一个容量为 的背包,每个物品有重量 和价值 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总价值不超过背包的容量
定义状态 表示前 个物品,容量为 的背包所能达到的最大价值
考虑转移,假设当前已经处理好了前 个物品的状态,那么对于第 个物品,不放入背包时就是 ,放入背包时就是
转移方程:
大部分背包问题的转移方程都是在这个公式上推导出来的,这个方程非常重要,所以详细解释一下:
# 优化空间复杂度
如果直接采用二维数组来进行记录,会出现 MLE。可以改成滚动数组的形式来优化
考虑到对 有影响的只有 ,可以去掉第一维,直接用 来表示处理到当前物品背包容量为 的最大价值,得出以下方程
当我们正着枚举时,发现会发生错误
for (int i = 1; i <= n; i++)
for (int l = 0; l <= W - w[i]; l++)
f[l + w[i]] = max(f[l] + v[i], f[l + w[i]]);
2
3
对于同一个物品,也就是同一个 中, 可能被放入多次,与题意不符
为了避免这种情况的发生,我们可以该边枚举的顺序,从 到 ,这样 就会在 前先更新,所以更新 的那个状态就是 与题意相符合
for (int i = 1; i <= n; i++)
for (int l = W; l >= w[i]; l--) f[l] = max(f[l], f[l - w[i]] + v[i]);
2
# 初始化细节问题
事实上,背包有两种不太相同的问法,有些题目要求 ”恰好装满背包“ 时的最优解,有的题目没有要求装满
如果是第一种问法,那么再初始化时除了 为 ,其他的 均为 。这样, 的 都存在一种方法刚好装到
如果第二种问法,那么初始化时全为 ,这样任何容量的背包都有一个合法解:”什么都不装“
# 一个常数优化
不懂
# 完全背包
完全背包模型与 01 背包类似,与 01 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。
我们可以借鉴 01 背包的思路,进行状态定义:设 为只能选前 个物品时,容量为 的背包可以达到的最大价值。
考虑一种朴素的做法,对于第 件物品,枚举其选了 个来转移。这样的时间复杂度是 的
考虑做一个简单的优化,可以发现对于 ,只要通过 来转移就好了
考虑到在转移时 已经被 更新过,所以 就是充分考虑了第 件物品所选次数后得到的最优结果。
我们可以去掉第一维来优化空间复杂度,不难明白压缩后的循环时正向的
for (int i = 1; i <= n; i++)
for (int l = w[i]; l <= W; l++)
if (f[l - w[i]] + v[i] > f[l]) f[l] = f[l - w[i]] + v[i]; // 核心状态方程
2
3
# 一个简单有效的优化
考虑若两个物品 满足 且 则可以将物品 直接去掉,不用考虑。显然, 比 更有性价比
具体实现是可以采用 来实现,也可以讲费用大于 的物品去掉,然后使用类似于计数排序的做法,计算出重量相同的物品中,哪个价值最高,是 的复杂度
对于随机数据能大大减少物品的件数,但是我们可以通过造特殊数据让这种优化失效。
# 多重背包
多重背包也是 01 背包的一个变式。与 01 背包的区别在于每种物品有 个,而非一个。
一个很朴素的想法就是:把「每种物品选 次」等价转换为「有 个相同的物品,每个物品选一次」。这样就转换成了一个 01 背包模型,套用上文所述的方法就可已解决。状态转移方程如下:
时间复杂度为
# 二进制拆分
考虑优化,显然 的部分无法优化了,只能从 下手
考虑到一种物品有 个,而且选择第一个+第二个 和选择 第二个+第三个 完全等价,浪费了很多时间,考虑将一个物品拆分
我们可以通过「二进制分组」的方式
简单的说,就是把 拆分成几个 和一个剩余项
例如:
通过这种拆分方式,组合出任意一个 的数
时间复杂度
# 混合背包
如果把前面的三种情况混合在一起,也就是说,有的物品可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)
# 01 背包与完全背包的混合
考虑在刷 01 背包和完全背包只有第二层遍历顺序的不同,那么我们只需要根据物品的类别选用逆序或者顺序即可,复杂度为
# 再加上多重背包
我们将一类物品分成 个 01背包
如果考虑单调队列,那么时间复杂度可以优化到
最清晰的写法是调用我们前面给出的三个过程。
# 二维费用的背包问题
二维费用背包只是一个背包有两个费用 和 ,两种费用可付出的最大值(背包容量)是 和 ,物品价值为
方法和 01背包 是类似的,只是多了一维,定义 表示枚举到第 个物品,第一个容量装了 ,第二个容量装了 ,转移方程为:
这个方程也可以采用 01 背包的想法来优化空间
for (int k = 1; k <= ts; k++) // 循环每一组
for (int i = m; i >= 0; i--) // 循环背包容量
for (int j = 1; j <= cnt[k]; j++) // 循环该组的每一个物品
if (i >= w[t[k][j]]) // 背包容量充足
dp[i] = max(dp[i],
dp[i - w[t[k][j]]] + c[t[k][j]]); // 像0-1背包一样状态转移
2
3
4
5
6
这里要注意:一定不能搞错循环顺序,这样才能保证正确性。
# 分组背包
有 个物品和一个容量为 的背包,第 件物品的费用是 ,价值是 ,这些物品被划分为 组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
对于每组来说,要么选一个,要么一个都不选。设 表示前 组物品花费费用 能取得的最大权值,有
for (int k = 1; k <= ts; k++) // 循环每一组
for (int i = m; i >= 0; i--) // 循环背包容量
for (int j = 1; j <= cnt[k]; j++) // 循环该组的每一个物品
if (i >= w[t[k][j]]) // 背包容量充足
dp[i] = max(dp[i],dp[i - w[t[k][j]]] + c[t[k][j]]); // 像0-1背包一样状态转移
2
3
4
5
分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题。由分组的背包问题进一步可定义“泛化物品”的概念
# 有依赖的背包问题
在背包问题中,可能某些物品之间存在某种 “依赖” 的关系,也就是说,物品 依赖于物品 ,表示若想选择物品 的先决条件是选了物品 。
# 简化的问题
我们先考虑一个比较简单的问题,如果没有某个物品既依赖于别的物品,又被别的物品所依赖。另外,没有某件物品同时依赖多件物品
我们可以把不依赖于别物品的物品称为 "主键",依赖于某主键的物品称为 “附件”。那么这个简化问题可以看成所有物品由若干主键和依赖于每个主键的一个附件集合组成
考虑一个主键和它的附件集合,可能的方案数达到了 个(主键+每个附件选或者不选)
考虑到所有策略都是互斥的,也就是说,一个主键和他的附件的一种方案可以看成是一个物品,一个主键的所有方案就可以看多是分组背包中的一个物品组。但是这一步转化并不能给出一个好的算法,物品组中的物品还是和原问题的策略一样多
我们想到完全背包那个简单有效的优化,对于第 物品中的物品,对于第 个物品组中的物品,所有费用相同的物品只留一个价值最大的。所以,对主键 的附件集合先进行一次 01背包,得到多件附件重量为 时的最大价值 。那么可以把主键以及它的附件集合看成时 个物品,其中重量 的物品的价值是
然后直接用分组背包的算法解决问题即可
# 较一般的问题
我们去掉那个简化条件,那么依赖关系就是一个图论中的 “森林”。主件的附件可以拥有自己的附件集合,但是附件的附件不能是自己,也就是说,不能产生环
解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。由于附件可能还有附件,我们可以递归的来看,先处理所有儿子节点,把然后再想办法把儿子节点合并,着有点触及到了 “泛化物品” 的思想。
其实数种的每个子树都等价于一个泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的泛化物品的和
# 泛化物品
考虑这样一种物品,没有固定的重量和价值,物品的价值和你分配给他的重量有关。
在背包容量为 的背包问题种,泛化物品的价值 和重量 之间满足一个函数关系
在普通的 01背包中,除了给定的 之外, 的所有值都为
如果是完全背包,那么 其他地方都为
如果多重背包,那么每个物品 的 最多到物品的上限
显然,在分组背包中的一个组也可以看成是一个泛化物品
# 泛化物品的和
如果给定了两个泛化物品 和 ,要用一定费用从这两个泛化物品中河道最大价值?也就是要把两个泛化物品合成成一个泛化物品 ,我们只需要枚举一个给定的容量 ,需要分配多少空间给 ,多少空间给 ,也就是
在一个背包问题中,若将两个泛化物品代以它们的和,不影响问题的答案。事实上,对于其中的物品都是泛化物品的背包问题,求它的答案的过程也就是求所有这些泛化物品之和的过程。若问题的和为 ,则答案就是 中的最大值。
一个背包问题,无论怎么转化,可能会给出很多条件,包括每种物品的费用、价值等属性,物品之间的分组、依赖等关系等。但肯定能将问题对应于某个泛化物品。
其实可以把泛化物品单纯的理解成一种函数,函数的定义域 ,它的值就对于着 大小的背包所对应的最大价值,背包背的过程,就是对多个函数间的合并运算来得到最后的那个总的泛化物品,从总的泛化物品中来得到最后的答案。
这是编程中一种非常常见的抽象思维
# 背包问题变体
传统背包问题要求在背包容量的限制下求可以取到的最大价值,对于一些背包的变体,我们可以修改 数组的定义来得到我们想要的答案
# 输出方案
一般而言,背包问题要求一个最优质,如果要求输出这个最优质的方案,可以参照一般动态规划问题的输出方法:记录下每个状态的最优值是由状态方程的那个策略推出来的。
具体地,方程 ,再用一个数组 来表示推出 是用了哪一项,当 时表示推出 的值时时采用了 ,当 时表示采用了
# 输出字典序最小的最优方案
“字典序最小” 指的是 号物品的选择方案排列出来以后字典序最小
# 求方案总数
我们需要得到装满背包或者背包装到一个指定容量的方案数
我们只需要把转移方程中的 改成 就好了
初始条件时
# 最优方案的总数
最优方案是指物品总价值最大的方案,我们结合最大总价值和方案总数,定义 表示前 个物品,背包大小为 的最大价值, 表示这种状态下的方案数
如果 是从 这里转移过来的,那么 ,如果是从 这里转移过来的,那么
# 回退背包
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll TT = 998244353;
int main() {
freopen ("F.in", "r", stdin);
int Q, m; cin >> Q >> m;
vector<ll> dp (m + 1, 0);
dp[0] = 1;
while (Q--) {
char op; cin >> op;
int x; cin >> x;
if (op == '+') {
for (int i = m; i >= x; i--)
dp[i] = (dp[i] + dp[i - x]) % TT;
}
if (op == '-') {
for (int i = x; i <= m; i++)
dp[i] = (dp[i] - dp[i - x] + TT) % TT;
}
cout << dp[m] << '\n';
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24