背包类问题总结
零、一些记号与约定
物品种类个数:\(n\)。
背包最大容量:\(m\),无特殊声明外非负。
每种物品的体积:\(v_i\),无特殊声明外非负。
每种物品的价值:\(w_i\),无特殊声明外非负。
每种物品的数量:\(c_i\),无特殊声明外 \(c_i=1\)。
物品体积的最大值:\(V=\max_{i}{v_i}\)。
物品价值的最大值:\(W=\max_{i}{w_i}\)。
物品数量的最大值:\(C=\max_{i}{c_i}\)。
一、朴素动态规划
01 背包
\(n\) 个物品,每个物品有体积 \(v_i\) 和价值 \(w_i\),求体积和不超过 \(m\) 的情况下价值和的最大值。
设 \(f_{i,j}\) 表示当前考虑了前 \(i\) 个物品,体积和为 \(j\) 的最大价值和。
转移即为 \(f_{i-1,j}+w_i \rightarrow f_{i,j+v_i}\)。
考虑把第一维去掉,设 \(f_j\) 表示目前体积和为 \(j\) 的最大价值和。
每次新加第 \(i\) 个物品,有转移 \(f_j+w_i \rightarrow f'_{j+v_i}\)。实际上不需要 \(f'\),因为 \(v_i \geq 0\),所以可以直接倒序枚举 \(j\) 转移来避免后效性。
时间复杂度 \(\mathcal O(nm)\),空间复杂度 \(\mathcal O(n+m)\)。
for(int i=1;i<=n;i++) for(int j=m-v[i];j>=0;j--) f[j+v[i]]=max(f[j+v[i]],f[j]+w[i]);
完全背包
\(n\) 个物品,每个物品有体积 \(v_i\) 和价值 \(w_i\),数量有无限个(\(c_i=+\infty\)),求体积和不超过 \(m\) 的情况下价值和的最大值。
跟 \(01\) 背包差不多,只不过转移变为 \(f_j+kw_i \rightarrow f'_{j+kv_i}(k \geq 1)\),直接顺序 dp 即可。
时间复杂度 \(\mathcal O(nm)\),空间复杂度 \(\mathcal O(n+m)\)。
for(int i=1;i<=n;i++) for(int j=0;j<=m-v[i];j--) f[j+v[i]]=max(f[j+v[i]],f[j]+w[i]);
多重背包
\(n\) 个物品,每个物品有体积 \(v_i\) 和价值 \(w_i\),数量为 \(c_i\),求体积和不超过 \(m\) 的情况下价值和的最大值。
还是考虑 01 背包的 dp 形式,转移变为 \(f_j+kw_i \rightarrow f'_{j+kv_i}(k \in [1,c_i])\)。
二进制分组优化背包
将每个 \(c_i\) 拆成 \(2^0+2^1+\cdots+2^k+(m-2^{k+1}+1),m-2^{k+1}+1 \in [0,2^{k+1})\),容易证明 \([0,c_i]\) 的所有数都可以被这 \(\lfloor \log{c_i} \rfloor+1\) 个数表示出来。
注意不是将 \(c_i\) 直接二进制拆分!!!
那么这就变成了 \(n\log C\) 个物品的 01 背包问题。举个例子,第 \(i\) 个物品的 \(c_i\) 被分成的其中一个数为 \(x\),那么这个新物品的体积为 \(xv_i\),价值为 \(xw_i\),数量为 \(1\)。
时间复杂度 \(\mathcal O(nm\log C)\),空间复杂度 \(\mathcal O(n\log C+m)\)。
单调队列优化背包
每次新加第 \(i\) 个物品,将 \(f_j\) 按照 \(j \bmod v_i\) 分组,那么显然只有同一组的会互相转移。
假设 \(j_1,j_2\) 属于同一组且 \(j_1<j_2\),那么转移可以改写为 \(f_{j_1}+(\lfloor \frac{j_2}{v_i} \rfloor-\lfloor \frac{j_1}{v_i} \rfloor)w_i \rightarrow f'_{j_2}\),且要求 \(\lfloor \frac{j_2}{v_i} \rfloor-\lfloor \frac{j_1}{v_i} \rfloor \leq c_i\)。
直接用单调队列优化,时间复杂度 \(\mathcal O(nm)\),空间复杂度 \(\mathcal O(n+m)\)。
二、特殊背包问题解法
贪心+小范围 dp
适用范围:任意背包,\(m,v_i,w_i\) 可负。
首先对物品进行一下预处理:若 \(v_i \geq 0,w_i \leq 0\),则此物品一定不优,丢掉;若 \(v_i \leq0 ,w_i \leq 0\),则先默认选取此物品,然后将 \(v_i,w_i\) 取反。
然后按性价比排序,贪心从前往后选取物品直至下一步会超过 \(m\) 或者取完了。如果取完了则直接输出答案,否则此时选取物品的体积总和 \(S \in (m-V,m]\)。
考虑目前贪心的方案与最终答案的区别。首先最终答案的体积总和 \(S'\) 一定也属于 \((m-V,m]\)(否则可以多选物品),然后最终方案一定是在目前方案的基础上,丢掉一些物品,再选择一些物品得到的。
由于所有物品 \(|v_i|\leq V\),于是可以通过调整操作顺序使得体积总和 \(S\) 始终处于 \((m-V,m+V]\)。具体来说,若当前 \(S \leq m\),则选择加入物品的操作;否则选择丢掉物品的操作。
进一步观察可以发现,在上述操作的过程中,一定不会出现多次相同的 \(S\)。证明大概就是考虑第一次到达 \(S\) 和第二次到达 \(S\) 中间的操作,由于第二次更优,所以中间操作的 \(\Delta w>0\)。将这些操作作用在一开始贪心得到的方案,就得到了一个体积和与一开始相同,且价值和更大的方案。然而我们一开始是按照性价比贪心选取的,选出来的一定是当前体积和的最优解(类似实数背包),矛盾。
于是之后最多进行 \(2V\) 次操作。枚举 \(n\) 种物品,假设第 \(i\) 种物品在贪心的时候选了 \(d_i\) 个,还剩 \(c_i-d_i\) 个,那么第 \(i\) 种物品就可以进行 \(d_i\) 次丢掉物品的操作和 \(c_i-d_i\) 次加入物品的操作,根据 \(v_i\) 的正负性放进背包。由于最多选择 \(2V\) 个物品,所以背包容量大小是 \(2V^2\) 级别的。
同理,还可以做恰好体积和为 \(m\) 的背包。时间复杂度 \(\mathcal O(n\log n+nV^2)\),空间复杂度 \(\mathcal O(n+V^2)\)。
例题:
[BalticOI 2022 Day1] Uplifting Excursion
分治法
适用范围:完全背包
记 \(f(S)\) 为 \(S\) 内所有物品的体积之和。考虑某个选取方案 \(X\),那么一定存在某种将 \(X\) 划分为 \(S\) 和 \(X/S\) 的方式,使得 \(|f(S)-f(X/S)| \leq V\)。
证明:考虑 \([\frac{f(X)-V}{2},\frac{f(X)+V}{2}]\) 这段长为 \(V\) 的区间,将 \(X\) 中的数一个个加进 \(S\),由于 \(f(S)\) 的跨度不超过 \(V\),所以一定存在某个时刻使得 \(f(S)\) 在这段区间里面。
所以如果要求体积和恰好为 \(m\) 的完全背包问题,就可以将其划分为两个完全独立的子问题,即递归求出 \([\frac{m-V}{2},\frac{m+V}{2}]\) 内的答案后,更新 \(m\) 的答案。
由于 \([l,r]\) 的区间会递归到 \([\frac{l-V}{2},\frac{r+V}{2}]\),初始区间为 \([m,m]\),所以区间长度始终不会超过 \(2V\),递归 \(\log m\) 层之后区间内的值就会变得很小,可以直接预处理。
如果不要求恰好为 \(m\),由于最终答案的体积和一定属于 \((m-V,m]\),所以就令初始区间为这个,区间长度仍然不会超过 \(2V\)。
具体实现可以不用递归。考虑第 \(i\) 层的区间大概就是 \([\frac{m}{2^i}-V,\frac{m}{2^i}+V]\),预处理出大概 \([0,2V]\) 的 dp 值后直接按层从下往上推就行了。
时间复杂度 \(\mathcal O(nV+V^2\log m)\)。
例题:
gym10106L The Knapsack problem
子集和问题
适用范围:01 背包,没有 \(w_i\) 或 \(w_i\) 全相同
折半搜索
分别枚举前一半和后一半的所有情况,之后用哈希表求解,时间复杂度 \(O(2^{\frac{n}{2}})\)
如果是求有背包容量上限的最大体积和,那么可以排序之后双指针,时间复杂度 \(O(n2^{\frac{n}{2}})\),空间复杂度 \(O(n+2^{\frac{n}{2}})\)。
bitset 优化
朴素算法
直接套用 01 背包。由于没有 \(w_i\),所以 \(f\) 的值只有 \(0/1\),表示是否可行,用 bitset 优化转移即可做到时间复杂度 \(\mathcal O(\frac{nm}{\omega})\),空间复杂度 \(\mathcal O(n+\frac{m}{\omega})\)。
随机算法
首先贪心地从前往后选物品,使得总体积 \(S \leq m\)。
然后考虑当前与最优方案的区别,一定是在目前方案的基础上,丢掉左边的物品,再选取一些右边的物品。
将左边的 \(v_i\) 取反,那么问题变为一个新的 01 背包问题,要选一些数使得和恰好为 \(m-S\)。
假设我已经知道了要选取哪些数使得这些数的和为 \(m-S\),那么将它随机打乱,其前缀和的绝对值最大值是 \(\sqrt{n}V\) 级别的。
于是将 \(v_i\) 随机打乱,做容量为 \(\sqrt{n}V\) 级别的 bitset 优化 01 背包,时间复杂度 \(\mathcal O(\frac{n\sqrt{n}V}{\omega})\),空间复杂度 \(\mathcal O(n+\frac{\sqrt{n}V}{\omega})\)。
二进制分组
设 \(S=\sum{v_i}\),那么此算法可以同时对所有 \(m \in [1,S]\) 求解。
将 \(v_i\) 相同的物品合并到一起,然后用二进制分组优化。
考虑优化完后还剩多少个物品,设 \(a_i\) 为初始体积为 \(i\) 的物品数量,那么最后会剩 \(\sum\limits_{k\geq 0}\sum\limits_{i=1}^{S}{[a_i \geq 2^k]}\) 个物品。
由于 \(\sum\limits_{i=1}^{S}{ia_i}=S\),所以 \(a_i \geq 2^k\) 的数量最多 \(\sqrt{\frac{S}{2^k}}\) 种,总和即为 \(\sum\limits_{k\geq 0}{\sqrt{\frac{S}{2^k}}}=\mathcal O(\sqrt{S})\) 种。
时间复杂度 \(\mathcal O(n+\frac{S\sqrt{S}}{\omega})\),空间复杂度 \(\mathcal O(n+\frac{S}{\omega})\)。
更优秀的线性解法
整体思路其实跟之前的「贪心+小范围 dp」差不多。首先贪心地选取前面的物品使得总和 \(\leq m\),若取完了则直接输出答案,注意可以不用对 \(v_i\) 排序。
之后还是考虑与最优方案的区别。设贪心时已经取完了前 \(pos\) 个数,后面 \(n-pos\) 个数没有选,那么最优方案一定是在目前方案的基础上,丢掉一些左边的物品,再选取一些右边的物品得到的。
不妨假设是从中间的断点 \(pos\) 往两边操作的,若当前体积和 \(\leq m\),则选择右边的物品加入,否则选取左边的物品丢掉,这样体积和始终在 \((m-V,m+V]\) 内。
于是设计 dp 状态 \(f_{l,r,w}\) 表示只考虑操作 \([l,r]\) 内的数,是否可以构造出和为 \(w\) 的方案。转移即枚举上一步操作是从 \(f_{l+1,r}\) 还是 \(f_{l,r-1}\) 转移过来,以及 \(l\) 和 \(r\) 有没有操作。具体转移如下:
-
\(f_{l,r,w} \leftarrow f_{l,r-1,w}\)
-
\(f_{l,r,w} \leftarrow f_{l+1,r,w}\)
-
\(f_{l,r,w+a_r} \leftarrow f_{l,r-1,w}(w \leq m)\)
-
\(f_{l,r,w-a_l} \leftarrow f_{l+1,r,w}(w > m)\)
需要注意到 \(w\in (m-V,m+V]\),即 \(w\) 范围是 \(\mathcal O(V)\) 的。
考虑优化,设 \(g_{r,w}\) 表示要使得 \(f_{l,r,w}=1\),\(l\) 最大是多少,没有则记为 \(0\)。
转移时还是考虑上一步从哪边转移,以及 \(l,r\) 要不要操作。具体转移如下:
-
\(g_{r,w} \leftarrow g_{r-1,w}\),即从 \(r-1\) 转移过来且不操作 \(r\);
-
\(g_{r,w+a_r} \leftarrow g_{r-1,w}(w \leq C)\),即从 \(r-1\) 转移过来且操作 \(r\);
-
\(g_{r,w-a_l} \leftarrow l(w > C,l < g_{r,w})\),即从左边转移过来且操作 \(l\)。
从左边转移但不操作 \(l\) 的情况不需要考虑,因为 \(g\) 取的是 \(\max\)。
状态数变少了,但复杂度没变,其瓶颈在于第 \(3\) 步。注意到当 \(l<g_{r-1,w}\) 时,其会在 \(r-1\) 时就被转移,然后从第 \(1\) 步转移回来。所以只需要转移 \(l \geq g_{r-1,w}\) 即可。对于单个 \(w\),转移的复杂度为 \(\mathcal O(\sum{g_{r,w}-g_{r-1,w}})=\mathcal O(n)\),于是总时间复杂度为 \(\mathcal O(nV)\)。空间复杂度用滚动数组优化可以做到 \(\mathcal O(n+V)\)。
例题:
决策单调性优化背包
适用范围:任意背包
当所有的 \(v_i\) 相同时,可以直接将 \(w_i\) 从大到小排序后贪心选,这些点构成了一个凸包。这启发我们在 \(V\) 很小(或者 \(v_i\) 种类数很少)的时候尝试把这些凸包加起来。
直接做闵可夫斯基和是错的,因为这样每次只能保留 dp 数组构成的凸包上的点,而非凸包位置上的答案是无法被计算的。凸包上的点在后续过程中并不能完全偏序非凸包上的点,于是这是错误的。
假设现在正在把体积为 \(v\) 的凸包加上去,于是按照 \(i \bmod v\) 的值对当前背包 dp 数组 \(f_i\) 分组,每组单独做。
每一组都是形如一个 \(c_i=\max\limits_{j=0}^{i}{a_j+b_{i-j}}\) 的 \((max,+)\) 卷积形式,不妨设 \(f_i\) 表示的是体积和 \(\leq i\) 的答案,那么序列 \(a\) 会满足单调性,序列 \(b\) 会满足凸性(也就有四边形不等式)。
设矩阵 \(A_{i,j}=a_j+b_{i-j}(j \leq i)\),那么不难证明 \(A\) 满足四边形不等式,即 \(A\) 为完全单调矩阵,使用 SMAWK 求解每行的最大值所在位置即可。
时间复杂度 \(\mathcal O(n\log n+mV)\),空间复杂度 \(\mathcal O(n+m)\)。如果偷懒用分治或二分栈,后面的复杂度会带一个 \(\log\),但是常数较小。
多项式优化背包
适用范围:无限背包,算方案数而非最优化,可对 \([1,m]\) 都求答案
多项式不在本文考虑范围内,只是浅浅地点一下。
考虑写出体积为 \(v\) 的生成函数:\(F_{v}(x)=\frac{1}{1-x^{v}}\),于是答案即为 \([x^{1\sim m}]\prod\limits_{i=1}^{n}{F_{v_i}(x)}\)。
直接卷积不太行,考虑把所有 ln 加起来再 exp 回去。
注意到 \(\operatorname{ln}(1-x^{v})=-\sum\limits_{i \geq 1}{\frac{x^{vi}}{i}}\),于是只需要统计出每个 \(v\) 的出现次数,然后给对应的 \(\frac{m}{v}\) 项系数加一下就行了。
时间复杂度 \(\mathcal O(n+m\log m)\),空间复杂度 \(\mathcal O(n+m)\)。
例题: