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

2025.2.7 添加内容

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

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

题目描述

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

ck=ij=kaibj

其中 {or,and,xor} 为按位或、按位与、按位异或三种运算之一。

卷积运算律

这三种卷积本身具有交换律、结合律、分配律。

交换律、结合律由 所代表的运算天然提供。

注意什么叫分配律。例如有 ab=c 为我们刚才提到的卷积,那么定义 f(x,y)=Ax+By 其中 A,B 是常数,x,y 为数字,若 x,y 为数组则表示将这两个数组对位分别做 f 运算。则

f(a,b)c=f(ac,bc)

这是因为

(f(a,b)c)k=ij=kf(a,b)icj=ij=kf(ai,bi)cj=ij=kf(aicj,bicj)=ij=k(Aaicj+Bbicj)=Aij=kaicj+Bij=kbicj=f(ac,bc)k

三种卷积

按位异或卷积

c=axorb,表示 xor 卷积。

a,b,c 按照二进制最高位奇偶分段为 a0,a1,b0,b1,c0,c1

则有:

c0=(a0xorb0)+(a1xorb1)c1=(a0xorb1)+(a1xorb0)

观察到:

(a0+a1)xor(b0+b1)=a0xorb0+a1xorb1+a0xorb1+a1xorb0(a0a1)xor(b0b1)=a0xorb0+a1xorb1a0xorb1a1xorb0

记这两个为 d0,d1,我们的思路是算出 d0,d1,然后 c0=12(d0+d1),c1=12(d0d1)

所以:

a0a1b0b1a0+a1a0a1b0+b1b0b1

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

c0c112(d0+d1)12(d0d1)

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

复杂度 O(n2n)

点击查看代码
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=aorb,表示 or 卷积。

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

c0=a0orb0c1=a0orb1+a1orb0+a1orb1

施蝴蝶变换:

a0orb0=a0orb0.(a0+a1)or(b0+b1)=a0orb0+a1orb1+a0orb1+a1orb0

a0a1b0b1a0a0+a1b0b0+b1

c0c1d0d1d0

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

按位与卷积

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

a0a1b0b1a0+a1a1b0+b1b1

c0c1d0d1d1

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

线性性

矩阵形式

我们可以写出:

FWTxor(a)S=T(1)|ST|aT

FWTor(a)S=TSaT

FWTand(a)S=TSaT

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

IFWTxor(a)=2nFWTxor(a)

IFWTor(a)S=TS(1)|ST|aT

IFWTand(a)S=TS(1)|TS|aT

对应到代码则更简单,xor 的情况在最后除掉(注意到 2np12n(modp) 这可以简化运算),or 和 and 的情况由于按位独立性可以将 += 改成 -=

也就是说:FWT 的变换事实上是左乘一个矩阵。矩阵为这种变换带来了线性性。

FWT 是系数转点值

你可以将一个数组 FWT 后的结果进行各种操作,好像把它当作一个 valarray 一样进行运算,最后再对其做 IFWT(也就是 FWT 的逆运算、逆变换)

这是因为 FWT 实际上是将一个集合幂级数转换为 2n点值表示,就像 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);

快速修改系数

有时候我们需要修改原来的集合幂级数的某个系数,那么怎么对应到新的点值表示?非常简单,例如 ai 要加上 b,则你对一个 ai=b,其它位置为 0 的集合幂级数做一次 FWT,然后再加回去到原来的点值表示。这就是线性性的一个应用,这里用到分配律。

按位独立性

按位独立性

FWT 的一个重要性质是:处理每一位的顺序是无关紧要的。这点与 FFT 不同,因为运算都是位运算。

这意味着每一位的位运算操作可以是不同的,有题目考察到了这一点:P5406 [THUPC 2019] 找树 - 洛谷 | 计算机科学教育新生态。做法是每一位分别去做对应的 FWT。

如果输入的 a 数组进行了对位的一个轮换(即置换,例子如 FFT 的蝴蝶变换),那么 FWT(a) 也做同样的轮换就行了。

逆运算也是按位独立的,每一位是否做了 FWT 或者 IFWT 不会影响其它位。

如何单独对第 b 位做 FWT 呢?枚举所有 0i<2n 中满足 i >> b & 1 的,然后拿出 a[i]a[i ^ (1 << b)] 这两项进行操作即可。

提取某一位、添加位、删除位

例如我们现在手上有点值,如何得知原来的系数中奇数项和偶数项是否相等,或者偶数项是否全为零?我们只需要单独对这一位做 IFWT,这样把原数组分成两份,这两份分别就是这一位为 0 的系数对应的点值和这一位为 1 的系数对应的点值。然后就能判断以上这些问题了。由此你可以在 O(2n) 的时间内由点值找出最前的非零系数的位置,我勒个 FWT 二分。

添加位的时候,首先进行位轮换为将要添加的位腾出位置。然后根据添加的这一位是全填 0 还是 1 还是都填去做相应的复制,最后单独对这一位做 FWT。

删除位的时候,单独对这一位做 IFWT,然后选择某种策略将其合并或删除,最后进行位轮换把它移出去。

实际代码不用这么麻烦,可以预先知道“单独对这一位做 FWT”的效果,少写一点东西。

子集卷积、集合幂级数相关问题

【模板】子集卷积 - caijianhong - 博客园

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

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

posted @   caijianhong  阅读(77)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示