【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)

2025.2.7 添加内容

参考文献:题解 P4717 【【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)】 - 洛谷专栏

下文不区分 FWT 和 FMT 而统一称作 FWT。

题目描述

有两个长度为 \(2^n\) 的数组 \(a, b\),你需要求出一个长度为 \(2^n\) 的数组 \(c\) 使得:

\[c_k=\sum_{i\oplus j=k}a_ib_j \]

其中 \(\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\) 运算。则

\[f(a, b)\otimes c=f(a\otimes c, b\otimes c) \]

这是因为

\[\begin{aligned} (f(a, b)\otimes c)_k&=\sum_{i\oplus j=k}f(a, b)_ic_j=\sum_{i\oplus j=k}f(a_i, b_i)c_j=\sum_{i\oplus j=k}f(a_ic_j, b_ic_j)\\ &=\sum_{i\oplus j=k}(Aa_ic_j+Bb_ic_j)=A\sum_{i\oplus j=k}a_ic_j+B\sum_{i\oplus j=k}b_ic_j\\ &=f(a\otimes c, b\otimes c)_k \end{aligned} \]

三种卷积

按位异或卷积

\(c=a\otimes_{\text{xor}} b\),表示 \(\text{xor}\) 卷积。

\(a,b,c\) 按照二进制最高位奇偶分段为 \(a_0,a_1,b_0,b_1,c_0,c_1\)

则有:

\[\begin{aligned} c_0&=(a_0\otimes_{\text{xor}}b_0)+(a_1\otimes_{\text{xor}}b_1)\\ c_1&=(a_0\otimes_{\text{xor}}b_1)+(a_1\otimes_{\text{xor}}b_0) \end{aligned} \]

观察到:

\[\begin{aligned} (a_0+a_1)\otimes_{\text{xor}}(b_0+b_1)&=a_0\otimes_{\text{xor}}b_0+a_1\otimes_{\text{xor}}b_1+a_0\otimes_{\text{xor}}b_1+a_1\otimes_{\text{xor}}b_0\\ (a_0-a_1)\otimes_{\text{xor}}(b_0-b_1)&=a_0\otimes_{\text{xor}}b_0+a_1\otimes_{\text{xor}}b_1-a_0\otimes_{\text{xor}}b_1-a_1\otimes_{\text{xor}}b_0 \end{aligned} \]

记这两个为 \(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)\)

所以:

\[\begin{matrix} &a_0 &a_1 &b_0 &b_1\\ &\Downarrow &\Downarrow &\Downarrow &\Downarrow \\ &a_0+a_1 &a_0-a_1 &b_0+b_1 &b_0-b_1 \end{matrix} \]

相当于我们篡改了我们要卷的东西,然后计算答案的时候改回去变成对的。假设继续递归算了 \(d_0,d_1\),则回溯时

\[\begin{matrix} &c_0 &c_1\\ &\Uparrow &\Uparrow \\ &\frac{1}{2}(d_0+d_1) &\frac{1}{2}(d_0-d_1) \end{matrix} \]

做完了。实际代码实现会有点鬼畜。

复杂度 \(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}\) 卷积。

我们复读一遍上面的过程:因为

\[\begin{aligned} c_0&=a_0\otimes_{\text{or}}b_0\\ c_1&=a_0\otimes_{\text{or}}b_1+a_1\otimes_{\text{or}}b_0+a_1\otimes_{\text{or}}b_1 \end{aligned} \]

施蝴蝶变换:

\[\begin{aligned} a_0\otimes_{\text{or}}b_0&=a_0\otimes_{\text{or}}b_0.\\ (a_0+a_1)\otimes_{\text{or}}(b_0+b_1)&=a_0\otimes_{\text{or}}b_0+a_1\otimes_{\text{or}}b_1+a_0\otimes_{\text{or}}b_1+a_1\otimes_{\text{or}}b_0 \end{aligned} \]

\[\begin{matrix} &a_0 &a_1 &b_0 &b_1\\ &\Downarrow &\Downarrow &\Downarrow &\Downarrow \\ &a_0 &a_0+a_1 &b_0 &b_0+b_1 \end{matrix} \]

\[\begin{matrix} &c_0 &c_1\\ &\Uparrow &\Uparrow \\ &d_0 &d_1-d_0 \end{matrix} \]

点击查看代码
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

按位与卷积

这和按位或卷积是互相对称的,可以将结论和代码反过来。

\[\begin{matrix} &a_0 &a_1 &b_0 &b_1\\ &\Downarrow &\Downarrow &\Downarrow &\Downarrow \\ &a_0+a_1 &a_1 &b_0+b_1 &b_1 \end{matrix} \]

\[\begin{matrix} &c_0 &c_1\\ &\Uparrow &\Uparrow \\ &d_0-d_1 &d_1 \end{matrix} \]

点击查看代码
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

线性性

矩阵形式

我们可以写出:

\[\text{FWT}_{\text{xor}}(a)_S=\sum_{T}(-1)^{|S\cap T|}a_T \]

\[\text{FWT}_{\text{or}}(a)_S=\sum_{T\subseteq S}a_T \]

\[\text{FWT}_{\text{and}}(a)_S=\sum_{T\supseteq S}a_T \]

第一个式子不是很显然,但是你可以看代码发现它们是一样的。关于它们的逆运算(IFWT,也可以称作逆变换),你可以认为,FWT 是一个线性运算,我们将 \(a\) 想象成一个长度为 \(2^n\) 的列向量,这三种 FWT 就是将 \(a\) 左乘了一个矩阵,那么 IFWT 只需要左乘那个矩阵的逆矩阵就行了。以下写出三种 FWT 对应的逆矩阵:

\[\text{IFWT}_{\text{xor}}(a)=2^{-n}\text{FWT}_{\text{xor}}(a) \]

\[\text{IFWT}_{\text{or}}(a)_S=\sum_{T\subseteq S}(-1)^{|S\setminus T|}a_T \]

\[\text{IFWT}_{\text{and}}(a)_S=\sum_{T\supseteq S}(-1)^{|T\setminus S|}a_T \]

对应到代码则更简单,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 - 博客园

我建议你可以看看这篇文章

数学小记 #3:从 CF1103E 浅谈异或卷积 - 洛谷专栏

posted @ 2022-11-16 20:09  caijianhong  阅读(70)  评论(1编辑  收藏  举报