从SOSDP到FWT
本文原名 SOSDP ,2025.08.20添加了FWT部分。
符号:
如无特殊说明,\(\cap\) 表示按位与,\(\cup\) 表示按位或, \(\oplus\) 表示按位异或。
\(i\subseteq x\) 表示的是 \(i\) 作为二进制表示的集合是 \(x\) 的子集。形式化地,\(i\cap x=i\)。
SOSdp
SOSdp(sum over subsets dynamic programming)用来解决这样的问题:
给定数组 \(a\) ,定义 \(F(x)=\sum_{i \subseteq x} a_i\),求每个 \(F(x)\) 的值。
也即对 \(x\) 二进制意义下的子集作为下标的 \(a\) 求和。
为了解决这一问题,我们定义 \(f_{x,i}\) 表示二进制下仅有后 \(i\) 位与 \(x\) 不同,且是 \(x\) 子集的数作为下标的 \(a\) 求和。位数从 \(0\) 开始编号,如 \(101\) 是两位。
举个例子,\(f_{\bold{101}1010,3}=a_{\bold{101}1010}+a_{\bold{101}1000}+a_{\bold{101}0010}+a_{\bold{101}0000}\) ,加粗部分代表一定相同。
现在考虑转移。
简单解释一下:若 \(x\) 的第 \(i\) 位为 \(0\) ,那么此时被求和的元素的这一位都一定是 \(0\) ,可以直接推到 \(f_{x,i-1}\) ,否则被求和元素分为该位是 \(0/1\) 的两部分, 式子中后一项为选 \(1\) 的,前一项中异或 \(2^i\) 从而该位变为 \(0\) 。正确性得证。
于是,我们便可以在 \(O(n\log n)\) 的时间内解决问题!( \(n\)为数组元素数,若 \(n\) 为二进制下的位数则复杂度为原博客的 \(O(n2^n)\) )
压掉第二维非常好写!
//假设n为位数,101为两位数(
for(int i=0;i<n;i++)
for(int x=(1<<i);x<(1<<n);x++)
if(x&(1<<i))f[x]+=f[x^(1<<i)];
哇这也太优美了111!!!
其他应用
sosdp除了算子集还能算超集!!(超集:若 \(A\) 是 \(B\) 的子集,则 \(B\) 是 \(A\) 的超集)
for(int i=0;i<n;i++)
for(int x=(1<<i);x<(1<<n);x++)
if(!(x&(1<<i)))f[x]+=f[x^(1<<i)];
唯一的区别在于 if 中的条件。
sosdp的思维还能优化高维前缀和!!!
这里是思维,我绕了好久,并不是直接用sosdp!!!!!!!!!
先将 sum 每个位置赋值为原数组该位上的值,然后做三次这个,第一次不变,第二次line 7的i-1变为i,我们对j方向累加,第三次同理
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
for(int k=1;k<=l;k++)
{
sum[i][j][k]+=sum[i-1][j][k];
}
}
}
如果你每次从旁边几个加过来然后再容斥,复杂度应为 \(O(nml\cdot2^d)\) ,而这样可以优化到 \(O(dnml)\) !!
太伟大了sosdp。
原博客下给出了很多例题,帮大家把在 cf 上的搬运过来了(
FWT
之前一直以为 FWT 是和 FFT 之类的多项式内容强相关的,今天发现不是。
FWT一般用来解决神秘位运算卷积,形式化地,拿到两个序列 \(A,B\) ,然后我们要求的 \(C\) 由如下方式得到:
其中,\(\odot\) 为按位与,按位或,按位异或中的一种。
按位与、按位或
这两种的处理方法较为类似。下面先以按位或为例。
首先,这个要求 \(j\cup k=i\) ,比较困难,但是我们发现求 \(j\cup k\subseteq i\) 是更好求的。
我们设一个新的数组:\(FC\),满足
我们来简单转化一下,可以发现
后面这两个相乘的式子,对 \(i\) 的所有子集求和……这不是我们刚刚才学的 SOSdp 吗?
直接再设两个数组 \(FA,FB\),\(FA_i=\sum_{j\subseteq i}A_j\),\(FB\) 同理。则 \(FC_i=FA_i\cdot FB_i\)。
观察我们求出来的 \(FC\)。
Wow,和 \(FA,FB\) 的结构一模一样。(这同时意味着,如果你要对多个数组做这个卷积,可以直接拿两个数组得到的这个 \(F\) 接着做下去!)
要从 \(FC\) 还原出 \(C\),我们只要倒着再做一遍 SOSdp。like this:
for(int i=n-1;i>0;i--)
for(int x=(1<<i);x<(1<<n);x++)
if(x&(1<<i))f[x]-=f[x^(1<<i)];
到这里大家肯定都猜到按位与怎么做了。只需要把上述流程中的子集变成超集就可以了。
但是,要通过洛谷上的板子,还有一种运算。
为了更好的理解下面这个更抽象的卷积,我们先来小结一下。其实刚刚的 \(FA\) 数组就是 \(A\) 数组在按位与/按位或意义下沃尔什变换得到的新序列。如果你学过 FFT 的话,你肯定能发现这两种算法之间的共同点:
- 先通过一种方式,使用神秘的变换,把原来的序列变换成另一个。
- 此时,原来的卷积在新序列上转化成了 按位相乘 的形式。
- 变换存在逆变换。也即,我们可以通过变换后得到的序列求解出原来的序列。
现在,你已经明白了沃尔什变换的核心思想,让我们掌声有请……
按位异或!
SOSdp 是对于单一序列做的,所以当然不能弄出来出什么 “怎么怎么异或就得到 \(i\)” 的数组。我们没法直接套用上面的方法。
借鉴我们刚刚的思路:我们需要通过某种方式得到一个 \(F\) 数组的构造方式,将原来的卷积转化为按位相乘。
这个构造个人觉得其实难以注意到。我们定义 \(x \circ y\) 表示 \(x \cap y\) 中 \(1\) 数量的奇偶性,即 \(x \circ y = \mathrm{popcnt}(x \cap y) \bmod 2,\) 那么有 \((x \circ y) \oplus (x \circ z) = x \circ (y \oplus z).\)
简单口胡一下证明:把这个运算拆开来就变成
令 \(y'=y\cap x,z'=z\cap x\) 则
大概感知一下,\(y'\oplus z'\) 只可能将若干对 \(1\) 变成 \(0\),所以不改变奇偶性。
那我设 \(FA_i=\sum_{i\circ j=0}A_j-\sum_{i\circ j=1}A_j\),下面来证明,\(FC_i=FA_i\cdot FB_i\)。
然后我们又知道 \((j\oplus k)\circ i=(j\circ i)\oplus (k\circ i)\),所以上式可以变形
所以这个是对的!
现在我们要来由原序列算出 \(F\)。依旧记 \(f_{x,i}\) 表示仅考虑二进制下仅有后 \(i\) 位与 \(x\) 不同的所有下标对当前位置的 \(F\) 的贡献。
枚举每一位,若该位为 \(0\),则与当前位为 \(1\) 的数进行与运算时会使得该位由 \(1\) 变为 \(0\),贡献需要取负,\(f_{x,i}=f_{x,i-1}-f_{x\oplus 2^i,i-1}\)
若该位为 \(1\),则与当前位为 \(0\) 的数进行与运算时不会改变他的 popcount,直接加。\(f_{x,i}=f_{x,i-1}+f_{x\oplus 2^i,i-1}\)
上面这个过程其实比较抽象,我们来举个例子。
现在原数组
0,1,2,3位置上放的数就是0,1,2,3。初始时,\(f_{i,-1}=i\) (请先容许我用 \(-1\) 表示完全相同)。
考虑 \(f_{0,1}\) 的计算,\(0\) 的第 \(1\) 位为 \(0\) ,属于第一种情况。\(0\oplus2^1=2\),而下标 \(2\) 上原本的“和他的最大差异位至多是第 \(0\) 位的” 下标集合,也即 \(2,3\),是被算在 \(f_{2,0}\) 内的。
此时我们丢给 \(0\) 的下标集,第 \(1\) 位都与 \(2\) 相同,都是 \(1\),与 \(0\) 取交后这一位就没了,所以 \(\mathrm{popcount}-=1\) ,奇偶性改变。所以算 \(f_{0,1}\) 我们要减掉 \(f_{2,0}\)。
来到代码部分。我们注意到 \(j\) 与 \(j\oplus 2^i\) 要同时计算。更改一下 SOSdp 代码即可。下述代码取自洛谷板子,然后 \(cap=2^n\)。
for(int i=0;i<n;i++)
for(int j=0;j<cap;j++)
if(j&(1<<i))
{
int t=fa[j];
fa[j]=(fa[j]+fa[j^(1<<i)])%mod;
fa[j^(1<<i)]=(fa[j^(1<<i)]-t+mod)%mod;
}
求逆变换,也即我们需要通过 fc[j]+fc[j^(1<<i)],-fc[j]+fc[j^(1<<i)] 还原出两者。这个应该挺简单的。代码中的 i2 是 \(2\) 在模意义下的逆元。
for(int i=n-1;i>=0;i--)
for(int j=0;j<cap;j++)
if(j&(1<<i))
{
int t=fc[j],tt=fc[j^(1<<i)];
fc[j]=1LL*(t-tt+mod)*i2%mod;
fc[j^(1<<i)]=1LL*(t+tt)*i2%mod;
}
下面给出洛谷板子代码qwq
code
#include<cstdio>
using namespace std;
const int mn=131080,mod=998244353;
const int i2=499122177;
int n,cap;
int a[mn],b[mn],c[mn];
int fa[mn],fb[mn],fc[mn];
int main()
{
scanf("%d",&n);
cap=1<<n;
for(int i=0;i<cap;i++)scanf("%d",&a[i]);
for(int i=0;i<cap;i++)scanf("%d",&b[i]);
// OR
for(int i=0;i<cap;i++){fa[i]=a[i];fb[i]=b[i];}
for(int i=0;i<n;i++)
for(int j=0;j<cap;j++)
if(j&(1<<i))
{
fa[j]=(fa[j]+fa[j^(1<<i)])%mod;
fb[j]=(fb[j]+fb[j^(1<<i)])%mod;
}
for(int i=0;i<cap;i++)fc[i]=1LL*fa[i]*fb[i]%mod;
for(int i=n-1;i>=0;i--)
for(int j=0;j<cap;j++)
if(j&(1<<i))
fc[j]=(fc[j]-fc[j^(1<<i)]+mod)%mod;
for(int i=0;i<cap;i++)printf("%d ",fc[i]);
puts("");
// AND
for(int i=0;i<cap;i++){fa[i]=a[i];fb[i]=b[i];}
for(int i=0;i<n;i++)
for(int j=0;j<cap;j++)
if(!(j&(1<<i)))
{
fa[j]=(fa[j]+fa[j^(1<<i)])%mod;
fb[j]=(fb[j]+fb[j^(1<<i)])%mod;
}
for(int i=0;i<cap;i++)fc[i]=1LL*fa[i]*fb[i]%mod;
for(int i=n-1;i>=0;i--)
for(int j=0;j<cap;j++)
if(!(j&(1<<i)))
fc[j]=(fc[j]-fc[j^(1<<i)]+mod)%mod;
for(int i=0;i<cap;i++)printf("%d ",fc[i]);
puts("");
// XOR
for(int i=0;i<cap;i++){fa[i]=a[i];fb[i]=b[i];}
for(int i=0;i<n;i++)
for(int j=0;j<cap;j++)
if(j&(1<<i))
{
int t=fa[j];
fa[j]=(fa[j]+fa[j^(1<<i)])%mod;
fa[j^(1<<i)]=(fa[j^(1<<i)]-t+mod)%mod;
t=fb[j];
fb[j]=(fb[j]+fb[j^(1<<i)])%mod;
fb[j^(1<<i)]=(fb[j^(1<<i)]-t+mod)%mod;
}
for(int i=0;i<cap;i++)fc[i]=1LL*fa[i]*fb[i]%mod;
for(int i=n-1;i>=0;i--)
for(int j=0;j<cap;j++)
if(j&(1<<i))
{
int t=fc[j],tt=fc[j^(1<<i)];
fc[j]=1LL*(t-tt+mod)*i2%mod;
fc[j^(1<<i)]=1LL*(t+tt)*i2%mod;
}
for(int i=0;i<cap;i++)printf("%d ",fc[i]);
puts("");
}

浙公网安备 33010602011771号