扣除某个数的背包
这玩意还是挺常见的都遇到了两三次了还是记一下。
本文的「背包问题」指形如
的问题。
比如背包求方案数中 \((\oplus,\otimes)=(+,\times)\),背包求最大价值中 \((\oplus,\otimes)=(\max,+)\)。
背包问题,对每个物品求不加入这个物品的贡献的背包答案。
直接枚举物品每次重新背包是 \(O(n^2V)\) 的,但是这样显然太浪费了。
常见的一些技巧:
维护前后缀
扣掉一个东西的常见做法就是维护前后缀信息,让 \(f_{i,j}\) 表示物品 \([1,i]\) 的背包,\(g_{i,j}\) 表示物品 \([i,n]\) 的背包,如果要查询背包的单个值 \(ans_c\) 的话就可以体现出类似 meet-in-the-middle 不用合并全局的优势了,枚举 \(j\),求 \(\bigoplus f_{i-1,j}\otimes g_{i+1,c-j}\) 即可,时空复杂度是 \(O(nV)\)。
维护前后缀——求整个数组
求出整个数组对于一般的 \((\oplus,\otimes)\),当 \((\oplus,\otimes)\) 卷积可以低于 \(n^2\) 时才有用,常见的只有 \((+,\times)\) 卷积可以用 FFT/NTT 低于 \(O(n^2)\) 求出 dp 数组。直接使用前缀和后缀的卷积合并,时间复杂度是 \(O(n V\log V)\),空间不变。
像什么 \((\min,+)\) 卷积能做的话要背包具有凸性,那就根本不用背包。
序列分治
其实对于 \(i\) 位置扣掉物品 \(i\) 就是对于 \([1,i)\cup(i,n]\) 加入这个物品,可以考虑使用线段树分治或者分治完成。
线段树分治就直接加入 \([1,i),(i,n]\) 这两条线段就行了,然后 dfs 线段树,离开的时候撤回所有操作。
一般分治可以考虑将 \([l,r]\) 分治为 \([l,mid],(mid,r]\),递归左侧的时候加入右侧的物品,递归右侧时加入左侧的物品。
每次背包的变化量都是 \(O(n)\) 的,繁琐的撤回不如直接把整个背包数组存下来,然后撤回时直接赋值回操作前的背包数组。
时间复杂度是 \(O(nV\log n)\),空间不变,优点是不需要可逆。
背包回退
回退背包好像一般仅限于背包操作是 \((+,\times)\) 这种求方案数的,无序的,线性的,有可减性的,可逆的(本质即对背包操作的多项式的常数项非 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 解法在这里,是个很套路的题,其实你不会也不影响你学习这个背包回退的技巧,如果没兴趣不妨直接往下看。
我们现在要求一个概率的二维背包(维护实数),其转移形式为
其中满足 \(a_x,b_x,c_x\in[0,1],a_x+b_x+c_x=1\)(因为是概率)。
要分别求扣掉每个 \(x\) 的贡献后的背包。
直接做可以做到 \(O(n^2V^2)\) 求出所有方法,可以通过。
各种做法:
维护前后缀:因为要求整个数组,所以需要合并一个二维背包。使用二维 FFT,复杂度 \(O(nV^2\log V)\),常数大。
分治:也是类似的分治,可以在每层把操作前的 \(f\) 数组存下来直接回到操作前,而不用撤回,复杂度 \(O(nV^2\log n)\),常数小。
回退:
考虑我们的操作:
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;
}
}
但是现在就产生了一个问题:当被回退的数的 \(a=0\) 时,就无法进行回退了,因为 \(\times 0\) 并不是一个单一映射,它是不可逆的。
考虑 \(a+b+c=1\),所以 \(\max(a,b,c)\geq\frac 1 3\),用三者中的非零值来解方程即可,为精度着想当然是取最大值,这里举例用 \(b\)。
这样就得从右往左更新,如果选择 \(c\) 就要从下往上更新。
复杂度 \(O(nV^2)\),比分治快。
本文已经结束了。本文作者:ღꦿ࿐(DeepSea),转载请注明原文链接:https://www.cnblogs.com/Dreamerkk/p/17703589.html,谢谢你的阅读或转载!