浅谈快速沃尔什变换(FWT)&快速莫比乌斯变换(FMT)

目录

快速沃尔什变换(FWT)

按位或卷积

按位与卷积

按位异或卷积

一点性质/优化

快速莫比乌斯变换(FMT)

按位或FMT

按位与FMT

两者比较


快速沃尔什变换(FWT)

顾名思义,这是一种对数组的变换,而且和FFT是基本思想相同,即求C=AoB,“o”是一种卷积,把A、B变换为A‘、B’,使满足C'_i=A'_i*B'_i,最后把C‘转换回C。

沃尔什变换主要有三种:

按位或卷积

C_x=\sum_{i\bigcup j=x}A_i\cdot B_j,(数组长度n默认为2的次幂)求C数组。

“浅谈”,我们就直接上公式好了。

该变换为FWT(A)_i=\sum_{j\bigcap i=j}A_j\bigcap是按位与,显然有:

FWT(C)_y=\sum_{x\bigcap y=x}C_x

=\sum_{x\bigcap y=x}\sum_{i\bigcup j=x}A_i \cdot B_j

=\sum_{i\bigcap y=i}A_i \cdot \sum_{j\bigcap y=j}B_j

=FWT(A)_y\cdot FWT(B)_y

根据定义知道,暴力求该变换是O(n^2)的,怎么优化呢?

对于一个数组A,其下标为(0~n-1),n=2^m,那么数组前一半的下标为(二进制)0XX...,后一半下标为1XX...,那么对于FWT(A)_iFWT(A)_{i+\frac{n}{2}}_{i<\frac{n}{2}}),假设前者是所有下标为0YY...的数的和,那么后者一定是下标为0YY...1YY...的数的和(XX...YY...分别代指若干种01序列),

规范地说就是,把A数组割裂为前后两半,(0~n/2-1)部分构成数组A0,(n/2~n-1)部分构成数组A1(在A1中的下标变为(0~n/2-1)),那么FWT(A)_i=FWT(A0)_i_{i<\frac{n}{2}}),

FWT(A)_{i+\frac{n}{2}}=\sum_{j<\frac{n}{2},j\bigcap i=j}(A_j+A_{j+\frac{n}{2}})=FWT(A0)_i+FWT(A1)_i_{i<\frac{n}{2}}),

所以我们得到了该算法的核心:求出FWT(A0)FWT(A1),然后遍历求出FWT(A)_iFWT(A)_{i+\frac{n}{2}},进而求得FWT(A)

这是递推打法,类比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)。

那么逆变换怎么搞呢?

考虑倒着做整个过程,FWT(A)_i=FWT(A0)_iFWT(A)_{i+\frac{n}{2}}=FWT(A0)_i+FWT(A1)_i_{i<\frac{n}{2}}),

所以FWT(A0)_i=FWT(A)_iFWT(A1)_i=FWT(A)_{i+\frac{n}{2}}-FWT(A)_i,然后从上往下把它倒着转换回去:

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_x=\sum_{i\bigcap j=x}A_i\cdot B_j,求C数组。

和按位或很相似,相信读者们自己就能轻松推导,

该变换为:FWT(A)_i=\sum_{j\bigcap i=i}A_j,同样的推导:

FWT(C)_y=\sum_{x\bigcap y=y}C_x

=\sum_{x\bigcap y=y}\sum_{i\bigcap j=x}A_i \cdot B_j

=\sum_{i\bigcap y=y}A_i \cdot \sum_{j\bigcap y=y}B_j

=FWT(A)_y\cdot FWT(B)_y

同样地,根据该变换的性质,把A分裂为A0和A1,有FWT(A)_i=FWT(A0)_i+FWT(A1)_iFWT(A)_{i+\frac{n}{2}}=FWT(A1)_i_{i<\frac{n}{2}}),

倒着做的时候:FWT(A0)_i=FWT(A)_i-FWT(A)_{i+\frac{n}{2}}FWT(A1)_i=FWT(A)_{i+\frac{n}{2}}

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=\sum_{i\bigoplus j=x}A_i\cdot B_j\bigoplus是按位异或),求C数组。

这个就不好搞了,一般人很难想出来,但是沃尔什大佬不一样,他想出了如下变换:

FWT(A)_i=\sum_{d(j\bigcap i)=0}A_j-\sum_{d(j\bigcap i)=1}A_j,其中d(x)表示x的二进制中1的个数取模2,也就是1的个数的奇偶性,

这个公式要倒着证:

FWT(A)_i\cdot FWT(B)_i=(\sum_{d(j\bigcap i)=0}A_j-\sum_{d(j\bigcap i)=1}A_j)\cdot (\sum_{d(j\bigcap i)=0}B_j-\sum_{d(j\bigcap i)=1}B_j)

观察上式,发现结构是00+11-01-10,而0^0=1^1=0,0^1=1^0=1;

然后有个结论:d((x\bigoplus y)\bigcap i)=d(x\bigcap i)\bigoplus d(y\bigcap i)

所以原式可以继续变换:

=\sum_{d(x\bigoplus y\bigcap i)=0}A_x\cdot B_y-\sum_{d(x\bigoplus y\bigcap i)=1}A_x\cdot B_y

=\sum_{d(j\bigcap i)=0}C_j-\sum_{d(j\bigcap i)=1}C_j=FWT(C)_i

至此,我们发现它满足了沃尔什变换的性质😮!

第二步,用分治优化它,

我们故技重施,把A分成前一半A0和后一半A1;

已知任给i,j<\frac{n}{2}n=2^m,有d(i\bigcap j)=d(i\bigcap (j+\frac{n}{2}))=d((i+\frac{n}{2})\bigcap j)=d((i+\frac{n}{2})\bigcap (j+\frac{n}{2}))\bigoplus 1

所以容易得到:FWT(A)_i=FWT(A0)_i+FWT(A1)_iFWT(A)_{i+\frac{n}{2}}=FWT(A0)_i-FWT(A1)_i

逆推式:FWT(A0)_i=\frac{FWT(A)_i+FWT(A)_{i+\frac{n}{2}} }{2}FWT(A1)_i=\frac{FWT(A)_i-FWT(A)_{i+\frac{n}{2}} }{2}

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

FMT(A)_i=\sum_{j\bigcap i=j}A_j,和沃尔什变换的按位或除了优化方法之外完全一样,

它的优化方法是设dp(i,j)表示 x 在二进制第 i 位以后部分和 j 一样、第 i 位以前部分和 j 进行按位与后等于 x 的A_x的和,

则显然有

并且(设数组长度n=2^mFMT(A)_i=dp(m+1,i)dp(0,i)=A_i

所以调整枚举顺序,把第一个下标省略后,再在原数组上进行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;
}

如何进行逆变换呢?

反过来,我们设dp'(i,j)表示 x 在二进制第 i 位以前部分和 j 一样、第 i 位以后部分和 j 进行按位与后等于 x 的A_x的和,

则可以得到

设数组长度n=2^m,有A_i=dp'(m+1,i)dp'(0,i)=FMT(A)_i

一样地省略第一个下标,得到逆变换:

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

FMT(A)_i=\sum_{j\bigcap i=i}A_j,也和沃尔什变换是一样的定义,DP式子和按位或有一点区别:

dp(i,j)表示 x 在二进制第 i 位以后部分和 j 一样、第 i 位以前部分和 j 进行按位与后等于 x 的A_x的和,

逆变换式子dp'(i,j)表示 x 在二进制第 i 位以前部分和 j 一样、第 i 位以后部分和 j 进行按位与后等于 x 的A_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时间复杂度一样,都是O(nlog_2n),几乎没有额外空间。

此文章之前的版本说莫比乌斯变换无法解决按位异或,那其实是笔误,

其实细心的读者可以发现,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的次幂的情况,只需再加个特判。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值