扣除某个数的背包

这玩意还是挺常见的都遇到了两三次了还是记一下。

本文的「背包问题」指形如

\[B(x) =\bigoplus_{(\sum v_i)=x} (\bigotimes_{i=1}^n A_i(v_i)) \]

的问题。

比如背包求方案数中 \((\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 数组,求方案数。

建议都写写看。

CF303E

基本 dp 解法在这里,是个很套路的题,其实你不会也不影响你学习这个背包回退的技巧,如果没兴趣不妨直接往下看。

我们现在要求一个概率的二维背包(维护实数),其转移形式为

\[f'_{i,j}=f_{i,j}\times a_x + f_{i,j-1}\times b_x +f_{i-1,j}\times c_x \]

其中满足 \(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\)

\[\begin{aligned} f'_{i,j} &= a f_{i,j} + bf_{i,j-1} + cf_{i-1,j}\\ f_{i,j-1}&= \dfrac {f'_{i,j}+af_{i,j}+cf_{i-1,j}}{b} \end{aligned} \]

这样就得从右往左更新,如果选择 \(c\) 就要从下往上更新。

复杂度 \(O(nV^2)\),比分治快。

posted @ 2023-09-14 21:49  寂静的海底  阅读(207)  评论(0编辑  收藏  举报