FWT 从入门到拍案叫绝
二进制卷积太美妙了。
高维前缀和
本质上是 OR 卷积。
https://www.cnblogs.com/heyuhhh/p/11585358.html
这个写得挺好。我觉得应该还算是很好理解吧?
位运算卷积
快速沃尔什变换 (FWT)
令 \(C_i=\sum\limits_{j\oplus k}A_jB_k\),则 \(C\) 是 \(A,B\) 的位运算 \(\oplus\) 的卷积。
我们希望对 \(A,B,C\) 进行 FWT 取得 \(f(A),f(B),f(C)\),然后点积计算 \(f(C)=f(A)\cdot f(B)\),再对 \(f(C)\) 进行一次 IFWT 取得 \(C\) 以控制时间复杂度。
考虑构造 FWT 的变换方案。令 \(c(i,j)\) 表示 \(A_j\) 对 \(f(A)_ i\) 的贡献系数那么有 \(f(A)_ i=\sum\limits_ {i=0}^{n-1} c(i,j)A_j\)。当然绝大多数 FWT 题都满足 \(n\) 是 \(2\) 的幂次,否则不方便我们之后的操作。不过如果真的 \(n\neq 2^k\),其实在后面补足 \(0\) 就行了。
然后进行一个狠狠的等价变换:
对左边根据定义式进行一个代换:\(C_ i=\sum\limits_{p\oplus q=i} A_pB_q\),那么有 \(f(C)_ i=\sum\limits_{j=0}^{n-1} c(i,j)\sum\limits_{p\oplus q=j} A_pB_q\)。
回代,等价变换。鉴于 \(n\) 总是等于 \(2^k\),可以再更换枚举方式:
统一字母,发现得到关系 \(c(i,j)c(i,k)=c(i,j\oplus k)\)。利用真值表和高中数学抽象函数求值的方法可以取得 \(c([0,1],[0,1])\) 也就是 \(c\) 函数的位矩阵。
为了方便之后分治卷积的操作并满足上述关系,我们考虑让 \(c(i,j)\) 在 \(i>1\lor j>1\) 时每一位都独立开。令 \(i_t\) 表示 \(i\) 二进制下第 \(t\) 位,\(j_t\) 表示 \(j\) 二进制下第 \(t\) 位,我们使 \(c(i,j)=\prod c(i_t,j_t)\)。回代可以发现,由于位矩阵早已满足关系,所以对于每个 \(t\) 都有 \(c(i_t,j_t)c(i_t,k_t)=c(i_t,j_t\oplus k_t)\),再把每一位合并在一起从而得到与 \(c(i,j)c(i,k)=c(i,j\oplus k)\) 等价。
假设我们已经取得了所有 \(c\),下一步就是猛算 \(f(A)_i=\sum\limits_{j=0}^{n-1} c(i,j)A_j\),当然这还是 \(\mathcal O(n^2)\) 的。
鉴于 \(n\) 总是等于 \(2^k\),考虑按二进制下最高位的值分治,再利用 \(c\) 每一位的独立性拆分进行变换。记 \(i_0\) 为 \(i\) 的最高位:
考虑 \(A_0\) 表示前半部分的 \(A_j\) 组成的数列,\(A_1\) 表示后半部分的 \(A_j\) 组成的数列(注意因为抹掉了最高位所以下标也是从 \(0\) 而不是 \(\frac{n}{2}\) 开始),那么分类之后有:
- \(i_0=0\Rightarrow i'=i\),有 \(j'=j\Rightarrow f(A)_ i=c(0,0)f(A_0)_ i+c(0,1)f(A_1)_ i\)。
- \(i_0=1\Rightarrow i'=i-\frac{n}{2}\),有 \(f(A)_ {i+\frac{n}{2}}=c(1,0)f(A_0)_ i+c(1,1)f(A_1)_ i\)。
当然,\(i\in[0,\frac{n}{2})\)。
很明显这里问题规模减半了,内部可以分治下去求。
显然可以使用 \(\mathcal O(n)\) 的代价合并两个问题,于是做到 \(\mathcal O(n\log n)\)。分治边界处取 \(f(A)_ i=A_i\)。
求出 \(f(A)\) 和 \(f(B)\) 后进行点积得到 \(f(C)\)。
接下来考虑 IFWT,把 \(f(C)\) 反演回去。
快速沃尔什逆变换 (IFWT)
对位矩阵 \(c\) 进行肉眼矩阵求逆,得到一个新的位矩阵,这个矩阵就是 IFWT 的分治系数,其他部分与 FWT 没有区别。这也在构造的时候提出了严苛的要求:矩阵必须要有逆。一个有逆的矩阵至少需要满足不存在某一行或某一列全是 \(0\)。
肉眼矩阵求逆可以从 \(0\) 的位置入手,必要时可能需要列出方程。
对 \(f(C)\) 进行 IFWT 后得到的就是 \(C\) 的原数组了。
两条性质
FWT 本质上是线性变换,所以有 \(f(A+B)=f(A)+f(B)\) 和 \(f(cA)=cf(A)\)(这里是点积)。
写法
首先整个东西都需要一个这样的外壳:
for(int p=2,q=1;p<=n;p<<=1,q<<=1)
for(int i=0;i<n;i+=p)
显然这其实就是在循环从下往上进行分治合并。每次我们只需合并 \([i,i+q)\) 和 \([i+q,p)\) 的 FWT/IFWT 结果,合并好之后放到 \([i,p)\) 供下次使用。
讲一下或卷积。
或卷积的位矩阵是 \(\left [ \begin{matrix} 1 & 0 \\ 1 & 1\\ \end{matrix} \right ]\),注意到每一层的 \(n\) 都总是二的次幂,所以:
- 对于前一半 \(i\in[0,\frac{n}{2})\),有 \(f(A_i)=\operatorname{merge}(c(0,0)f(A_0),c(0,1)f(A_1))\)。
- 对于后一半 \(i\in[\frac{n}{2},n)\),有 \(f(A_i)=\operatorname{merge}(c(1,0)f(A_0),c(1,1)f(A_1))\)。
因为 \(c(0,1)=0\),所以我们可以直接继承前一半,然后后一半加上前一半。
也就是写成这样:
void FOR(int *A){
for(int p=2,q=1;p<=n;p<<=1,q<<=1)
for(int i=0;i<n;i+=p)
for(int j=i+q;j<=i+p-1;j++)
add(A[j],A[j-q]);
}
它的逆矩阵是 \(\left [ \begin{matrix} 1 & 0 \\ -1 & 1\\ \end{matrix} \right ]\),显然也是左半边不动,右一半减掉左一半,那么逆变换就是:
void IFOR(int *A){
for(int p=2,q=1;p<=n;p<<=1,q<<=1)
for(int i=0;i<n;i+=p)
for(int j=i+q;j<=i+p-1;j++)
des(A[j],A[j-q]);
}
于是就做完了。
实际上我们根本不用构造位矩阵,直接暴力位矩阵就行了。