FWT-快速沃尔什变换
FWT
这大概是十分基础的知识,然而我在模拟测中挂了 \(INF\) 次后终于不咕了
我们要求
\(\operatorname{qwq}\) 是某种二元运算,在 \(FFT\) 中,\(\operatorname{qwq}\) 对应的是加法,而在 \(FWT\) 中,\(\operatorname{qwq}\) 通常表示 \(\operatorname{bitor},\operatorname{bitand},\operatorname{xor}\) 中的一个
那么我们当然可以沿用 \(FFT\) 中的思路,我们把 \(a\) 变成 \(fwt_a\),然后可以直接和 \(fwt_b\) 对应项相乘得出 \(fwt_c\),最后在逆变换回去就得出了 \(c\)
为了方便描述,我们用 \(\cup\) 表示序列的拼接,比如 \(\{1,2,3\}\cup\{ 2,3\}=\{1,2,3,2,3\}\)
结论
\(\operatorname{bitor}\)
直接构造 \(f[a]_i=\sum_{j|i=i}a_j\) 为 \(a\) 在 \(\operatorname{bitor}\) 意义下的 \(FWT\) 数组
然后发现直接对应点乘是对的
不难发现 \(f=f[0]\cup (f[0]+f[1])\)
其中,\(f[0]\) 表示下标最高位为 \(0\) 的序列,\(f[1]\) 表示下标最高位为 \(1\) 的序列
逆变换也一样,
于是可以写出来
for(int o=2,k=1;o<=n;o<<=1,k<<=1)
for(int i=0;i<n;i+=o)
for(int j=0;j<k;++j)
f[i+j+k]+=f[i+j]*x;
\(x=\pm 1\) 分别表示正反变换
因为 \(FWT\) 都是基于奇偶分段的,递归的写法也都差不多,可以简记为 \(\operatorname{fwt\_for}\)
就像这样
#define fwt_for(n) for(int o=2,k=1;o<=n;o<<=1,k<<=1) for(int i=0;i<n;i+=o) for(int j=0;j<k;++j)
可以简略地写为
fwt_for(n) f[i+j+k]+=f[i+j]*x;
\(\operatorname{bitand}\)
不难发现这就是 \(\operatorname{bitor}\) 反过来,式子两边也反过来就可以了
fwt_for(n) f[i+j]+=f[i+j+k]*x;
\(\operatorname{xor}\)
异或稍微困难一些,逆变换是应该使用 \(x=\frac 12\)
这里直接给出结论之后再推导
fwt_for(n)
f[i+j]+=f[i+j+k],
f[i+j+k]=f[i+j]-f[i+j+k]*2,
f[i+j]*=x,f[i+j+k]*=x;
如果长于压行的话完全可以写在一排
推导
思路都是一样的,我们进行奇偶分段
考虑下标最高位的为 \(0\) 的为 \(a_0,b_0,c_0\),为 \(1\) 的为 \(a_1,b_1,c_1\)
以 \(\operatorname{xor}\) 为例,我们容易知道
为了方便理解,我们在这里放出另外两种位运算的结果
直接这样分治是 \(O(n^2)\) 的
考虑高精时一个 \(naive\) 的想法
通过变换,我们减少了一次乘法(如果再减少就是 \(FFT\) 了)
同理,我们考虑
于是
然后就是 \(O(n\log n)\) 的了
子集卷积
我们常常遇到这样一个问题:
如果我们暴力枚举每个子集,那么复杂度是 \(O(3^n)\) 的(\(n=\log S_{\operatorname{max}}\))
有了 \(FWT\),我们可以把复杂度优化到 \(O(n^22^n)\)
对于 \(i\operatorname{bitor}j=S\) 的约束我们直接做一个 \(\operatorname{bitor}\) 的 \(FWT\) 就好了
对于 \(i\operatorname{bitand}j=0\) 的约束,感性理解一下就是两个集合不交,那么必然有
我们只需要把原本的 \(A_i\) 表示为 \(0,0\dots A_{\operatorname{popcount}(i),i}\dots0,0\)
也就是把原本的序列拆为 \(n\) 个序列,原本序列上的值 \(A_i\) 只在第 \(\operatorname{popcount}(i)\) 个序列上存储,然后我们发现直接枚举就可以了
for(int i=0;i<=n;++i)
for(int j=0;j<=i;++j)
for(int k=0;k<(1<<n);++k)
c[i][k]+=a[j][k]*b[i-j][k];
注意一个细节,求解 \(\operatorname{popcount}\) 有一个显然的 \(O(n)\) 的做法,而更优的写法较为复杂,所以在实际运行中我们采用预处理 \(2^k\) 个 \(\operatorname{popcount}\) 值,然后分段直接查,可以做到 \(O(1)\),在实际测试中吊打理论上 \(O(\alpha(n))\) 的做法

浙公网安备 33010602011771号