目录
快速沃尔什变换(FWT)
顾名思义,这是一种对数组的变换,而且和FFT是基本思想相同,即求,“o”是一种卷积,把A、B变换为A‘、B’,使满足
,最后把C‘转换回C。
沃尔什变换主要有三种:
按位或卷积
,(数组长度n默认为2的次幂)求C数组。
“浅谈”,我们就直接上公式好了。
该变换为,
是按位与,显然有:
根据定义知道,暴力求该变换是的,怎么优化呢?
对于一个数组A,其下标为(0~n-1),,那么数组前一半的下标为(二进制)
,后一半下标为
,那么对于
和
(
),假设前者是所有下标为
的数的和,那么后者一定是下标为
、
的数的和(
、
分别代指若干种01序列),
规范地说就是,把A数组割裂为前后两半,(0~n/2-1)部分构成数组A0,(n/2~n-1)部分构成数组A1(在A1中的下标变为(0~n/2-1)),那么(
),
(
),
所以我们得到了该算法的核心:求出和
,然后遍历求出
和
,进而求得
,
这是递推打法,类比FFT和NTT:
inline void FWTOR(int*a,int n){
for(int k=2;k<=n;k<<=1)//从下往上合并
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a[j+(k>>1)]=(a[j+(k>>1)]+a[j])%MOD;//FWT(A)(i+n/2)=FWT(A0)(i)+FWT(A1)(i)
//FWT(A)(i)=FWT(A0)(i),此时直接不管它
}
简要解释一下,原本递归到最底层后会有n个长度为1的数组,而长度为1的数组的FWT就是它本身,所以由下往上合并为长度为2、长度为4、一直到长度为n的数组,就是要求的FWT(A)。
那么逆变换怎么搞呢?
考虑倒着做整个过程,,
(
),
所以,
,然后从上往下把它倒着转换回去:
inline void IFWTOR(int*a,int n){
for(int k=n;k>1;k>>=1)//从上往下倒推
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a[j+(k>>1)]=(a[j+(k>>1)]-a[j]+MOD)%MOD;
}
按位与卷积
,求C数组。
和按位或很相似,相信读者们自己就能轻松推导,
该变换为:,同样的推导:
同样地,根据该变换的性质,把A分裂为A0和A1,有,
(
),
倒着做的时候:,
inline void FWTAND(int*a,int n){
for(int k=2;k<=n;k<<=1)
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a[j]=(a[j]+a[j+(k>>1)])%MOD;
}
inline void IFWTAND(int*a,int n){
for(int k=n;k>1;k>>=1)
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a[j]=(a[j]-a[j+(k>>1)]+MOD)%MOD;
}
按位异或卷积
(
是按位异或),求C数组。
这个就不好搞了,一般人很难想出来,但是沃尔什大佬不一样,他想出了如下变换:
,其中
表示x的二进制中1的个数取模2,也就是1的个数的奇偶性,
这个公式要倒着证:
观察上式,发现结构是00+11-01-10,而0^0=1^1=0,0^1=1^0=1;
然后有个结论:
所以原式可以继续变换:
至此,我们发现它满足了沃尔什变换的性质😮!
第二步,用分治优化它,
我们故技重施,把A分成前一半A0和后一半A1;
已知任给,
,有
所以容易得到:,
逆推式:,
inline void FWTXOR(int*a,int n){
for(int k=2;k<=n;k<<=1)
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a0=a[j],a1=a[j+(k>>1)],a[j]=(a0+a1)%MOD,a[j+(k>>1)]=(a0-a1+MOD)%MOD;
}
//此处应有个快速幂
inline void IFWTXOR(int*a,int n){
ll in2=ksm(2,MOD-2,MOD),a0,a1;//in2为2的逆元
for(int k=n;k>1;k>>=1)
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a0=a[j],a1=a[j+(k>>1)],a[j]=(a0+a1)%MOD*in2%MOD,a[j+(k>>1)]=(a0-a1+MOD)%MOD*in2%MOD;
}
一点性质/优化
一点很常用的性质:k的枚举顺序可以任意调换。
具体来说,每个代码(包括每个逆变换)循环的开头一行在“for(int k=2;k<=n;k<<=1)”和"for(int k=n;k>1;k>>=1)"之间互换,得到的数列完全相同。
笔者手打了好多草稿,发现这个性质单用FWT的思想真的不方便证,就只好举个例子感性说明一下:
给定数组A:
A: a b c d
进行一次FWTXOR变换,k从小到大:
A: a b c d
↓
A: a+b a-b c+d c-d
↓
A: a+b+c+d a-b+c-d a+b-c-d a-b-c+d
k从大到小:
A: a b c d
↓
A: a+c b+d a-c b-d
↓
A: a+b+c+d a-b+c-d a+b-c-d a-b-c+d
结果完全一样,OR变换和AND变换也一样。
读者们可以自行造数据测试,或者学学巨佬们推一下(我菜不行
有了这个性质,我们就可以统一正变换和逆变换的枚举顺序,然后把他们写在一个函数里:
//所有函数中传入inv=1表示正变换,-1表示逆变换
inline void FWTOR(int*a,int n,int inv){ //按位或
for(int k=2;k<=n;k<<=1)//k的枚举从大到小和从小到大,结果是完全一样的
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a[j+(k>>1)]=(a[j+(k>>1)]+a[j]*inv+MOD)%MOD;
}
inline void FWTAND(int*a,int n,int inv){ //按位与
for(int k=2;k<=n;k<<=1)
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a[j]=(a[j]+a[j+(k>>1)]*inv+MOD)%MOD;
}
//此处应有个快速幂
inline void FWTXOR(int*a,int n,int inv){ //异或
int in2=inv>0?1:ksm(2,MOD-2,MOD),a0,a1;
for(int k=2;k<=n;k<<=1)
for(int i=0;i<n;i+=k)
for(int j=i;j<i+(k>>1);j++)
a0=a[j],a1=a[j+(k>>1)],a[j]=(a0+a1)%MOD*in2%MOD,a[j+(k>>1)]=(a0-a1+MOD)%MOD*in2%MOD;
}
然后,据某位对计算机原理了如指掌的专业人士说,一般k的枚举顺序从大到小可以比从小到大枚举快几百毫秒😦(某常数大师直呼**,效果我还没试过,读者感兴趣的可以试一试
快速莫比乌斯变换(FMT)
这个变换用处不是很大,几乎就只有代替FWTOR和FWTAND的功能,它的原理是基于DP。
莫比乌斯变换本来有很严谨的定义(好像和什么集合幂级数有关),但是这里我们只根据它的实际用处通俗地讲。
笔者看到有的博客里说FWTOR和FWTAND的本质是FMT,这其实是在众多博客的混淆下搞错了,它们推导原理都不同(分治和DP)
(也有可能是我搞错了,反正我看到的博客中一半支持其本质FMT,一半又持不同观点,教练看了代码也没说什么)
按位或FMT
,和沃尔什变换的按位或除了优化方法之外完全一样,
它的优化方法是设表示 x 在二进制第 i 位以后部分和 j 一样、第 i 位以前部分和 j 进行按位与后等于 x 的
的和,
则显然有
并且(设数组长度)
,
所以调整枚举顺序,把第一个下标省略后,再在原数组上进行DP,就得到该变换的完整过程:
inline void FMTOR(int*a,int n){
for(int k=1;k<n;k<<=1)
for(int i=0;i<n;i++)
if(~i&k)a[i|k]=(a[i|k]+a[i])%MOD;
}
如何进行逆变换呢?
反过来,我们设表示 x 在二进制第 i 位以前部分和 j 一样、第 i 位以后部分和 j 进行按位与后等于 x 的
的和,
则可以得到
设数组长度,有
,
一样地省略第一个下标,得到逆变换:
inline void IFMTOR(int*a,int n){
for(int k=1;k<n;k<<=1)
for(int i=0;i<n;i++)
if(~i&k)a[i|k]=(a[i|k]-a[i]+MOD)%MOD;
}
正变换和逆变换只有一个加减号的区别,所以可以合并为一个函数:
inline void FMTOR(int*a,int n,int inv){
for(int k=1;k<n;k<<=1)
for(int i=0;i<n;i++)
if(~i&k)a[i|k]=(a[i|k]+a[i]*inv+MOD)%MOD;
}
这一般比FWT常数小,而且好打
按位与FMT
,也和沃尔什变换是一样的定义,DP式子和按位或有一点区别:
设表示 x 在二进制第 i 位以后部分和 j 一样、第 i 位以前部分和 j 进行按位与后等于 x 的
的和,
逆变换式子表示 x 在二进制第 i 位以前部分和 j 一样、第 i 位以后部分和 j 进行按位与后等于 x 的
的和,
一样的省去第一个下标,只不过枚举顺序反过来,从n-1到0:
inline void FMTAND(int*a,int n,int inv){//正变换和逆变换
for(int k=1;k<n;k<<=1)
for(int i=n-1;i>=0;i--)
if(i&k)a[i^k]=(a[i^k]+a[i]*inv+MOD)%MOD;
}
两者比较
FWT和FMT时间复杂度一样,都是,几乎没有额外空间。
此文章之前的版本说莫比乌斯变换无法解决按位异或,那其实是笔误,
其实细心的读者可以发现,FWT和FMT在程序运行过程中每一步对数组A干的事情是完全一样的,仅仅是代码长得不一样。为什么不一样?因为思路不同。
从代码角度看,FWT就是FMT,FMT就是FWT,思想不同但是走到了同一条路上(难怪总有人把它们搞混),
由于在推DP的过程中,我们发现完全可以定义一个枚举顺序和正变换一样的DP来达到逆变换,而FWT和FMT本质上操作是一样的,所以终于可以解释为什么FWT的枚举顺序也可以任意倒了。
由上面的发现,我们甚至可以强行把FWTXOR打成长得像FMT的样子:
inline void FMTXOR(int*a,int n,int inv){
int in2=inv>0?1:ksm(2,MOD-2,MOD),a0,a1;
for(int k=1;k<n;k<<=1)
for(int i=0;i<n;i++)
if(~i&k)a0=a[i],a1=a[i|k],a[i]=(a0+a1)%MOD*in2%MOD,a[i|k]=(a0-a1+MOD)%MOD*in2%MOD;
}
这么说来,按位异或变换也应该有个DP式子,但是叫不叫莫比乌斯变换就不清楚了。
另外,由于DP打法的灵活性,FMT能更方便地解决n不是2的次幂的情况,只需再加个特判。