快速沃尔什变换

Published: by Creative Commons Licence

  • Tags:

引言

快速沃尔什变换(Fast Walsh Transform)适用于计算一类多项式卷积的高效算法。定义

C=AB=nk=0Xkij=kAiBj

其中ABC均为n阶多项式。而Ai,Bj分别表示多项式AXi项的系数和多项式BXj项的系数。

看着眼熟吧,这不是和多项式的乘法非常像吗,当ij=i+j时,上面就是我们熟知的多项式乘法公式。我们知道解决多项式乘法不仅有O(n2)的朴素算法,还有O(nlog2n)的快速傅里叶变换。快速傅里叶变换实际上是将两个多项式A,B转换为点值表示法DFT(A)和DFT(B),之后利用向量乘法得到DFT(C)=DFT(A)DFT(B),最后从DFT(C)中还原多项式C。我们不免想到,是否能利用同样的技术进行加速呢?

或运算

如果ij=ij,那么我们可以定义:

FWT(A)=ni=0XijiAj

其中ji表示j中1出现的二进制位置是i的子集,换言之i=ij。我们注意到

FWT(C)k=tkCt=tkij=tAiBj=ijkAiBj=ikAijkBj=(ikAi)(ikBi)=FWT(A)kFWT(B)k

因此我们发现了FWT(C)=FWT(A)FWT(B)。下面我们讨论如何快速计算FWT(A)。记m=n2L(A)=m1i=0AiXiR(A)=ni=mAiXi。很显然A=L(A)+R(A)Xm

FWT(A)=ni=0XijiAj=ni=0Xi(ji,j<mAj+ji,jmAj)=m1i=0Xi(ji,j<mAj+ji,jmAj)+ni=mXi(ji,j<mAj+ji,jmAj)=m1i=0XijiL(A)j+Xmm1i=0Xi(jiL(A)j+j+mi+mR(A)j)=FWT(L(A))+Xm(FWT(L(A))+FWT(R(A)))

我们可以简单地记作

FWT(A)=(FWT(L(A)),FWT(L(A))+FWT(R(A)))

这样我们可以每次都将A拆分为前部和后部进行分治计算。时间复杂度为O(nlog2n)。而FWT的逆也很容易得到:

FWT1(A)=FWT1(L(A))+XmFWT1(R(A)L(A))=(FWT1(L(A)),FWT1(R(A)L(A)))

同样可以采用分治算法解决。

且运算

如果代表且运算,那么FWT函数定义如下:

FWT(A)=ni=0XiijAj

其余推理类似,不再赘述,直接给出公式:

FWT(C)=FWT(A)FWT(B)FWT(A)=(FWT(L(A))+FWT(R(A)),FWT(R(A)))FWT1(A)=(FWT1(L(A)R(A)),FWT1(R(A)))

异或运算

如果代表亦或运算。记E为所有二进制形式中1的个数为偶数个的正整数集合,而O表示所有二进制形式中1的个数为奇数个的正整数集合。那么FWT函数定义如下:

FWT(A)=ni=0Xi(i&jEAji&jOAj)

由于(可以通过枚举证明)

(i&j)k=(ik)&(jk)

因此:

FWT(C)k=k&tECtk&tOCt=k&tEij=tAiBjk&tOij=tAiBj=k&(ij)EAiBjk&(ij)OAiBj=(k&i)(k&j)EAiBj(k&i)(k&j)OAiBj

b(i)表示i的二进制形式中1的个数。记r表示ij中可能的1的数量,初始值为b(i)+b(j)。遍历每一位,若浏览某一位时,ij均为0,则r不变,若一者为1r依旧不变,否则二者都为1,则r减少2。很显然这个算法最终得到的就是b(i&j)。因此我们证明了b(i&j)b(i)+b(j)的二进制形式中1出现的次数拥有相同的奇偶性。

=k&iEAik&jEBj+k&iOAik&jOBjk&iEAik&jOBjk&iOAik&jEBj=(k&iEAik&iOAi)(k&iEBik&iOBi=FWT(A)kFWT(B)k

因此我们可以推出:

FWT(C)=FWT(A)FWT(B)

接着我们考虑如何拆分多项式。

FWT(A)=ni=0Xi(i&jEAji&jOAj)=ni=0Xi(i&jE,j<mAj+i&jE,jmAji&jO,j<mAji&jO,jmAj)=m1i=0Xi(i&jE,j<mAj+i&jE,jmAji&jO,j<mAji&jO,jmAj)+ni=mXi(i&jE,j<mAj+i&jE,jmAji&jO,j<mAji&jO,jmAj)=m1i=0Xi(i&jEL(A)j+i&jER(A)ji&jOL(A)ji&jOR(A)j)+Xmm1i=0Xi(i&jEL(A)j+i&jOR(A)ji&jOL(A)ji&jER(A)j)=FWT(L(A))+FWT(R(A))+Xm(FWT(L(A))FWT(R(A)))=(FWT(L(A))+FWT(R(A)),FWT(L(A))FWT(R(A)))

而逆函数容易得到:

FWT1(A)=(FWT1(L(A)+R(A)2),FWT1(L(A)R(A)2))

整体的时间复杂度为O(nlog2n)

题目1:给定长度为2m的向量x,要求计算n个连续的x的异或卷积:xxx。其中1m20n1018,结果对素数p取模。

考虑到FWT(xn)=FWT(x)n,因此可以先对x求一次FWT,之后将每个元素xi替换为xni,最后IFWT恢复。

题目2:给定长度为2m的向量x,要求计算n个连续的x的异或卷积:xxx。其中1m20n1018,结果对任意数p取模,其中1p109

由于IFWT的时候需要做除2操作,因此这里不能直接使用。有一种叫做K进制FWT的技术,其只需要在做IFWT的最后阶段,将整个向量除上2m即可。但是这里依旧不能保证gcd(2m,p)=1,因此我们需要将p先乘上2m做取模,之后每个向量元素能整除2m的性质就不会被影响。最后的阶段才将整个向量对p取模。

题目3:给定两个序列A0,b,其中A0的长度为2mb的长度为m+1。定义Ai,j=kAi1,kb[bitcount(kj)]。要求输出An,结果对p取模。其中1m201n1018

提供一道题目

我们将b展开,定义c[i]=b[bitcount(i)],那么我们要求的就是Ai,j=kAi1,kc[kj]。可以发现Ai=Ai1c,因此结果为An=A0cn

我们可以用题目2提到的方法计算出最终结果。

快速子集卷积

考虑这样一个问题,给定序列ab,要求找到另外一个序列c,满足:

ci=jk=ij&k=0ajbk

我们可以定义Ai,j=aj[bitcount(j)=i],同理定义BC。那么考虑Tk=ki=0AiBki,其与Ck的区别非常小,实际上Ck,i=Tk,i[bitcount(i)=k]

因此如果我们完成上面共n2次卷积,就可以求出C1,C2,,而我们要求的结果为c=iCi

上面的过程时间复杂度为O(n32n)

我们考虑到可以预先处理出A1,A2,以及B1,B2,的FWT形式,然后两两点积即可,最后再整体做一次IFWT即可(FWT和IFWT都是线性变换)。即Tk=IFWT(ki=0FWT(Ai)FWT(Bki))。这样就只需要做n2次的点积以及O(n)次的FWT和IFWT操作即可,时间复杂度为O(n22n)

K进制FWT

本章主要参考位运算卷积(FWT) & 集合幂级数

考虑A,B是两个定义在Zknk上的两个向量。记C=AB,定义Ci=jk=iAjBk

考虑我们现在要找到一个可逆矩阵F,满足F(A)F(B)=F(AB),这里的是点积的意思。这样我们就可以通过F1(F(A)F(B))快速得到AB

简单令c(i,j)=Fi,j。可以推出:

kn1j=0c(i,j)Ajkn1t=0c(i,t)Bt=kn1j=0c(i,j)Cjkn1j=0kn1t=0c(i,j)c(i,t)AjBt=kn1j=0c(i,j)tl=jAtBlkn1j=0kn1t=0c(i,j)c(i,t)AjBt=kn1j=0kn1t=0c(i,jt)AjBt

可以得出c(i,j)c(i,t)=c(i,jt)

考虑到位运算可以逐位处理,简单记x(i)表示k进制下x的第i位的值。因此我们可以简单的令c(i,j)=tc(i(t),j(t))。这样我们就只需要考虑k×k的一个矩阵。

接下来考虑在给定c的前提下快速计算FWT的算法。很显然直接用矩阵乘法的时间复杂度为O(k2n),下面给个更快的分治法算法。

F(A)i=kn1j=0c(i,j)Aj=k1b=0(b+1)kn11j=bkn1c(i,j)Aj

这里简单记i表示i去掉k进制最高位后的值,A[i]。表示将A均分为k段,第i段对应的向量。

=k1b=0(b+1)kn11j=bkn1c(i(n1),j(n1))c(i,j)Aj=k1b=0c(i(n1),b)(b+1)kn11j=bkn1c(i,j)Aj=k1b=0c(i(n1),b)F(A[b])i

上面给出的算法的时间复杂度为O(nkn+1)。而IFWT则类似,只是替换了c数组,并且先做还原操作,后递归分治。

最大值运算

按位取最大值操作在k=2时对应的就是或运算。

考虑到c(x,y)c(x,y)=c(x,max(y,y)),可以推出c(x,y)只可能为01

对于任意l<r,有c(x,l)c(x,r)=c(x,r)。这意味着c(x,l)=1c(x,r)=0,无论哪种情况都有c(x,l)c(x,r)。故可以发现c(x,?)是个前缀为1,后缀为0的向量。在满足条件的矩阵中有多个选择,我们选择任意一个可逆的矩阵即可,比如下面矩阵。

[1000110011101111]

它的逆矩阵为:

[1000110001100001]

一般做法时间复杂度为O(nkn+1),但是由于两个矩阵分别是前缀和和差分矩阵,因此可以O(nkn)计算。

最小值运算

按位取最小值操作在k=2时对应的就是且运算。

考虑到c(x,y)c(x,y)=c(x,min(y,y)),可以推出c(x,y)只可能为01

对于任意l<r,有c(x,l)c(x,r)=c(x,l)。这意味着c(x,l)=0c(x,r)=1,无论哪种情况都有c(x,l)c(x,r)。故可以发现c(x,?)是个前缀为0,后缀为1的向量。在满足条件的矩阵中有多个选择,我们选择任意一个可逆的矩阵即可,比如下面矩阵。

[1111011100110001]

它的逆矩阵为:

[1000011000110001]

一般做法时间复杂度为O(nkn+1),但是由于两个矩阵分别是后缀和和差分矩阵,因此可以O(nkn)计算。

不进位加法

不进位加法在k=2的时候对应的就是异或运算。考虑到c(x,l)c(x,r)=c(x,lr),因此可以认为这个矩阵的每一行都有循环的性质。如果我们取一个k次单位本原根wk。并且令矩阵Fi,j=wi×jk,那么线性变换F实际上是范德蒙德矩阵,其逆矩阵满足F1i,j=1kwi×jk

如果我们成功找到了k次单位本原根,那么时间复杂度为O(nkn+1)

当然我们并不能一定找到这样一个k次单位本原根。找不到怎么办。我们可以类似复数加入i一样,我们加入一个值x扩充我们的代数结构,且满足x1,,xn11xn=1。于是我们现在需要通过一个k1阶多项式来表示我们的代数中的任意值。

一般到这里我们为了避免多项式变得过大,可以让其对xk1取模。但是对xk1取模的情况下,我们的代数结构并不是一个整环,这样就不能保证FF1=I的成立。一个简单的解决方案就是我们在计算的时候对分圆多项式Φk(x)取模,这时候可以保证所有多项式都可逆。

上面的过程已经给我们提供了O(nkn+3)时间复杂度的算法。这里由于Φk(x)|xn1,因此我们在计算的过程中可以让中间结果对xn1,而最后要取得最终结果的时候才对Φk(x)取模,这样可以大幅加快取模的效率。时间复杂度可以优化到O(nkn+2)

提供几道题目:

不进位加法子集统计

考虑有mZnk的向量组成的向量组v1,,vm,对于所有uZnk,输出有多少V的子集,其子集和正好为u。答案对素数p取模。其中1k61n61m106

这种问题还是比较常见的,提供几道题目:

做法很神奇。

首先我们记Ai表示长度为kn的一个计数数组,其中仅vi0被设置为1,其它全为0。而我们要求的实际上是P=ni=1Ai

由于不好直接算,所以我们需要惯例先算出F(Ai),之后通过F(P)=ni=1F(Ai)来得到中间结果,最后P=F1(F(P))恢复数据。直接进行这个流程给出一个时间复杂度为O(mnkn+2)的算法。

下面我们来优化时间复杂度。首先可以发现F(At)i=1+c(i,vt)=1+wk1b=0i(b)×v(b)tk。因此我们不需要真的做FWT操作,直接通过这个公式就可以得到F(A1),,F(Am),时间复杂度可以优化到O(mkn+nkn+2)。如果vi=vj,那么我们只需要算一次F(vi)F(vj),因此m也可以去掉,得到时间复杂度O(k2n+nkn+2)

实际上我们可以进一步优化。考虑F(P)i仅可能出现w0k,w1k,,wk1k这些项,因此有F(P)i=kj=0(1+wjk)xi,j。因此我们只需要算出所有xi,j即可通过快速幂得到F(P)。一般的时间复杂度为O(kn+3log2m),如果我们预先计算幂处理结果的话,时间复杂度可以优化到O(mk3+kn+3)

接下来考虑如何快速得到所有的xi,j。记Ci=mj=1[vj=i],即统计i在输入中出现的次数。可以发现vj对于F(C)i的贡献为c(i,vj),而xi,jF(C)i的贡献为wjk,因此有:

F(C)i=k1j=0xi,jwjk

这是正常情况下的FWT的结果,接下来我们将C矩阵的每个元素取原来的0z<k次方,则可以得到k组行列式:

Fz(C)i=k1j=0xi,jwzjk

接下来将F0,,Fk1作为列组成一个新的矩阵F,将x视作一个kn×k的矩阵,而c则是一个范德蒙德矩阵。上面的方程组实际上是:

F=x×cF×c1=x

我们可以先通过k次对C做FWT算出F,之后右乘范德蒙德矩阵的逆,就可以得出x了。这一步总的时间复杂度为O(nkn+3)

因此总的时间复杂度为O(nkn+3+mk3)。特殊的如果Zp中我们能找到k次本原单位根,那么可以将时间复杂度优化到(nkn+2+mk),即减少一个k因子。

位独立运算

考虑这样一个问题,定义一个新的运算=(0,1,,n1),对于两个Zk上的向量ab,定义它们运算的结果为ab=(a00b0,b11b1,,an1n1bn1)。其中i可以替代为最大、最小、不进位运算。

现在给定两个Zknk上的向量A,B,要求计算C,其中Ci=jt=iAjBt

其实根据K进制FWT一节的推导可以发现,我们可以让c(i,j)=tct(i(t),j(t)),其中ct(i,j)ct(i,p)=ci(i,jtp)。而分治算法流程是不需要改变的,我们只需要复用其中推导出来的矩阵来进行运算。

因此我们依旧可以O(nkn+2)来解决这个问题。如果只涉及且和或运算,那么我们可以O(nkn)来解决这个问题。

提供一些题目:

子集和计数

题目1:给定一个序列a1,,an。其中0ai<2K。要求输出b0,,b2K1,其中bi表示a存在多少非空子序列,满足它们的或运算之和正好为i。其中1n106K=20

首先我们计算Ai,其中Ai表示序列中i的子集数。这可以通过高维前缀和或者FWT来实现。

之后我们定义Bi,其中Bi表示有多少非空子序列,其并集是i的子集,很显然Bi=2Ai1

之后我们可以通过高维差分或者FWT从B中计算出b,时间复杂度为O(n+K2K)

题目2:给定一个序列a1,,an。其中0ai<2K。要求输出b0,,b2K1,其中bi表示a存在多少非空子序列,满足它们的且运算之和正好为i。其中1n106K=20

首先我们计算Ai,其中Ai表示序列中i的超集数。这可以通过高维后缀和或者FWT来实现。

之后我们定义Bi,其中Bi表示有多少非空子序列,其交集是i的超集,很显然Bi=2Ai1

之后我们可以通过高维差分或者FWT从B中计算出b,时间复杂度为O(n+K2K)

题目3:给定一个序列a1,,an。其中0ai<2K。要求输出b0,,b2K1,其中bi表示a存在多少非空子序列,满足它们的亦或运算之和正好为i。其中1n106K=20

可以使用k进制不进位加法来解决这个问题。

参考资料