扣除某个数的背包
这玩意还是挺常见的都遇到了两三次了还是记一下。
本文的「背包问题」指形如 给出若干二元组 ,求
的问题。
比如背包求方案数中 ,背包求最大价值中 。
背包问题,对每个物品求不加入这个物品的贡献的背包答案。
直接枚举物品每次重新背包是 的,但是这样显然太浪费了。
常见的一些技巧:
维护前后缀——求固定体积
扣掉一个东西的常见做法就是维护前后缀信息,让 表示物品 的背包, 表示物品 的背包,如果要查询背包的单个值 的话就可以体现出类似 meet-in-the-middle 不用合并全局的优势了,枚举 ,求 即可,时空复杂度是 。
维护前后缀——求整个背包数组
求出整个数组对于一般的 ,当 卷积可以低于 时才有用,常见的只有 卷积可以用 FFT/NTT 低于 求出 dp 数组。直接使用前缀和后缀的卷积合并,时间复杂度是 ,空间不变。
像什么 卷积能做的话要背包具有凸性,那就根本不用背包。
序列分治
其实对于 位置扣掉物品 就是对于 加入这个物品,可以考虑使用线段树分治或者分治完成。
线段树分治就直接加入 这两条线段就行了,然后 dfs 线段树,离开的时候撤回所有操作。
一般分治可以考虑将 分治为 ,递归左侧的时候加入右侧的物品,递归右侧时加入左侧的物品。
每次背包的变化量都是 的,繁琐的撤回不如直接把整个背包数组存下来,然后撤回时直接赋值回操作前的背包数组。
时间复杂度是 ,空间不变,优点是不需要可逆。
背包回退
回退背包好像一般仅限于背包操作是 这种求方案数的,无序的,线性的,有可减性的,可逆的(本质即对背包操作的多项式的常数项非 0 或矩阵满秩)。
考虑背包更新是无序的,先加入拿个物品是一样的,所以我们先求出一个完整的背包,然后扣掉这个物品,因为无序性我们可以假装这个物品是最后加入的,然后将它对背包的更新做一遍逆操作。
例:01 背包的操作
for(int i = V ; i >= 0 ; -- i)
f[i] += f[i-a];
的逆操作就是
for(int i = 0 ; i <= V ; ++ i)
f[i] -= f[i-a];
循环的顺序改变是因为本身我们的(01)背包是要用 没更新的数 去更新数,所以现在要用 已经还原的数 去还原待还原的数。
如果方案数很大,维护可行性的话可以使用多哈希取模的方法,这种特殊的问题用自然溢出就是找死。
小逝牛刀
板子,求整个 dp 数组,求方案数。
建议都写写看。
基本 dp 解法在这里,是个很套路的题,其实你不会也不影响你学习这个背包回退的技巧,如果没兴趣不妨直接往下看。
我们现在要求一个概率的二维背包(维护实数),其转移形式为
其中满足 (因为是概率)。
要分别求扣掉每个 的贡献后的背包。
直接做可以做到 求出所有方法,可以通过。
各种做法:
维护前后缀:因为要求整个数组,所以需要合并一个二维背包。使用二维 FFT,复杂度 ,常数大。
分治:也是类似的分治,可以在每层把操作前的 数组存下来直接回到操作前,而不用撤回,复杂度 ,常数小。
回退:
考虑我们的操作:
for(int i = c ; i >= 0 ; -- i) {
for(int j = c ; j >= 0 ; -- j) {
f[i][j] *= a;
if(j > 0) f[i][j] += f[i][j - 1] * b ;
if(i > 0) f[i][j] += f[i - 1][j] * c ;
}
}
对它进行逆操作
for(int i = 0 ; i <= c ; ++ i) {
for(int j = 0 ; j <= c ; ++ j) {
if(j > 0) f[i][j] -= f[i][j - 1] * b ;
if(i > 0) f[i][j] -= f[i - 1][j] * c ;
f[i][j] /= a;
}
}
但是现在就产生了一个问题:当被回退的数的 时,就无法进行回退了,因为 并不是一个单一映射,它是不可逆的。
考虑 ,所以 ,用三者中的非零值来解方程即可,为精度着想当然是取最大值,这里举例用 。
这样就得从右往左更新,如果选择 就要从下往上更新。
复杂度 ,比分治快。
本文已经结束了。本文作者:ღꦿ࿐(DeepSea),转载请注明原文链接:https://www.cnblogs.com/Dreamerkk/p/17703589.html,谢谢你的阅读或转载!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2022-09-14 GYM103861F 解题报告