10.背包问题
背包问题
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]]);
对于同一个物品,也就是同一个
为了避免这种情况的发生,我们可以该边枚举的顺序,从
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]);
初始化细节问题
事实上,背包有两种不太相同的问法,有些题目要求 ”恰好装满背包“ 时的最优解,有的题目没有要求装满
如果是第一种问法,那么再初始化时除了
如果第二种问法,那么初始化时全为
一个常数优化
不懂
完全背包
完全背包模型与 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]; // 核心状态方程
一个简单有效的优化
考虑若两个物品
具体实现是可以采用
对于随机数据能大大减少物品的件数,但是我们可以通过造特殊数据让这种优化失效。
多重背包
多重背包也是 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背包一样状态转移
这里要注意:一定不能搞错循环顺序,这样才能保证正确性。
分组背包
有
对于每组来说,要么选一个,要么一个都不选。设
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背包一样状态转移
分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题。由分组的背包问题进一步可定义“泛化物品”的概念
有依赖的背包问题
在背包问题中,可能某些物品之间存在某种 “依赖” 的关系,也就是说,物品
简化的问题
我们先考虑一个比较简单的问题,如果没有某个物品既依赖于别的物品,又被别的物品所依赖。另外,没有某件物品同时依赖多件物品
我们可以把不依赖于别物品的物品称为 "主键",依赖于某主键的物品称为 “附件”。那么这个简化问题可以看成所有物品由若干主键和依赖于每个主键的一个附件集合组成
考虑一个主键和它的附件集合,可能的方案数达到了
考虑到所有策略都是互斥的,也就是说,一个主键和他的附件的一种方案可以看成是一个物品,一个主键的所有方案就可以看多是分组背包中的一个物品组。但是这一步转化并不能给出一个好的算法,物品组中的物品还是和原问题的策略一样多
我们想到完全背包那个简单有效的优化,对于第
然后直接用分组背包的算法解决问题即可
较一般的问题
我们去掉那个简化条件,那么依赖关系就是一个图论中的 “森林”。主件的附件可以拥有自己的附件集合,但是附件的附件不能是自己,也就是说,不能产生环
解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。由于附件可能还有附件,我们可以递归的来看,先处理所有儿子节点,把然后再想办法把儿子节点合并,着有点触及到了 “泛化物品” 的思想。
其实数种的每个子树都等价于一个泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的泛化物品的和
泛化物品
考虑这样一种物品,没有固定的重量和价值,物品的价值和你分配给他的重量有关。
在背包容量为
在普通的 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;
}