背包类问题总结
零、一些记号与约定
物品种类个数:。
背包最大容量:,无特殊声明外非负。
每种物品的体积:,无特殊声明外非负。
每种物品的价值:,无特殊声明外非负。
每种物品的数量:,无特殊声明外 。
物品体积的最大值:。
物品价值的最大值:。
物品数量的最大值:。
一、朴素动态规划
01 背包
个物品,每个物品有体积 和价值 ,求体积和不超过 的情况下价值和的最大值。
设 表示当前考虑了前 个物品,体积和为 的最大价值和。
转移即为 。
考虑把第一维去掉,设 表示目前体积和为 的最大价值和。
每次新加第 个物品,有转移 。实际上不需要 ,因为 ,所以可以直接倒序枚举 转移来避免后效性。
时间复杂度 ,空间复杂度 。
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]);
完全背包
个物品,每个物品有体积 和价值 ,数量有无限个(),求体积和不超过 的情况下价值和的最大值。
跟 背包差不多,只不过转移变为 ,直接顺序 dp 即可。
时间复杂度 ,空间复杂度 。
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]);
多重背包
个物品,每个物品有体积 和价值 ,数量为 ,求体积和不超过 的情况下价值和的最大值。
还是考虑 01 背包的 dp 形式,转移变为 。
二进制分组优化背包
将每个 拆成 ,容易证明 的所有数都可以被这 个数表示出来。
注意不是将 直接二进制拆分!!!
那么这就变成了 个物品的 01 背包问题。举个例子,第 个物品的 被分成的其中一个数为 ,那么这个新物品的体积为 ,价值为 ,数量为 。
时间复杂度 ,空间复杂度 。
单调队列优化背包
每次新加第 个物品,将 按照 分组,那么显然只有同一组的会互相转移。
假设 属于同一组且 ,那么转移可以改写为 ,且要求 。
直接用单调队列优化,时间复杂度 ,空间复杂度 。
二、特殊背包问题解法
贪心+小范围 dp
适用范围:任意背包, 可负。
首先对物品进行一下预处理:若 ,则此物品一定不优,丢掉;若 ,则先默认选取此物品,然后将 取反。
然后按性价比排序,贪心从前往后选取物品直至下一步会超过 或者取完了。如果取完了则直接输出答案,否则此时选取物品的体积总和 。
考虑目前贪心的方案与最终答案的区别。首先最终答案的体积总和 一定也属于 (否则可以多选物品),然后最终方案一定是在目前方案的基础上,丢掉一些物品,再选择一些物品得到的。
由于所有物品 ,于是可以通过调整操作顺序使得体积总和 始终处于 。具体来说,若当前 ,则选择加入物品的操作;否则选择丢掉物品的操作。
进一步观察可以发现,在上述操作的过程中,一定不会出现多次相同的 。证明大概就是考虑第一次到达 和第二次到达 中间的操作,由于第二次更优,所以中间操作的 。将这些操作作用在一开始贪心得到的方案,就得到了一个体积和与一开始相同,且价值和更大的方案。然而我们一开始是按照性价比贪心选取的,选出来的一定是当前体积和的最优解(类似实数背包),矛盾。
于是之后最多进行 次操作。枚举 种物品,假设第 种物品在贪心的时候选了 个,还剩 个,那么第 种物品就可以进行 次丢掉物品的操作和 次加入物品的操作,根据 的正负性放进背包。由于最多选择 个物品,所以背包容量大小是 级别的。
同理,还可以做恰好体积和为 的背包。时间复杂度 ,空间复杂度 。
例题:
[BalticOI 2022 Day1] Uplifting Excursion
分治法
适用范围:完全背包
记 为 内所有物品的体积之和。考虑某个选取方案 ,那么一定存在某种将 划分为 和 的方式,使得 。
证明:考虑 这段长为 的区间,将 中的数一个个加进 ,由于 的跨度不超过 ,所以一定存在某个时刻使得 在这段区间里面。
所以如果要求体积和恰好为 的完全背包问题,就可以将其划分为两个完全独立的子问题,即递归求出 内的答案后,更新 的答案。
由于 的区间会递归到 ,初始区间为 ,所以区间长度始终不会超过 ,递归 层之后区间内的值就会变得很小,可以直接预处理。
如果不要求恰好为 ,由于最终答案的体积和一定属于 ,所以就令初始区间为这个,区间长度仍然不会超过 。
具体实现可以不用递归。考虑第 层的区间大概就是 ,预处理出大概 的 dp 值后直接按层从下往上推就行了。
时间复杂度 。
例题:
gym10106L The Knapsack problem
子集和问题
适用范围:01 背包,没有 或 全相同
折半搜索
分别枚举前一半和后一半的所有情况,之后用哈希表求解,时间复杂度
如果是求有背包容量上限的最大体积和,那么可以排序之后双指针,时间复杂度 ,空间复杂度 。
bitset 优化
朴素算法
直接套用 01 背包。由于没有 ,所以 的值只有 ,表示是否可行,用 bitset 优化转移即可做到时间复杂度 ,空间复杂度 。
随机算法
首先贪心地从前往后选物品,使得总体积 。
然后考虑当前与最优方案的区别,一定是在目前方案的基础上,丢掉左边的物品,再选取一些右边的物品。
将左边的 取反,那么问题变为一个新的 01 背包问题,要选一些数使得和恰好为 。
假设我已经知道了要选取哪些数使得这些数的和为 ,那么将它随机打乱,其前缀和的绝对值最大值是 级别的。
于是将 随机打乱,做容量为 级别的 bitset 优化 01 背包,时间复杂度 ,空间复杂度 。
二进制分组
设 ,那么此算法可以同时对所有 求解。
将 相同的物品合并到一起,然后用二进制分组优化。
考虑优化完后还剩多少个物品,设 为初始体积为 的物品数量,那么最后会剩 个物品。
由于 ,所以 的数量最多 种,总和即为 种。
时间复杂度 ,空间复杂度 。
更优秀的线性解法
整体思路其实跟之前的「贪心+小范围 dp」差不多。首先贪心地选取前面的物品使得总和 ,若取完了则直接输出答案,注意可以不用对 排序。
之后还是考虑与最优方案的区别。设贪心时已经取完了前 个数,后面 个数没有选,那么最优方案一定是在目前方案的基础上,丢掉一些左边的物品,再选取一些右边的物品得到的。
不妨假设是从中间的断点 往两边操作的,若当前体积和 ,则选择右边的物品加入,否则选取左边的物品丢掉,这样体积和始终在 内。
于是设计 dp 状态 表示只考虑操作 内的数,是否可以构造出和为 的方案。转移即枚举上一步操作是从 还是 转移过来,以及 和 有没有操作。具体转移如下:
需要注意到 ,即 范围是 的。
考虑优化,设 表示要使得 , 最大是多少,没有则记为 。
转移时还是考虑上一步从哪边转移,以及 要不要操作。具体转移如下:
-
,即从 转移过来且不操作 ;
-
,即从 转移过来且操作 ;
-
,即从左边转移过来且操作 。
从左边转移但不操作 的情况不需要考虑,因为 取的是 。
状态数变少了,但复杂度没变,其瓶颈在于第 步。注意到当 时,其会在 时就被转移,然后从第 步转移回来。所以只需要转移 即可。对于单个 ,转移的复杂度为 ,于是总时间复杂度为 。空间复杂度用滚动数组优化可以做到 。
例题:
决策单调性优化背包
适用范围:任意背包
当所有的 相同时,可以直接将 从大到小排序后贪心选,这些点构成了一个凸包。这启发我们在 很小(或者 种类数很少)的时候尝试把这些凸包加起来。
直接做闵可夫斯基和是错的,因为这样每次只能保留 dp 数组构成的凸包上的点,而非凸包位置上的答案是无法被计算的。凸包上的点在后续过程中并不能完全偏序非凸包上的点,于是这是错误的。
假设现在正在把体积为 的凸包加上去,于是按照 的值对当前背包 dp 数组 分组,每组单独做。
每一组都是形如一个 的 卷积形式,不妨设 表示的是体积和 的答案,那么序列 会满足单调性,序列 会满足凸性(也就有四边形不等式)。
设矩阵 ,那么不难证明 满足四边形不等式,即 为完全单调矩阵,使用 SMAWK 求解每行的最大值所在位置即可。
时间复杂度 ,空间复杂度 。如果偷懒用分治或二分栈,后面的复杂度会带一个 ,但是常数较小。
多项式优化背包
适用范围:无限背包,算方案数而非最优化,可对 都求答案
多项式不在本文考虑范围内,只是浅浅地点一下。
考虑写出体积为 的生成函数:,于是答案即为 。
直接卷积不太行,考虑把所有 ln 加起来再 exp 回去。
注意到 ,于是只需要统计出每个 的出现次数,然后给对应的 项系数加一下就行了。
时间复杂度 ,空间复杂度 。
例题:
本文作者:AFewSuns
本文链接:https://www.cnblogs.com/AFewSuns/p/knapsack.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步