FWT(Fast Walsh Transformation)快速沃尔什变换学习笔记

引入

对于两个多项式,形如

a0×x0+a1×x1++an×xn

我们将其记为f(x)g(x),当我们需要快速求h(x)=f(x)g(x)(卷积)时,我们就会用FFT算法来解决这个问题。
FWT就是来解决集合上的一类卷积问题。


  • 前言

在比赛与平时练题时,我们常常会遇到一些关于集合的动态规划的问题,比如集合的方案数计数,某些集合的权值计算等,但是一般推出的递推式复杂度会成指数或者阶乘之高,是无法忍受的,但是又没有好的解决方法,于是就放弃去打爆力算了。


那么对于两个集合,形如

{a0,a1,a2,,an}

我们将其记为AB,现在又两个分别定义在AB的函数fg,现有另一个定义在集合上的函数h。对于一个集合ShS的计算方式如下,我们用O表示全集,其中AO,BO,SO

hS=IOJO[IJ=S]fI×gJ

这里为一类集合的运算,一般为(交,并,对称差),这个便是集合的卷积。

那么有没有类似FFT的方法快速计算两个集合之间的卷积呢?若有不就可以解决前言中的问题了?答案肯定是有的。


前置

集合幂级数

对数列理论熟悉的读者应该知道,我们可以把一个数列f0,f1,f2,和一个形式幂级数f(x)=0k fk×xk相对应,称之为该数列的生成函数。由于有很多相关的理论与快速算法,这个方法就能解决很多的数列的计算问题。

那么我们对于集合也定义类似的东西,看看能否达到同样的效果。

F是一个域,则称函数f:O > FF上的一个集合幂级数,对于一个集合S(SO),我们令fS为把S带入函数f时的函数值,同样的将fS称为该集合幂级数的第S项的系数。那么如果所有的fS确定了的话,该函数f也就确定了。

那么我们设f,g为集合幂级数,定义加法f+g=hh也是一个集合幂级数,且对于一个集合S,满足如下式子:

hS=fS+gS

所以,集合幂级数的加法就是对应系数相加,减法同理,改为fSgS即可。

下面我们定义集合幂级数的具体形式。参照形式幂级数的样子,我们定义如下: 对于任意的vF,SO,我们用v×xS表示f函数中的第S项的系数为v

那么对于一个集合幂级数的函数f,形式如下:

f=SOfS×xS

fS×xS一般简写为fSxS

我们以此来表示一个集合幂级数。

举个栗子,当O={1,2},F=R(R表示实数集),对于该函数f,我们可以用f(x)=3x+7x{1}+6x{2}+9x{1,2}来表示一个对应系数为3,7,6,9的集合幂级数。

我们同样定义乘法如下,对于集合幂级数h,f,g,其中h=f×g,且其中的每一项满足(fIxI)×(gJxJ)=(fI×gJ)xI×J,那么这个乘法就可以满足交换,结合,分配等定律了。


集合的并,交,对称差卷积

我们令三个集合幂级数f,g,h,其中h满足如下式子:

hS=IOJO[IJ=S]fI×gJ

那么我们称hf,g的集合并卷积,简记为fg=h

我们令三个集合幂级数f,g,h,其中h满足如下式子:

hS=IOJO[IJ=S]fI×gJ

那么我们称hf,g的集合交卷积,简记为fg=h

对称差

我们令三个集合幂级数f,g,h,其中h满足如下式子:

hS=IOJO[IJ=S]fI×gJ

那么我们称hf,g的集合对乘差卷积,简记为fg=h或者f×g(能看懂就行)。


对于如上的三个卷积,显然都可O(|O|2)的时间复杂度计算(括号外面的O为时间复杂度的符号,里面的O表示全集,|O|表示全集大小)。

但是这样肯定不能解决大多数问题,那么我们需要考虑快速算法。


分治是一种降低复杂度的好方法,那么这个能否分治呢?

我们将全集O中的一个元素w单独提出,也就是对于所有含w的集合提一个w出来,那么f可以写成由两个集合组合出来的函数,f表示不含w的部分,f+表示含有w的部分,那么f=f+x{w}f+,由此我们可以把原式写成如下形式

f×g=(f+x{w}f+)×(g+x{w}g+)

对于不同的,我们可以将其化简成不同的形式。

  • =

由于当前的运算为并,那么x{w}×x{}=x{w},x{w}×x{w}=x{w}x{}×x{}=x{}(有元素w的与没有的并起来就有元素w,都有元素w并起来还是有,都没有并起来就还是没有)。

所以我们进行如下化简:

f×g=(f+x{w}f+)×(g+x{w}g+)

=f×g+x{w}×(f×g+)+x{w}×(f+×g)+x{w}×(f+×g+)

这里将乘号略写
=fg+x{w}(fg+ +f+g+f+g+)

将其化简为乘积的形式

=fg+x{w}((f+f+)×(g+g+)fg)

我们将其看作两种形式,第一种为单独的fg,另一种为单独的(f+f+)(g+g+),那么我们通过分治的方法,先将fg分别变化成f=(f,(f+f+))g=(g,(g+g+))
然后对应的项乘起来,我们令h=f×g,此时h=(h,h+)=(fg,(f+f+)×(g+g+)),但是此时的h并不是我们要求的h,联想FFT的做法,既然变换过去了,肯定要找个方法变换回来。再看我们推导化简式子的最后有一个((f+f+)×(g+g+)fg),所以我们将h变成h=(h,h+h),就可以得到h了。

复杂度分析,我们令size为元素种类的大小,设总的变换的时间复杂度为T(size),可得T(size)=2×T(size1)+O(|O|),其中2×T(size1)是因为提出一个元素后,元素种类减少1,分成了本来有该元素和本来无该元素的两部分,所以乘2,再加上当前这次变换的复杂度。最后解得总复杂度T(size)=O(size×|O|),而size大小是远远小于由size个元素组成的集合的全集O的大小的,一般|O|=2size,复杂度远远低于暴力复杂度,可以看做O(nlog2n)

说明:例如当前size=3,有1,2,3元素,那么O={{},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3}},所以O的大小为2size=8


  • =

同样对于交,我们化简原式,过程略,得到如下式子:

乘号略写公式太难打了QAQ

fg=fg+f+g+fg++x{w}(g+f+)

说明:因为交是必须两个都有w元素,最后才会有,否则就没有w元素。

写成乘积形式

=((f+f+)(g+g+)f+g+)+x{w}(g+f+)

同样的,提出类似的部分后,我们可以对f,g先自己变换,然后相乘(对应项),然后再变换一次变回来就可以求出h

其中f=((f+f+),f+),g=((g+g+),g+),h=(hh+),h+

复制度同上。


  • =

对于对称差,都是大同小异。

化简后得到

fg=(fg+f+g+)x{w}(fg++f+g)

说明:因为对称差计算有点奇怪好吧其实也不奇怪,两者都有或者都没有该元素,计算出来就没有。只要其中一个有另一个没有,计算出来就有。

所以变换为:

f=((f+f+),(ff+))
g同理。

h=((h+h+)2,(hh+)2)

自己手推一下应该可以得到。

  • 对称差的另外一种推导方法

我们这么半天,集合幂级数的性质也没太明显用,下面就将对称差的另外一种推导方法。

首先引入如下式子:

对于一个集合S有:

12sizeTO(1)|ST|=[S=]

对于证明我们较容易发现,当S=|ST|恒为0,那么(1)|ST|也就恒为1,又因为|O|=2size,所以最后除以了一个2size,式子的值就为1。对于S不为空集,令一个元素vS,由于(1)|S(T{v})|,根据对称差的分配律S(T{v})=(S{v})(T{v})=(ST){v},原式等于(1)|(ST){v}|,又因为ST中没有{v}交集大小增加1,有{v}交集大小减少1,所以怎么都要变化1,所以(1)|(ST){v}|=(1)|ST|,那么可以得知(1)|ST|+(1)|(ST){v}|=0,由于T{v}{v}=T,所以TT{v}一一对应,所以最后原式的值肯定为0。

我们接下来看:

h=fg

对于一个集合S,可知

hS=IOJO[IJ=S]fIgJ

=IOJO[IJS=]fIgJ

=IOJO12sizeTO(1)|(IJS)T| fIgJ

=IOJO12sizeTO(1)|TI|(1)|TJ|(1)|TS| fIgJ

说明:这里能这样变换是因为(1)×(1)=1对答案无影响,会消掉,所以如果IJS有或者没有某个元素,和T交后,会被如上方法抵消影响。

举个栗子:I,J,S的某个元素{v}的有无情况如下(因为都与T相交,所以可以不管T的影响):
{1,0,0}{0,1,0}{0,0,1}{1,1,1},此时 IJS会有该元素,但是三种情况分别为(1)1,(1)3都为1不变,同样IJS没有该元素的都为1不变,所以可以拆开,其实因为出来有该元素必须有奇数个1,无该元素必须有偶数个1,所以(1)的奇数次方为-1,偶数次方为0,而奇数出来有该元素,就是(1)1还是1,偶数没有,就是(1)0,所以为1。

=12sizeTO(1)|TS|(IO(1)|TI|fI)(JO(1)|TJ|gJ)

这里前半部分12sizeTO(1)|TS|只有当S=时后半部分才为1,所以这个式子的值为1,然后对于单个集合f的变换,我们可以将集合g看作一个常量,也就是式子最后半部分关于g的那个括号里的值可以看作1,所以这种形式可以启发我们定义以下变换。

这样我们就可以定义一种变换,对于一个集合幂级数f,我们定义它的沃尔什变换为集合幂级数f,其中:

fS=TOfT(1)|ST|

其逆变换同样由上面推导式得到:

fS=12sizeTOfT(1)|ST|

再从集合对称差卷积得到:

hS=fSgS

于是我们先可以对f,g做沃尔什变换,再相乘得到h然后对其做沃尔什逆变换得到答案h

这里可以用递推的方法,我们设fS(i)为只考虑那些与S的对称差是{1,,i}的子集的沃尔什变换的第S项(也就是TS{1,,i}的集合TfT(1)|ST|之和),这里容易得到对于不包含iS

fS(i)=fS(i1)fS{i}(i1)

fS{i}(i)=fS(i1)+fS{i}(i)

说明:对于第一个式子,不含有{i}元素的fS(i)根据定义式=TOfT(1)|ST|,将其拆分为两部分=TOfT(1)|ST|[{i}T]+TOfT(1)|ST|[{i}T],对于前半部分因为已经是S不含有i了,但是对于后者,我们要用S{i}来表示,所以当S换成S{i}时,|ST|变成|(S{i})T|,它的集合大小会增加1,因为开始S中没有{i},交出来也没有,后面|S{i}|中有了,所以交出来就比原来多了一个{i}(这里的T中是有{i}的),所以TOfT(1)|ST|[{i}T]=TOfT(1)|(S{i})T|     [{i}T],所以可以得到一式。
对于二式,也是同样的道理,开始变成了含有{i}的集合S{i},同样用定义式将其拆成两部分TOfT(1)|(S{i})T|     [{i}T]+TOfT(1)|(S{i})T|     [{i}T],然后后半部分已经为S{i}所以要将前半部分变成没有{i}S,同样来看,|(S{i})T||ST|没有变化,因为这里的T是不包含{i}元素的,所以可以得到二式。

但是对于最底层的fS(0),根据定义来看,我们最底层的T只能等于S,这样ST=,所以fS(0),当S大小为奇数时它等于fS,为偶数时他就等于fS,这样十分不好处理,所以我们统一规定,fS(0)=fS,那么对于上面求出的式子也要进行改动。

我们这里假设(1)|ST|=1,那么(1)|(S{i})T|=1,(其实反过来也可以,虽然式子符号(正负)会变,最终的答案不会改变),因为后者会比前者多一个元素{i},所以fS{i}(i)本来要乘以1,又因为我们上面的重定义,所以不能乘以1,所以对于后者要取相反数,这里我们便得到了最终的集合对称差卷积的沃尔什变换的式子:

fS(i)=fS(i1)+fS{i}(i1)

fS{i}(i)=fS(i1)fS{i}(i)

其实也就和之前的证明方法的结果相同了,f=(f+f+,ff+)

逆变换也就是原来的变换乘以了12size,所以我们发现这里可以不用像原来的逆变换每次除以2,可以做顺变换,最后答案除以2size即可。

注:对于其它两个集合运算也有类似的证明,但是过于繁琐,而且实际应用中集合对称差用到此证明公式的最多,所以其余两个就不讲此类证法,有兴趣的读者可自行查阅资料。


到这里,沃尔什变换就大概完整了。

三种卷积的变换与逆变换

集合并

  • 沃尔什变换 f=(f,f+f+)

  • 沃尔什逆变换 f=(f,f+f)

集合交

  • 沃尔什变换 f=(f+f+,f+)

  • 沃尔什逆变换 f=(ff+,f+)

集合对称差

  • 沃尔什变换 f=(f+f+,ff+)

  • 沃尔什逆变换 f=(f+f+2,ff+2)

    注意:在取模运算时,除2要变成乘以2对应的逆元。


那么对于全集的每个元素,我们可以用0或者1来表示它是否在某个集合里,就可以用一个二进制数来表示集合,这在运算上就十分方便了,原因如下:

  • 对于集合并,它就相当于位运算中的|(or)10110110=1111 , 1011|0110=1111

  • 对于集合交,它就相当于位运算中的&(and),10110110=0010 , 1011&0110=0010。

  • 对于集合对称差,你应该想到了,没错,它就是(xor)10110110=110110110110=1101


那么由于位运算每一位之间互不影响,所以我们可以把它按照当前位为0或1分治,也就是表示当前元素有无分治。

但是递归虽然可以,有时常数以及实际效率并不是很优秀,所以我们考虑不用递归的形式。

我们发现它的全集O,可以用二进制数全部表示出来,转换为十进制来看就是{0,1,2,,2size1},其中0表示

那么我们来看二进制的形式:

{0,1,2,3,4,5,6,7}就等于

{000,001,010,011,100,101,110,111}

我们发现对于第i位(从右往左数),它为0相隔2i1个数,为1也是,如1:{0,1,0,1,0,1,0,1},2:{0,0,1,1,0,0,1,1},3:{0,0,0,0,1,1,1,1},以此类推,所以我们可以用循环来代替递归。

第一层循环,枚举当前为第i

for(int bit=1;bit<(1<<size);bit<<=1)

第二次枚举当前连续的一段0和1

int inc=bit<<1;
for(int i=0;i<(1<<size);i+=inc)

依次枚举这一段对应的0和1,

for(int j=0;j<bit;j++)

那么我们用x表示0这一位,y表示1这一位,那么就有

x=a[i+j],y=a[i+j+bit];
//eg.当前这一位是{0,0,1,1,0,0,1,1}
//               |   |
//               i   i+bit
// j {0~1}

所以对于三种变化我们计算如下

运算方式 or并 and交 xor对称差
沃尔什变换 a[i+j+bit]=x+y a[i+j]=x+y a[i+j]=x+y,a[i+j+bit]=x-y
沃尔什逆变换 a[i+j+bit]=y-x a[i+j]=x-y a[i+j]=(x+y)/2,a[i+j+bit]=(x-y)/2

那么代码就很容易写了。
模板题目

Luogu P4717
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const int M=1<<19;
const ll mod=998244353ll;
int n;
ll A[M],B[M];
ll a[M],b[M];
ll c[3][M],inv;
ll pow(ll a){
    ll b=mod-2,ans=1;
    for(;b;b>>=1,a=(a*a)%mod){
        if(b&1) ans=(ans*a)%mod;
    }
    return ans;
}
void WT(ll *w,int n,int f,int type){
    ll x,y;
    //type:0 or,1 and,2 xor
    //f=1 沃尔什变换 f=-1 沃尔什逆变换
    for(int d=1;d<n;d<<=1){
        for(int ad=d<<1,i=0;i<n;i+=ad){
            for(int j=0;j<d;j++){
                x=w[i+j],y=w[i+j+d];
                if(f==1){
                    switch(type){
                        case 0:{
                            w[i+j+d]=(x+y)%mod;
                            break;
                        }
                        case 1:{
                            w[i+j]=(x+y)%mod;
                            break;
                        }
                        case 2:{
                            w[i+j]=(x+y)%mod;
                            w[i+j+d]=(x-y+mod)%mod;
                            break;
                        }
                    }
                }else{
                    switch(type){
                        case 0:{
                            w[i+j+d]=(y-x+mod)%mod;
                            break;
                        }
                        case 1:{
                            w[i+j]=(x-y+mod)%mod;
                            break;
                        }
                        case 2:{
                            w[i+j]=(x+y)*inv%mod;
                            w[i+j+d]=(((x-y)*inv%mod)+mod)%mod;
                            break;
                        }
                    }
                }
            }
        }
    }
}
void FWT(ll *a,ll *b,int n){
    for(int f=0;f<3;f++){
        memset(A,0,sizeof(A));memset(B,0,sizeof(B));
        for(int i=0;i<n;i++) A[i]=a[i],B[i]=b[i];
        WT(A,n,1,f);WT(B,n,1,f);
        for(int i=0;i<n;i++) A[i]=(A[i]*B[i])%mod;
        WT(A,n,-1,f);
        for(int i=0;i<n;i++) c[f][i]=(A[i]%mod+mod)%mod;
    }
}

int main(){
    inv=pow(2);
    scanf("%d",&n);n=(1<<n);
    for(int i=0;i<n;i++) scanf("%lld",&a[i]);
    for(int i=0;i<n;i++) scanf("%lld",&b[i]);
    FWT(a,b,n);
    for(int i=0;i<3;i++){
        for(int j=0;j<n;j++) printf("%lld ",c[i][j]);puts("");
    }
    return 0;
}

hdxrie的更简洁的模板IN THERE
其中有一些简单的特判,避免了我的switch语句的麻烦。(感谢hdxrie提供)

而暴力计算其实十分简单,为了方便理解,这里放一份暴力代码。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const int M=1<<19;
const ll mod=998244353ll;
int n;
ll a[M],b[M];
ll cxor[M],cor[M],cand[M];
int main(){
    scanf("%d",&n);
    if(n>13) return 0;
    n=(1<<n);
    for(int i=0;i<n;i++)scanf("%lld",&a[i]);
    for(int i=0;i<n;i++)scanf("%lld",&b[i]);
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            ll now=(a[i]*b[j])%mod;
            (cor[i|j]+=now)%=mod;
            (cxor[i^j]+=now)%=mod;
            (cand[i&j]+=now)%=mod;
        }
    }
    for(int i=0;i<n;i++) printf("%lld ",cor[i]);puts("");
    for(int i=0;i<n;i++) printf("%lld ",cand[i]);puts("");
    for(int i=0;i<n;i++) printf("%lld ",cxor[i]);puts("");
    return 0;
}

End

这里就结束了,如果有不清楚或者错误的地方,欢迎向我提出,我会尽量及时回答与修改(我也是个初学者)

参考

具体应用与题目

参考这篇文章

Thanks♪(・ω・)ノ for reading!

原文地址:https://www.cnblogs.com/VictoryCzt/p/10053379.html