FWT(快速沃尔什变换)
FWT(快速沃尔什变换)
前言
萌新刚学多项式 1ms,有误或者不严谨指出欢迎指出,感谢大佬!
update 2025.10.25 这是我,第三次学习 FWT 了吧。应该这次能学会吧?修改了之前蒙蒙的时候写的东西。
参考
简介
使用类似 FFT 的方法求位运算卷积。
我们熟知的多项式卷积是求 \(\sum_{i=1}^n a_i b_{n-i}\)。
对于位运算 \(\oplus\),可以是 or,and,xor。其卷积大概是 \(\sum_{i=1}^n \sum_{i \oplus j = n} a_i b_j\)。
比如或卷积是 \(\sum_{i=1}^n \sum_{i|j = n} a_i b_j\),也就是我们常说的枚举子集。
对于 \(c_n = \sum_{i=1}^n \sum_{i \oplus j = n} a_i b_j\),我们要求出 \(c_n\) 或者 \(\{c_i\}\),最朴素的暴力枚举 \(i,j\) 时间复杂度是 \(O(n^2)\) 的。考虑使用 FWT 优化到 \(O(n \log n)\)。
算法流程
引入
记对序列 \(A=\{a_i\}\) 进行 FWT 变换后的序列(形式幂级数)是 \(fwt(A)\)。
\(A,B,C\) 均指序列,\(a_i\) 之类指序列的第 \(i\) 项(从 \(0\) 开始记下标)。\(fwt(A)\) 之类是序列,\(fwt(A)_i\) 之类是指序列的第 \(i\) 项(同样从 \(0\) 开始记下标)。
我们要构造 \(A \oplus B = C \Longleftrightarrow fwt(A) \cdot fwt(B) = fwt(C)\)。需要在 \(O(n \log n)\) 的时间复杂度内计算 \(A \to fwt(A)\) 和 \(fwt(A)\to A\),和线性求出 \(fwt(A) \cdot fwt(B) = fwt(C)\)。
可以类似 FFT,将 \(A,B,C\) 理解为多项式(系数),那么 \(fwt(A),fwt(B),fwt(C)\) 就是点值。
其中 \(A \oplus B = C\) 指 \(c_x = \sum_{i \oplus j = x} a_i b_j\),即位运算卷积。
\(fwt(A) \cdot fwt(B) = fwt(C)\) 指 \(fwt(C)_i = fwt(A)_i fwt(B)_i\),即点值相乘。
设 \([i,j]\) 表示 \(a_j\) 是否对 \(fwt(A)_i\) 贡献。有 \([i,j] \in \{0,1\}\)?(其实不一定)
即 \(fwt(A)_i=\sum_{j=0}^{n-1} [i,j] a_j\)。
这个操作就类似于将 \(i\) 代入 \(A\) 得到的点值就是对应项的值乘以对应项的贡献系数相加。因为是位运算,所以贡献系数只有 \(0,1\)(其实不一定),而不是像一般多项式是 \(x^k\)。
求 FWT
我们需要知道 \([i,j]\) 是什么,即如何构造 \(fwt(A)\)。
由于 \(A \oplus B = C\),我们有 \(c_n = \sum_{i \oplus j = i} a_i b_j\)。
因此我们只需要保证对于所有的 \([i,p]=1\) 当且仅当 \(j \oplus k = p\) 的时候 \([i,j]=[i,k]=1\)。
因为是位运算,我们只需要分别考虑每一位,然后把每一位的贡献系数乘起来。即考虑 \([0,0],[0,1],[1,0],[1,1]\) 的取值。每种位运算都是不同的取值。
即(\(i_d,j_d\) 表示 \(i,j\) 二进制下从高数第 \(d\) 位):
如何快速根据 \(A\) 求 \(fwt(A)\)?
直接算是 \(O(n^2)\) 的。
考虑按照 \(j\) 的最高位分开算。其中 \(i_0\) 指 \(i\) 从高数的第 \(0\) 位。\(i',j'\) 表示原数去掉最高位(含前导零)。\(A_0,A_1\) 表示序列 \(A\) 的前/后半部分。
注意,\(A_0,A_1\) 的长度均为 \(\frac n 2\)。下标从 \(0\) 开始。
整理可得:
这样我们就通过 \(O(n)\) 的合并复杂度把求 \(fwt(A)_i,i\in[0,n)\) 变成求 \(fwt(A_{0/1})_i,i\in [0,\frac{n}{2})\) 了。
这种合并一共要做 \(\log n\) 层,因此总时间复杂度是 \(O(n \log n)\)。
求 FWT 逆运算
由点值 \(fwt(A)\) 求插值 \(A\)。
把 \(2 \times 2\) 的矩阵求逆,相当于贡献系数求逆,其他步骤和上面一样。
注意我们构造的矩阵要有逆。
因此上面说的 \([i,j]\) 的取值不一定是 \([0,1]\)。
下面用线性代数证明为什么是对的。但是我并不会线代,所以只能感性理解一下了
写作矩阵形式,会更容易理解:
记前面那个矩阵为 \(M\)。逆 FWT 就是用 \(M^{-1}\) 乘上 \(fwt(A)\) 序列。
对于异或卷积,\(M = M^T\)。
异或卷据的变换矩阵具有准正交性,即满足 \(M^T \cdot M = nI\) 的矩阵。(\(n\) 是序列长度,需为 \(2\) 的幂)
至于为什么有这个性质我不知道。
所以 \(M^{-1} = \frac 1 n M^T\)。
对于与、或、异或卷积,也可以直接求 \(M^T\)。
因为 FWT 的性质,所以对于这些卷积,将子矩阵
取逆,刚好就可以。
至于为什么刚好我不知道。
其中异或卷积的变换矩阵的子矩阵取逆后每个数刚好是原矩阵的 \(\frac12\)。
code
不能理解,不是很好背诵。又学不会,又背不会,真是文理兼废,怎么办呢?
大概理解了,希望过两天还可以看懂。
当然,这是之前学的时候发的牢骚了
函数:
void calc() { rep(i,0,n-1) _mul(a[i],b[i]); }
void OR(int *f,int x=1) {
for(int o=2,k=1; o<=n; o<<=1,k<<=1)
for(int i=0; i<n; i+=o)
rep(j,0,k-1) _add(f[i+j+k],mul(f[i+j],x));
}
void AND(int *f,int x=1) {
for(int o=2,k=1; o<=n; o<<=1,k<<=1)
for(int i=0; i<n; i+=o)
rep(j,0,k-1) _add(f[i+j],mul(f[i+j+k],x));
}
void XOR(int *f,int x=1) {
for(int o=2,k=1; o<=n; o<<=1,k<<=1)
for(int i=0; i<n; i+=o)
rep(j,0,k-1)
_add(f[i+j],f[i+j+k]), f[i+j+k]=add(f[i+j],mod-add(f[i+j+k],f[i+j+k])), _mul(f[i+j],x), _mul(f[i+j+k],x);
}
调用方式:
OR(a), OR(b), calc(), OR(a,mod-1);
AND(a), AND(b), calc(), AND(a,mod-1);
XOR(a), XOR(b), calc(), XOR(a,ksm(2));
拓展
FWT 的每一位可以任意顺序,还可以每一位使用不同的位运算。
但是我好像没有完全理解为什么。
本文来自博客园,作者:wing_heart,转载请注明原文链接:https://www.cnblogs.com/wingheart/p/18644476

浙公网安备 33010602011771号