Part 1 简介
FFT 本质上是处理加法卷积,即 AiBj 贡献到 Ci+j。而 FWT 则是处理位运算卷积,即 AiBj 贡献到 Ci⋆j,其中 ⋆ 是某种位运算。
FWT 的思想与 FFT 相近,也是创造一种数列到数列的线性变换 FWT(A) 满足它与它的逆变换都可以快速计算且 FWT(A⋆B)=FWT(A)⋅FWT(B),其中 ⋅ 表示两个数列对应位置相乘。
有了这样的线性变换就可以先求出 FWT(A) 和 FWT(A),算出 FWT(A⋆B)=FWT(A)⋅FWT(B),最后求出 C=IFWT(A⋆B)。
本文中假定 ∣A∣=∣B∣=n 且 n 是 2 的整次幂。
Part 2 原理
先来解决 FWT(A) 如何求解,不妨设 FWT(A)i=j=0∑n−1ci,jAj,然后探讨 c 需要满足的条件。那么有:
(FWT(A)⋅FWT(B))iFWT(A⋆B)i=FWT(C)=j=0∑n−1ci,jAjk=0∑n−1ci,kBk=j=0∑n−1k=0∑n−1ci,jci,kAjBk=j=0∑n−1ci,jCj=j=0∑n−1ci,j1≤k,l≤n,k⋆l=j∑AkBl=j=0∑n−1k=0∑n−1ci,j⋆kAjBk
所以有:
ci,jci,k=ci,j⋆k
注意到由于 ⋆ 是位运算,所以不妨钦定 c 也可以拆位处理。令 (a)2=a0a1a2… 即 a 的二进制表示,不妨钦定 c 满足 ci,j=ci0,j0ci1,j1ci2,j2…,这样做的好处是只要知道 c0/1,0/1 就可以求出 ci,j,而且也有:
cil,jlcil,kl=cil,jl⋆kl⇔ci,jci,k=ci,j⋆k
但是暴力求 FWT(A) 是 O(n2) 的,考虑优化,令 a′ 为 a 去掉二进制最高位的数,按位折半即有:
FWT(A)i=j=0∑2n−1ci0,j0ci′,j′Aj+j=2n∑n−1ci0,j0ci′,j′Aj=ci0,0j=0∑2n−1ci′,j′Aj+ci0,1j=2n∑n−1ci′,j′Aj
那么考虑 i0 的取值,有:
FWT(A)i=⎩⎪⎪⎪⎨⎪⎪⎪⎧c0,0j=0∑2n−1ci′,j′Aj+c0,1j=2n∑n−1ci′,j′Ajc1,0j=0∑2n−1ci′,j′Aj+c1,1j=2n∑n−1ci′,j′Aj0≤i≤2n−12n≤i≤n−1
设 A0 为 A 中下标二进制最高位为 0 的部分,A1 为最高位为 1的部分,那么有:
FWT(A)i={c0,0FWT(A0)i+c0,1FWT(A1)ic1,0FWT(A0)i−2n+c1,1FWT(A1)i−2n0≤i≤2n−12n≤i≤n−1
假设 n=2m,则可以在 O(m2m) 即 O(nlogn) 的时间复杂度内求解 FWT(A)。
对于 IFWT(A),只需要构造出 c0/1,0/1 的逆即可。
Part 3 具体实现
根据 ci,jci,k=ci,j⋆k 和 ci0,j0ci1,j1=c2i0+i1,2j0+j1 构造 c0/1,0/1 即可,称其为位矩阵。
构造过程比较人类智慧,注意矩阵必须要有逆,即每一行和每一列都有至少一个位置不为 0 且不能有两行或者两列完全一样,否则就会有维度被丢失(线性代数说法)。
由于不同的位运算的 FWT 本质相同,只是 c 不同,所以不妨设 FWT(A,[c0,0c1,0c0,1c1,1]) 为 A 在对应的 c 意义下的 FWT 结果,那么有 FWT(FWT(A,c),c−1)=A。
3.1 OR 卷积
考虑构造满足 ci,jci,k=ci,j∣k 且存在逆的位矩阵。
c0,0c0,0=c0,0∣0=c0,0⇒c0,0∈{0,1}。
同理,c0/1,0/1∈{0,1}。
由于 c0,0c0,1=c0,1,所以 c0,0=1,c0,1=0 或者 c0,0=1,c0,1=1。
同理,c1,0=1,c1,1=0 或者 c1,0=1,c1,1=1。
那么位矩阵就有两种构造方式:
[1101][1110]
Tips:
观察这个位矩阵:
[1101]
注意到它满足 ci,j=[i&j=j],也就是说这种情况下 FWT(A,c) 实际上相当于子集求和。
这启发我们形如 Bi=i⋆j=i∑Aj 和 Bi=i⋆j=j∑Aj 这样的和式(⋆ 是某种位运算)也可以用 FWT 来快速求。
由于第一个位矩阵满足 ci,j=[i&j=j],所以下面采用第一个位矩阵,则设 c−1=[xzyw],则有:
⎩⎪⎪⎪⎨⎪⎪⎪⎧x+0z=1y+0w=0x+z=0y+w=1
解得:
⎩⎪⎪⎪⎨⎪⎪⎪⎧x=1y=0z=−1w=1
所以 c−1=[1−101]。
3.2 AND 卷积
c0,0c0,0=c0,0&0=c0,0⇒c0,0∈{0,1}。
同理,c0/1,0/1∈{0,1}。
由于 c0,0c0,1=c0,0,所以 c0,0=0,c0,1=1 或 c0,0=1,c0,1=1。
同理,c1,0=0,c1,1=1 或 c1,0=1,c1,1=1。
那么位矩阵就有两种构造方式:
[0111][1011]
由于第一个位矩阵满足 ci,j=[i∣j=2k−1],所以采用第一个位矩阵,同理,待定系数法求逆得 c−1=[−1110]。
3.3 XOR 卷积
由于对于任意的 x,y,均有 c0,0cx,y=cx,y,所以 c0,0=1。
根据 c1,1c1,1=c1,0 且矩阵不存在为 0 的行,所以 c1,0 与 c1,1 均非 0。
根据 c1,0c1,0=c1,0 且 c1,0=0 可得 c1,0=1。
根据,c0,1c0,1=c1,0,可得 c0,1=−1 或 c0,1=1。
同理,c1,1c1,1=c1,0,c1,1=−1 或 c1,1=1。
那么位矩阵就有两种构造方式:
[11−11][111−1]
同样的,由于第二个位矩阵满足 ci,j=(−1)∣i&j∣(∣a∣ 为 a 二进制表示中 1 的个数),所以采用第二个位矩阵,求逆得 c−1=[212121−21]。
3.4 代码实现
直接套
FWT(A)i={c0,0FWT(A0)i+c0,1FWT(A1)ic1,0FWT(A0)i−2n+c1,1FWT(A1)i−2n0≤i≤2n−12n≤i≤n−1
即可,P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT) 代码如下:
#include <iostream>
#include <cstdio>
using namespace std;
const int p=998244353,inv2=499122177;
inline int getlen(int n)
{
int res=1;
while(res<n) res<<=1;
return res;
}
const int ORC[2][2]={{1,0},{1,1}},IORC[2][2]={{1,0},{p-1,1}};
const int ANDC[2][2]={{0,1},{1,1}},IANDC[2][2]={{p-1,1},{1,0}};
const int XORC[2][2]={{1,1},{1,p-1}},IXORC[2][2]={{inv2,inv2},{inv2,p-inv2}};
inline void FWT(int n,int a[],const int c[2][2])
{
for(int len=2;len<=n;len<<=1)
{
int mid=len>>1;
for(int l=0;l<=n-len;l+=len)
{
for(int k=0;k<mid;k++)
{
int x=a[l+k],y=a[l+mid+k];
a[l+k]=(1ll*c[0][0]*x%p+1ll*c[0][1]*y%p)%p;
a[l+mid+k]=(1ll*c[1][0]*x%p+1ll*c[1][1]*y%p)%p;
}
}
}
}
int n;
int a[1<<17],b[1<<17],c[1<<17];
int main()
{
scanf("%d",&n);
n=1<<n;
for(int i=0;i<n;i++) scanf("%d",&a[i]);
for(int i=0;i<n;i++) scanf("%d",&b[i]);
FWT(n,a,ORC),FWT(n,b,ORC);
for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%p;
FWT(n,c,IORC);
for(int i=0;i<n;i++) printf("%d ",c[i]);
printf("\n");
FWT(n,a,IORC),FWT(n,b,IORC);
FWT(n,a,ANDC),FWT(n,b,ANDC);
for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%p;
FWT(n,c,IANDC);
for(int i=0;i<n;i++) printf("%d ",c[i]);
printf("\n");
FWT(n,a,IANDC),FWT(n,b,IANDC);
FWT(n,a,XORC),FWT(n,b,XORC);
for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%p;
FWT(n,c,IXORC);
for(int i=0;i<n;i++) printf("%d ",c[i]);
printf("\n");
return 0;
}
Part 4 更多拓展
有些时候考的往往不是裸的 FWT。
下文中若 A 与 B 为数列,⋆ 为某种位运算,那么 A⋆B 表示 A 与 B 在 ⋆ 运算下的卷积结果,即 (A⋆B)i=j⋆k=i∑AjBk。
FWT 应用时往往要利用它是线性变换来优化,即 FWT(A)+FWT(B)=FWT(A+B) 且 FWT(aA)=aFWT(A)。
若 A 只有少数项非 0 则可能有分类讨论优化时间复杂度的做法。
一些例题:
4.1 离线子集卷积
Ck=i∣j=k,i&j=0∑AiBj=i⊆k∑AiBk−i
发现 i&j=0 很烦,但是不难发现它等价于 ∣i∣+∣j∣=∣k∣(∣a∣ 表示 a 二进制表示中的 1 的个数),所以可以令 SAi,j=[∣j∣=i]Aj,SBi,j=[∣j∣=i]Bj,那么有:
Ri=j=0∑iSAj∣SBi−j
由于 FWT 是线性变换,所以有:
Ri=IFWT(j=0∑iFWT(SAj)⋅FWT(SBi−j))
答案即为 R∣i∣,i,时间复杂度 O(m22m),参考代码:(P6097 【模板】子集卷积)
int main()
{
scanf("%d",&n);
n=1<<n;
for(int i=0;i<n;i++) scanf("%d",&a[i]);
for(int i=0;i<n;i++) scanf("%d",&b[i]);
for(int i=0;i<(1<<20);i++) for(int j=0;j<20;j++) popc[i]+=i>>j&1;
for(int i=0;i<=20;i++) for(int j=0;j<n;j++) sa[i][j]=(popc[j]==i)*a[j],sb[i][j]=(popc[j]==i)*b[j];
for(int i=0;i<=20;i++) FWT(n,sa[i],ORC),FWT(n,sb[i],ORC);
for(int i=0;i<=20;i++) for(int j=0;j<=i;j++) for(int k=0;k<(1<<20);k++) r[i][k]=(r[i][k]+1ll*sa[j][k]*sb[i-j][k]%p)%p;
for(int i=0;i<=20;i++) FWT(n,r[i],IORC);
for(int i=0;i<n;i++) printf("%d ",r[popc[i]][i]);
printf("\n");
return 0;
}
4.2 半在线子集卷积
Ck=Bki∣j=k,i&j=0,i=k∑CiAj=Bki⊂k∑CiAk−i
和离线子集卷积类似,令 SAi,j=[∣j∣=i]Aj,SCi,j=[∣j∣=i]Cj,那么有:
SCi=B⋅k=0∑i−1SCk∣SAi−k=B⋅IFWT(j=0∑iFWT(SCj)⋅FWT(SAi−j))
那么从小到大枚举 i 计算即可。
4.3 每一位运算法则不同
给定一个长 logn 的字符串,字符集为 |&^
,表示每一位要进行的位运算。
依旧是考虑:
FWT(A)i={c0,0FWT(A0)i+c0,1FWT(A1)ic1,0FWT(A0)i−2n+c1,1FWT(A1)i−2n0≤i≤2n−12n≤i≤n−1
只不过此时 c 取字符串第 logn 位对应运算的那个矩阵。
inline void FWT(int n,int a[],int w)
{
for(int len=2,pos=1;len<=n;len<<=1,pos++)
{
int mid=len>>1;
int c[2][2];
if(w==1)
{
if(str[pos]=='|') memcpy(c,ORC,sizeof(ORC));
if(str[pos]=='&') memcpy(c,ANDC,sizeof(ANDC));
if(str[pos]=='^') memcpy(c,XORC,sizeof(XORC));
}
else
{
if(str[pos]=='|') memcpy(c,IORC,sizeof(IORC));
if(str[pos]=='&') memcpy(c,IANDC,sizeof(IANDC));
if(str[pos]=='^') memcpy(c,IXORC,sizeof(IXORC));
}
for(int l=0;l<=n-len;l+=len)
{
for(int k=0;k<mid;k++)
{
int x=a[l+k],y=a[l+mid+k];
a[l+k]=(1ll*c[0][0]*x%p+1ll*c[0][1]*y%p)%p;
a[l+mid+k]=(1ll*c[1][0]*x%p+1ll*c[1][1]*y%p)%p;
}
}
}
}