快速沃尔什变换
引言
快速沃尔什变换(Fast Walsh Transform)适用于计算一类多项式卷积的高效算法。定义
C=A⊗B=n∑k=0Xk∑i⊕j=kAiBj其中A,B,C均为n阶多项式。而Ai,Bj分别表示多项式A中Xi项的系数和多项式B中Xj项的系数。
看着眼熟吧,这不是和多项式的乘法非常像吗,当i⊕j=i+j时,上面就是我们熟知的多项式乘法公式。我们知道解决多项式乘法不仅有O(n2)的朴素算法,还有O(nlog2n)的快速傅里叶变换。快速傅里叶变换实际上是将两个多项式A,B转换为点值表示法DFT(A)和DFT(B),之后利用向量乘法得到DFT(C)=DFT(A)DFT(B),最后从DFT(C)中还原多项式C。我们不免想到,是否能利用同样的技术进行加速呢?
或运算
如果i⊕j=i∣j,那么我们可以定义:
FWT(A)=n∑i=0Xi∑j∈iAj其中j∈i表示j中1出现的二进制位置是i的子集,换言之i=i∣j。我们注意到
FWT(C)k=∑t∈kCt=∑t∈k∑i∣j=tAiBj=∑i∣j∈kAiBj=∑i∈kAi∑j∈kBj=(∑i∈kAi)(∑i∈kBi)=FWT(A)k⋅FWT(B)k因此我们发现了FWT(C)=FWT(A)⋅FWT(B)。下面我们讨论如何快速计算FWT(A)。记m=⌈n2⌉,L(A)=∑m−1i=0AiXi,R(A)=∑ni=mAiXi。很显然A=L(A)+R(A)⋅Xm。
FWT(A)=n∑i=0Xi∑j∈iAj=n∑i=0Xi(∑j∈i,j<mAj+∑j∈i,j≥mAj)=m−1∑i=0Xi(∑j∈i,j<mAj+∑j∈i,j≥mAj)+n∑i=mXi(∑j∈i,j<mAj+∑j∈i,j≥mAj)=m−1∑i=0Xi∑j∈iL(A)j+Xmm−1∑i=0Xi(∑j∈iL(A)j+∑j+m∈i+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的逆也很容易得到:
FWT−1(A)=FWT−1(L(A))+XmFWT−1(R(A)−L(A))=(FWT−1(L(A)),FWT−1(R(A)−L(A)))同样可以采用分治算法解决。
且运算
如果⊕代表且运算,那么FWT函数定义如下:
FWT(A)=n∑i=0Xi∑i∈jAj其余推理类似,不再赘述,直接给出公式:
FWT(C)=FWT(A)⋅FWT(B)FWT(A)=(FWT(L(A))+FWT(R(A)),FWT(R(A)))FWT−1(A)=(FWT−1(L(A)−R(A)),FWT−1(R(A)))异或运算
如果⊕代表亦或运算。记E为所有二进制形式中1的个数为偶数个的正整数集合,而O表示所有二进制形式中1的个数为奇数个的正整数集合。那么FWT函数定义如下:
FWT(A)=n∑i=0Xi(∑i&j∈EAj−∑i&j∈OAj)由于(可以通过枚举证明)
(i&j)⊕k=(i⊕k)&(j⊕k)因此:
FWT(C)k=∑k&t∈ECt−∑k&t∈OCt=∑k&t∈E∑i⊕j=tAiBj−∑k&t∈O∑i⊕j=tAiBj=∑k&(i⊕j)∈EAiBj−∑k&(i⊕j)∈OAiBj=∑(k&i)⊕(k&j)∈EAiBj−∑(k&i)⊕(k&j)∈OAiBj记b(i)表示i的二进制形式中1的个数。记r表示i⊕j中可能的1的数量,初始值为b(i)+b(j)。遍历每一位,若浏览某一位时,i与j均为0,则r不变,若一者为1,r依旧不变,否则二者都为1,则r减少2。很显然这个算法最终得到的就是b(i&j)。因此我们证明了b(i&j)与b(i)+b(j)的二进制形式中1出现的次数拥有相同的奇偶性。
=∑k&i∈EAi∑k&j∈EBj+∑k&i∈OAi∑k&j∈OBj−∑k&i∈EAi∑k&j∈OBj−∑k&i∈OAi∑k&j∈EBj=(∑k&i∈EAi−∑k&i∈OAi)(∑k&i∈EBi−∑k&i∈OBi)=FWT(A)kFWT(B)k因此我们可以推出:
FWT(C)=FWT(A)⋅FWT(B)接着我们考虑如何拆分多项式。
FWT(A)=n∑i=0Xi(∑i&j∈EAj−∑i&j∈OAj)=n∑i=0Xi(∑i&j∈E,j<mAj+∑i&j∈E,j≥mAj−∑i&j∈O,j<mAj−∑i&j∈O,j≥mAj)=m−1∑i=0Xi(∑i&j∈E,j<mAj+∑i&j∈E,j≥mAj−∑i&j∈O,j<mAj−∑i&j∈O,j≥mAj)+n∑i=mXi(∑i&j∈E,j<mAj+∑i&j∈E,j≥mAj−∑i&j∈O,j<mAj−∑i&j∈O,j≥mAj)=m−1∑i=0Xi(∑i&j∈EL(A)j+∑i&j∈ER(A)j−∑i&j∈OL(A)j−∑i&j∈OR(A)j)+Xmm−1∑i=0Xi(∑i&j∈EL(A)j+∑i&j∈OR(A)j−∑i&j∈OL(A)j−∑i&j∈ER(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)))而逆函数容易得到:
FWT−1(A)=(FWT−1(L(A)+R(A)2),FWT−1(L(A)−R(A)2))整体的时间复杂度为O(nlog2n)。
题目1:给定长度为2m的向量x,要求计算n个连续的x的异或卷积:x⊕x⊕…⊕x。其中1≤m≤20,n≤1018,结果对素数p取模。
考虑到FWT(xn)=FWT(x)n,因此可以先对x求一次FWT,之后将每个元素xi替换为xni,最后IFWT恢复。
题目2:给定长度为2m的向量x,要求计算n个连续的x的异或卷积:x⊕x⊕…⊕x。其中1≤m≤20,n≤1018,结果对任意数p取模,其中1≤p≤109。
由于IFWT的时候需要做除2操作,因此这里不能直接使用。有一种叫做K进制FWT的技术,其只需要在做IFWT的最后阶段,将整个向量除上2m即可。但是这里依旧不能保证gcd(2m,p)=1,因此我们需要将p先乘上2m做取模,之后每个向量元素能整除2m的性质就不会被影响。最后的阶段才将整个向量对p取模。
题目3:给定两个序列A0,b,其中A0的长度为2m,b的长度为m+1。定义Ai,j=∑kAi−1,kb[bitcount(k⊕j)]。要求输出An,结果对p取模。其中1≤m≤20,1≤n≤1018
提供一道题目。
我们将b展开,定义c[i]=b[bitcount(i)],那么我们要求的就是Ai,j=∑kAi−1,kc[k⊕j]。可以发现Ai=Ai−1⊕c,因此结果为An=A0⊕cn。
我们可以用题目2提到的方法计算出最终结果。
快速子集卷积
考虑这样一个问题,给定序列a和b,要求找到另外一个序列c,满足:
ci=∑j∣k=i∧j&k=0ajbk我们可以定义Ai,j=aj[bitcount(j)=i],同理定义B和C。那么考虑Tk=∑ki=0Ai⊕Bk−i,其与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(Bk−i))。这样就只需要做n2次的点积以及O(n)次的FWT和IFWT操作即可,时间复杂度为O(n22n)。
K进制FWT
本章主要参考位运算卷积(FWT) & 集合幂级数。
考虑A,B是两个定义在Zknk上的两个向量。记C=A∗B,定义Ci=∑j⊕k=iAjBk。
考虑我们现在要找到一个可逆矩阵F,满足F(A)⋅F(B)=F(A∗B),这里的⋅是点积的意思。这样我们就可以通过F−1(F(A)∗F(B))快速得到A∗B。
简单令c(i,j)=Fi,j。可以推出:
kn−1∑j=0c(i,j)Ajkn−1∑t=0c(i,t)Bt=kn−1∑j=0c(i,j)Cj⇒kn−1∑j=0kn−1∑t=0c(i,j)c(i,t)AjBt=kn−1∑j=0c(i,j)∑t⊕l=jAtBl⇒kn−1∑j=0kn−1∑t=0c(i,j)c(i,t)AjBt=kn−1∑j=0kn−1∑t=0c(i,j⊕t)AjBt可以得出c(i,j)c(i,t)=c(i,j⊕t)。
考虑到位运算可以逐位处理,简单记x(i)表示k进制下x的第i位的值。因此我们可以简单的令c(i,j)=∏tc(i(t),j(t))。这样我们就只需要考虑k×k的一个矩阵。
接下来考虑在给定c的前提下快速计算FWT的算法。很显然直接用矩阵乘法的时间复杂度为O(k2n),下面给个更快的分治法算法。
F(A)i=kn−1∑j=0c(i,j)Aj=k−1∑b=0(b+1)kn−1−1∑j=bkn−1c(i,j)Aj这里简单记i′表示i去掉k进制最高位后的值,A[i]。表示将A均分为k段,第i段对应的向量。
=k−1∑b=0(b+1)kn−1−1∑j=bkn−1c(i(n−1),j(n−1))c(i′,j′)Aj=k−1∑b=0c(i(n−1),b)(b+1)kn−1−1∑j=bkn−1c(i′,j′)Aj=k−1∑b=0c(i(n−1),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)只可能为0或1。
对于任意l<r,有c(x,l)c(x,r)=c(x,r)。这意味着c(x,l)=1或c(x,r)=0,无论哪种情况都有c(x,l)≥c(x,r)。故可以发现c(x,?)是个前缀为1,后缀为0的向量。在满足条件的矩阵中有多个选择,我们选择任意一个可逆的矩阵即可,比如下面矩阵。
[100⋯0110⋯0111⋯0⋯⋯⋯…⋯111⋯1]它的逆矩阵为:
[100⋯0−110⋯00−11⋯0⋯⋯⋯…⋯000⋯1]一般做法时间复杂度为O(nkn+1),但是由于两个矩阵分别是前缀和和差分矩阵,因此可以O(nkn)计算。
最小值运算
按位取最小值操作在k=2时对应的就是且运算。
考虑到c(x,y)c(x,y)=c(x,min(y,y)),可以推出c(x,y)只可能为0或1。
对于任意l<r,有c(x,l)c(x,r)=c(x,l)。这意味着c(x,l)=0或c(x,r)=1,无论哪种情况都有c(x,l)≤c(x,r)。故可以发现c(x,?)是个前缀为0,后缀为1的向量。在满足条件的矩阵中有多个选择,我们选择任意一个可逆的矩阵即可,比如下面矩阵。
[1⋯111⋯⋯⋯…⋯0⋯1110⋯0110⋯001]它的逆矩阵为:
[1⋯000⋯⋯⋯…⋯0⋯1−100⋯01−10⋯001]一般做法时间复杂度为O(nkn+1),但是由于两个矩阵分别是后缀和和差分矩阵,因此可以O(nkn)计算。
不进位加法
不进位加法在k=2的时候对应的就是异或运算。考虑到c(x,l)c(x,r)=c(x,l⊕r),因此可以认为这个矩阵的每一行都有循环的性质。如果我们取一个k次单位本原根wk。并且令矩阵Fi,j=wi×jk,那么线性变换F实际上是范德蒙德矩阵,其逆矩阵满足F−1i,j=1kw−i×jk。
如果我们成功找到了k次单位本原根,那么时间复杂度为O(nkn+1)。
当然我们并不能一定找到这样一个k次单位本原根。找不到怎么办。我们可以类似复数加入i一样,我们加入一个值x扩充我们的代数结构,且满足x1,…,xn−1≠1且xn=1。于是我们现在需要通过一个k−1阶多项式来表示我们的代数中的任意值。
一般到这里我们为了避免多项式变得过大,可以让其对xk−1取模。但是对xk−1取模的情况下,我们的代数结构并不是一个整环,这样就不能保证FF−1=I的成立。一个简单的解决方案就是我们在计算的时候对分圆多项式Φk(x)取模,这时候可以保证所有多项式都可逆。
上面的过程已经给我们提供了O(nkn+3)时间复杂度的算法。这里由于Φk(x)|xn−1,因此我们在计算的过程中可以让中间结果对xn−1,而最后要取得最终结果的时候才对Φk(x)取模,这样可以大幅加快取模的效率。时间复杂度可以优化到O(nkn+2)。
提供几道题目:
不进位加法子集统计
考虑有m个Znk的向量组成的向量组v1,…,vm,对于所有u∈Znk,输出有多少V的子集,其子集和正好为u。答案对素数p取模。其中1≤k≤6,1≤n≤6,1≤m≤106。
这种问题还是比较常见的,提供几道题目:
做法很神奇。
首先我们记Ai表示长度为kn的一个计数数组,其中仅vi和0被设置为1,其它全为0。而我们要求的实际上是P=∏ni=1Ai。
由于不好直接算,所以我们需要惯例先算出F(Ai),之后通过F(P)=∏ni=1F(Ai)来得到中间结果,最后P=F−1(F(P))恢复数据。直接进行这个流程给出一个时间复杂度为O(mnkn+2)的算法。
下面我们来优化时间复杂度。首先可以发现F(At)i=1+c(i,vt)=1+w∑k−1b=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,…,wk−1k这些项,因此有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,j对F(C)i的贡献为wjk,因此有:
F(C)i=k−1∑j=0xi,jwjk这是正常情况下的FWT的结果,接下来我们将C矩阵的每个元素取原来的0≤z<k次方,则可以得到k组行列式:
Fz(C)i=k−1∑j=0xi,jwzjk接下来将F0,…,Fk−1作为列组成一个新的矩阵F′,将x视作一个kn×k的矩阵,而c则是一个范德蒙德矩阵。上面的方程组实际上是:
F′=x×c⇒F′×c−1=x我们可以先通过k次对C做FWT算出F′,之后右乘范德蒙德矩阵的逆,就可以得出x了。这一步总的时间复杂度为O(nkn+3)。
因此总的时间复杂度为O(nkn+3+√mk3)。特殊的如果Zp中我们能找到k次本原单位根,那么可以将时间复杂度优化到(nkn+2+√mk),即减少一个k因子。
位独立运算
考虑这样一个问题,定义一个新的运算⊕=(⊕0,⊕1,…,⊕n−1),对于两个Zk上的向量a和b,定义它们运算的结果为a⊕b=(a0⊕0b0,b1⊕1b1,…,an−1⊕n−1bn−1)。其中⊕i可以替代为最大、最小、不进位运算。
现在给定两个Zknk上的向量A,B,要求计算C,其中Ci=∑j⊕t=iAjBt。
其实根据K进制FWT一节的推导可以发现,我们可以让c(i,j)=∏tct(i(t),j(t)),其中ct(i,j)ct(i,p)=ci(i,j⊕tp)。而分治算法流程是不需要改变的,我们只需要复用其中推导出来的矩阵来进行运算。
因此我们依旧可以O(nkn+2)来解决这个问题。如果只涉及且和或运算,那么我们可以O(nkn)来解决这个问题。
提供一些题目:
子集和计数
题目1:给定一个序列a1,…,an。其中0≤ai<2K。要求输出b0,…,b2K−1,其中bi表示a存在多少非空子序列,满足它们的或运算之和正好为i。其中1≤n≤106,K=20。
首先我们计算Ai,其中Ai表示序列中i的子集数。这可以通过高维前缀和或者FWT来实现。
之后我们定义Bi,其中Bi表示有多少非空子序列,其并集是i的子集,很显然Bi=2Ai−1。
之后我们可以通过高维差分或者FWT从B中计算出b,时间复杂度为O(n+K2K)。
题目2:给定一个序列a1,…,an。其中0≤ai<2K。要求输出b0,…,b2K−1,其中bi表示a存在多少非空子序列,满足它们的且运算之和正好为i。其中1≤n≤106,K=20。
首先我们计算Ai,其中Ai表示序列中i的超集数。这可以通过高维后缀和或者FWT来实现。
之后我们定义Bi,其中Bi表示有多少非空子序列,其交集是i的超集,很显然Bi=2Ai−1。
之后我们可以通过高维差分或者FWT从B中计算出b,时间复杂度为O(n+K2K)。
题目3:给定一个序列a1,…,an。其中0≤ai<2K。要求输出b0,…,b2K−1,其中bi表示a存在多少非空子序列,满足它们的亦或运算之和正好为i。其中1≤n≤106,K=20。
可以使用k进制不进位加法来解决这个问题。