高维前缀和 / $SOS\ dp$

1. 一些定义

  • 对于两个二进制数 \(x,y\) , 我们称 \(y\)\(x\) 的子集 , 当且仅当 \(x\&y=x\) , 意即 :
    \(x\)\(1\) 的位上 \(y\) 可取 \(1\)\(0\) , \(x\)\(0\) 的位上 \(y\) 只能取 \(0\) .

  • 对于一个二进制数 , 我们用 \(mask\) 表示它的子集中的元素 .

2. 引例

问题 :
给定一个有 \(2^n\) 个元素的数组 \(A\) , 再给定一个数,以这个数二进制下的子集作为 \(A\) 数组下标 , 求这些 \(A\) 数组元素之和 .

例如二进制数为 \(101\) , 那么由该二进制数的子集组成的集合 \(x=\left\{000,001,100,101\right\}\) ,那么需要求出 \(A_{000}+A_{001}+A_{100}+A_{101}\) .

这样的 子集和问题 可以通过 \(SOS\ dp\) 解决 .

3. 思想及代码

比如 , 对于一个二进制数的任一二进制位 :
若该位为 \(0\) , 那么在它的子集中的数该位也只能是 \(0\) ;
若该位为 \(1\) , 那么在它的子集中的数该位也可以是 \(0\)\(1\) .
那么由对应的情况来转移 .
nh
\(dp_{mask,i}\) 表示此时 \(mask\) 的前面几位固定和 \(mask\) 一样 , 但是最后的 \(i\) 位可以更改 , 相当于是最后 \(i\) 位的子集 (下标从 \(0\) 开始) .
则有代码 :

for(int mask=0; mask<(1<<n); mask++) //枚举每个数
{
	f[mask][-1] = A[mask];
	for(int i=0; i<n; i++) //枚举这个数的每一位
	{
		if(mask&(1<<i)) //该位是1
			dp[mask][i]=dp[mask][i-1]+dp[mask^(1<<i)][i-1]; //该位为0或1的两种情况相加
		else
			dp[mask][i]=dp[mask][i-1]; //该位为0的情况
	}
	F[mask]=dp[mask][N-1]; //考虑完所有的位即为答案
}

因为从小到大遍历的顺序保证该位为 \(0\) 的比该位为 \(1\) 的更早遍历并处理到 , 所以答案是正确的 .

举例 :

\(101\)

\(i=0\)
\(f_{001,0}=A_{000}+A_{001}\)
\(f_{101,0}=A_{100}+A_{101}\)

\(i=1\)
\(f_{001,1}=A_{000}+A_{001}\)
\(f_{101,1}=A_{100}+A_{101}\)

\(i=2\)
\(f_{101,2}=f_{001,1}+f_{101,1}=A_{000}+A_{001}+A_{100}+A_{101}\)

注意到每一个 \(i\) 都只与 \(i-1\) 相关联 ( \(i-1\) 时已完成了前面所有位数的处理 ) , 所以可以将数组改成一维 .
一维数组代码 :

for(int i=0; i<(1<<n); i++) //初始时都是自己
    F[i]=A[i];

for(int i=0; i<n; i++)
{
//进入循环前,当前的每个数只是由最后i-1位作为可取子集的位计算得到的结果
//现在枚举第i位(从低到高的数位),通过最后i-1位可变的情况来转移
    for(int mask=0; mask<(1<<n); mask++) //枚举数
    {
        if(mask&(1<<i)) F[mask]+=F[mask^(1<<i)];
        //该位为1就加上该位是0的情况
        //是0就不加,和之前的一样
        
    }
}

同样的 , 枚举到第 \(i\) 位时 , 对于转移 \(mask\) 所需的数肯定是已经处理到了的 , 答案正确 .

Q : 为什么不会算重复 ? 比如转移 \(101\) 的最高位 , \(100\) 也含有 \(000\) , \(001\) 也含有 \(000\) , 不会算多次吗 ?

A : 因为每次更新 \(mask\) 的第 \(i\) 位都不是用这些数最终的和来更新的 , 比如上述情况中 , 在上一轮转移 \(100\) 的时候 , 它的最高位 \(1\) 是固定的 , 此时它的 \(F\) 仅仅是后面几位可取子集的和 , 而上一轮的 \(101\) 也是一样 . 因此这一轮转移 \(101\) 时 ,所用到的 \(001\)\(101\) 事实上被划分成了两个完全不相交的子集和 ( 以最高位是否为 \(1\) 划分 ) , 是不会重复的 .

综上 , 正确性得到证明 , 时间复杂度为 \(\mathcal{O}(n*2^n)\) .

4. 总结

只需要满足集合的包含关系 , \(SOS\ dp\) 都可以处理 , 比如子集和 , 子集最大值 , 子集次大值 , 超集相关 , 等等 .

5. 相关题目做题记录

ARC100E Or Plus Max

\(i|j\le k\) 的条件说明 \(i,j\) 都是 \(k\) 的子集。
\(i\)\(j\) 不能相等,那 \(max(a_i+a_j)\) 就是令最大值与次大值相加。令一个数组存子集最大值,另外一个数组存子集次大值,不断用当前 \(k\) 取的值更新 \(ans\) 并输出。
为什么 \(ans\) 不是只取当下值输出?设输出答案时,之前枚举到一个 \(k'\) , 那因为 \(k'<k,i'|j'\le k'\) , 则 \(i'|j'< k\),一样满足条件,而且虽然 \(k\) 整体比 \(k'\) 大,但依然有可能出现 \(k'\) 的某些位取到 \(1\)\(k\) 只能取到 \(0\) 的情况,此时 \(k'\) 的部分子集 \(k\) 是取不到的,因此前面的情况对当下的 \(ans\) 也可能有贡献。

CF165E Compatible Numbers

基础分析

\(i\&j=0\) 的条件,意即
\(i\) 该位为 \(1\)\(j\) 该位只能是 \(0\) ;
\(i\) 该位为 \(0\)\(j\) 该位可以是 \(0\)\(1\) ;
考虑最基础的情况,我们肯定希望 \(i\)\(j\) 每一位都不同,也就是对于 \(i\&(\sim i)\),肯定是满足条件的。再根据上方的判断,我们发现将 \(\sim i\) 中取 \(1\) 的位变成 \(0\) 同样满足条件,也就是说,\(\sim i\) 的子集都是满足条件的,并且只有这种情况满足条件。
总结一下,对于 \(a_i\),我们需要判断在 \(\sim a_i\) 的子集中是否存在一个 \(a_j\)

理解

透过这道题我们来深化一下对于 \(SOS\ dp\) 的理解。
我原本的思路:既然要找 \(\sim a_i\) 的子集,那么存入 \(\sim a_i\) 进行 \(SOS\ dp\),后面乱搞。
这种思路为什么不对呢,我们一般 \(SOS\ dp\) 用的这种思想,是最开始存储最具体的信息,也就是将最开始的信息放入最“子”的子集,然后子集的“父”状态不断汇集这些子集的信息得到答案,是从子集向超集传递的过程。就像求子集和,先处理好“子”的情况,“父”的情况用“子”来求和。那么对于这样的题,最后的结果就汇集在“父”上面。而我刚刚讲的这种思路,相当于是存入了“父”,然后要看它的“子”存不存在,这实际上与上述的思想相反了。这种思路的做法也可以做,在后面补充。
因此我们采用直接向 \(f_{a_i}\) 中存入 \(a_i\) 的方法,此时进行 \(SOS\ dp\),则它是最“子”的信息,需要向上传递给那些数位上是 \(1\) 的“父”状态。也就是向超集扩展。那实际上,将这些扩展的状态取反之后,得到的就是将 \(a_i\) 取反之后的子集,也就使得查询这些子集里的数,即 \(a_j\) 时,取反后就能得到 \(a_i\)
例如,对于 \(1100\),其中一个父状态 \(1101\) 继承了 \(1100\) 的值,将它取反后是 \(0010\),是 \(!1100=0011\)的子集,这样就使得 \(1100\) 的补集的子集,查询它们时都会得到 \(1100\) 的值。

大体代码框架

for(int i = 1; i <= n; i++ ){
    f[a[i]]=a[i];
    c[i]=a[i]^((1<<k)-1); //取反,k就是最大位数+1,相当于把a[i]每一位异或上1
}
for(int i = 0; (1<<i) <= maxa; i++ ){
    for(int mask = 0; mask <= maxa; ++mask ){
        if(mask&(1<<i)&&f[mask^(1<<i)]) f[mask]=f[mask^(1<<i)];
    }
}
for(int i = 1; i <= n; i++ ){
    if(f[c[i]]==0) printf("-1 "); //c[i]为a[i]取反
    else printf("%d ",f[c[i]]);
}

然后刚才提到一种从超集传递到子集的思路,也是可做的,具体来说,初始就取反存入从大到小枚举集合(保证 \(1\) 的比 \(0\) 先处理到),若该位是 \(0\) 就从 \(1\) 转移。最后就可以直接输出了。

另一种理解

\(a_i\)\(0\)\(a_j\) 可取 \(0\)\(1\)
\(a_i\)\(1\)\(a_j\) 只取 \(0\)
发现和朴素的子集差别在于 \(a_i\) 取反:
\(\sim a_i\)\(1\)\(a_j\) 可取 \(0\)\(1\)
\(\sim a_i\)\(0\)\(a_j\) 只取 \(0\)
也就是说取反 \(a_i\) 后就可以直接在它的子集里找答案 \(a_j\)。但是 \(a_j\) 是没有变的,因此 \(f\) 存入每个 \(a_j\) 不变(就是存入了每个 \(a_i\))。

拓展

如果题目要求在有多个情况时输出下标最大的那个怎么解决?
我们知道我们在更新 \(f_{mask}\) 的时候事实上任意一种情况都是可行的,而且我们的 \(f_{mask}\) 里记录着原数组里的元素,那么我们可以改为让 \(f\) 数组记录 \(a\) 数组对应元素的下标,这样在更新 \(f_{mask}\) 时不断取 \(max\),最后在作为下标输出对应的 \(a\) 中元素即可。

CF1995D Cases

posted @ 2025-09-29 21:26  Sqqqz185  阅读(6)  评论(0)    收藏  举报