【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)
2025.2.7 添加内容
参考文献:题解 P4717 【【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)】 - 洛谷专栏
下文不区分 FWT 和 FMT 而统一称作 FWT。
题目描述
有两个长度为 \(2^n\) 的数组 \(a, b\),你需要求出一个长度为 \(2^n\) 的数组 \(c\) 使得:
其中 \(\oplus\in\{\text{or},\text{and},\text{xor}\}\) 为按位或、按位与、按位异或三种运算之一。
卷积运算律
这三种卷积本身具有交换律、结合律、分配律。
交换律、结合律由 \(\oplus\) 所代表的运算天然提供。
注意什么叫分配律。例如有 \(a\otimes b=c\) 为我们刚才提到的卷积,那么定义 \(f(x, y)=Ax+By\) 其中 \(A, B\) 是常数,\(x, y\) 为数字,若 \(x, y\) 为数组则表示将这两个数组对位分别做 \(f\) 运算。则
这是因为
三种卷积
按位异或卷积
令 \(c=a\otimes_{\text{xor}} b\),表示 \(\text{xor}\) 卷积。
将 \(a,b,c\) 按照二进制最高位奇偶分段为 \(a_0,a_1,b_0,b_1,c_0,c_1\)。
则有:
观察到:
记这两个为 \(d_0,d_1\),我们的思路是算出 \(d_0,d_1\),然后 \(c_0=\frac{1}{2}(d_0+d_1),c_1=\frac{1}{2}(d_0-d_1)\)。
所以:
相当于我们篡改了我们要卷的东西,然后计算答案的时候改回去变成对的。假设继续递归算了 \(d_0,d_1\),则回溯时
做完了。实际代码实现会有点鬼畜。
复杂度 \(O(n2^n)\)。
点击查看代码
const int P=998244353;
void red(LL&x){x=(x%P+P)%P;}
void fwt_xor(LL *f,int n,int op){
for(int len=2,k=1;len<=n;len<<=1,k<<=1){
for(int i=0;i<n;i+=len){
for(int j=0;j<k;j++){
LL ta=f[i+j],tb=f[i+j+k];
red(f[i+j]=(ta+tb)*op);
red(f[i+j+k]=(ta-tb)*op);
}
}
}
}
fwt_xor(a,n,1),fwt_xor(b,n,1),multiple(a,b,c,n),fwt_xor(c,n,499122177);
//multiple 是逐位相乘,499122177 是 2 的逆元
void fwt(mint f[], int n) {
for (int k = 1, len = 2; len <= n; len <<= 1, k <<= 1) {
for (int i = 0; i < n; i += len) {
for (int j = 0; j < k; j++) {
auto x = f[i + j], y = f[i + j + k];
f[i + j] = x + y, f[i + j + k] = x - y;
}
}
}
}
// 逆变换在最后需要对所有数除以 2^n,或者在每个 x+y 和 x-y 处都除以 2
按位或卷积
令 \(c=a\otimes_{\text{or}} b\),表示 \(\text{or}\) 卷积。
我们复读一遍上面的过程:因为
施蝴蝶变换:
点击查看代码
void fwt_or(LL *f,int n,int op){
for(int len=2,k=1;len<=n;len<<=1,k<<=1){
for(int i=0;i<n;i+=len){
for(int j=0;j<k;j++)
red(f[i+j+k]+=f[i+j]*op);
}
}
}
fwt_or(a,n,1),fwt_or(b,n,1),multiple(a,b,c,n),fwt_or(c,n,-1);
void fort(mint a[], int n, mint op) {
for (int k = 1, len = 2; len <= n; len <<= 1, k <<= 1) {
for (int i = 0; i < n; i += len) {
for (int j = 0; j < k; j++) {
a[i + j + k] += a[i + j] * op;
}
}
}
}
// 正变换传入 op=1,逆变换传入 op=-1
按位与卷积
这和按位或卷积是互相对称的,可以将结论和代码反过来。
点击查看代码
void fwt_and(LL *f,int n,int op){
for(int len=2,k=1;len<=n;len<<=1,k<<=1)
for(int i=0;i<n;i+=len){
for(int j=0;j<k;j++){
red(f[i+j]+=f[i+j+k]*op);
}
}
}
fwt_and(a,n,1),fwt_and(b,n,1),multiple(a,b,c,n),fwt_and(c,n,-1);
void fandt(mint a[], int n, mint op) {
for (int k = 1, len = 2; len <= n; len <<= 1, k <<= 1) {
for (int i = 0; i < n; i += len) {
for (int j = 0; j < k; j++) {
a[i + j] += a[i + j + k] * op;
}
}
}
}
// 正变换传入 op=1,逆变换传入 op=-1
线性性
矩阵形式
我们可以写出:
第一个式子不是很显然,但是你可以看代码发现它们是一样的。关于它们的逆运算(IFWT,也可以称作逆变换),你可以认为,FWT 是一个线性运算,我们将 \(a\) 想象成一个长度为 \(2^n\) 的列向量,这三种 FWT 就是将 \(a\) 左乘了一个矩阵,那么 IFWT 只需要左乘那个矩阵的逆矩阵就行了。以下写出三种 FWT 对应的逆矩阵:
对应到代码则更简单,xor 的情况在最后除掉(注意到 \(2^{-n}\equiv -\frac{p-1}{2^n}\pmod{p}\) 这可以简化运算),or 和 and 的情况由于按位独立性可以将 +=
改成 -=
。
也就是说:FWT 的变换事实上是左乘一个矩阵。矩阵为这种变换带来了线性性。
FWT 是系数转点值
你可以将一个数组 FWT 后的结果进行各种操作,好像把它当作一个 valarray
一样进行运算,最后再对其做 IFWT(也就是 FWT 的逆运算、逆变换)。
这是因为 FWT 实际上是将一个集合幂级数转换为 \(2^n\) 个点值表示,就像 FFT 将系数表示法转换为点值表示法一样。
这是一个例子,我们对 \(a\) 做 xor FWT 后对每个点值开 \(m\) 次幂再 IFWT,也就是自己对自己做了 \(m\) 次异或卷积。
fwt(f,1<<n,1);
for(int i=0;i<1<<n;i++) f[i]=qpow(f[i],m);
fwt(f,1<<n,-1);
快速修改系数
有时候我们需要修改原来的集合幂级数的某个系数,那么怎么对应到新的点值表示?非常简单,例如 \(a_i\) 要加上 \(b\),则你对一个 \(a_i=b\),其它位置为 \(0\) 的集合幂级数做一次 FWT,然后再加回去到原来的点值表示。这就是线性性的一个应用,这里用到分配律。
按位独立性
按位独立性
FWT 的一个重要性质是:处理每一位的顺序是无关紧要的。这点与 FFT 不同,因为运算都是位运算。
这意味着每一位的位运算操作可以是不同的,有题目考察到了这一点:P5406 [THUPC 2019] 找树 - 洛谷 | 计算机科学教育新生态。做法是每一位分别去做对应的 FWT。
如果输入的 \(a\) 数组进行了对位的一个轮换(即置换,例子如 FFT 的蝴蝶变换),那么 \(\text{FWT}(a)\) 也做同样的轮换就行了。
逆运算也是按位独立的,每一位是否做了 FWT 或者 IFWT 不会影响其它位。
如何单独对第 \(b\) 位做 FWT 呢?枚举所有 \(0\leq i<2^n\) 中满足 i >> b & 1
的,然后拿出 a[i]
和 a[i ^ (1 << b)]
这两项进行操作即可。
提取某一位、添加位、删除位
例如我们现在手上有点值,如何得知原来的系数中奇数项和偶数项是否相等,或者偶数项是否全为零?我们只需要单独对这一位做 IFWT,这样把原数组分成两份,这两份分别就是这一位为 \(0\) 的系数对应的点值和这一位为 \(1\) 的系数对应的点值。然后就能判断以上这些问题了。由此你可以在 \(O(2^n)\) 的时间内由点值找出最前的非零系数的位置,我勒个 FWT 二分。
添加位的时候,首先进行位轮换为将要添加的位腾出位置。然后根据添加的这一位是全填 \(0\) 还是 \(1\) 还是都填去做相应的复制,最后单独对这一位做 FWT。
删除位的时候,单独对这一位做 IFWT,然后选择某种策略将其合并或删除,最后进行位轮换把它移出去。
实际代码不用这么麻烦,可以预先知道“单独对这一位做 FWT”的效果,少写一点东西。
子集卷积、集合幂级数相关问题
我建议你可以看看这篇文章
本文来自博客园,作者:caijianhong,转载请注明原文链接:https://www.cnblogs.com/caijianhong/p/template-fwt.html