zoukankan      html  css  js  c++  java
  • FFT

    ( ext{Fast Fourier Transformation})(快速傅里叶变换),用来加速多项式乘法。朴素算法的时间复杂度:(O(n^2)),而 ( ext{FFT}) 则为 (nlog n)

    多项式的系数表示法和点值表示法

    ( ext{FFT}) 其实是一个用 (O(nlog_2n)) 的时间将一个用系数表示的多项式转换成它的点值表示的算法。

    系数表示法

    一个 (n) 次的多项式 (f(x)) 可以表示为:

    [f(x)=sum_{i=0}^{n}{a_ix^i} ]

    即可以用每一项的系数来表示 (f(x))

    [f(x)={a_0,a_1,dots,a_{n-1},a_{n} } ]

    点值表示法

    点值表示法是把这个多项式看成一个函数,放在平面直角坐标系中,从上面选取 (n+1) 个点,那么这 (n+1) 个点就可以唯一确定该多项式,也就是有且仅有一个多项式满足:(forall k,f(x_k)=y_k),从而利用这 (n+1) 个点来唯一地表示这个函数。因为,将这 (n+1) 个点的坐标代入,可以得到一个(n+1)(n+1) 元的线性方程组,有唯一解。

    即:

    [f(x)={(x_0,f(x_0)),(x_1,f(x_1)),dots,(x_n,f(x_n)) } ]

    通俗地说,多项式由系数表示法转为点值表示法的过程,就是 ( ext{DFT}) 的过程。相对地,把一个多项式的点值表示法转化为系数表示法的过程,就是 ( ext{IDFT})。而 ( ext{FFT}) 就是通过取某些特殊的 (x) 的点值来加速 ( ext{DFT})( ext{IDFT}) 的过程。

    对于用系数表示的两个多项式,相乘的复杂度为:(O(n^2)),而用点值表示的多项式相乘则需要 (O(n))

    假设两个多项式的点值表示如下:

    [f(x)={(x_0,f(x_0)),(x_1,f(x_1)),dots,(x_n,f(x_n)) }\ g(x)={(x_0,g(x_0)),(x_1,g(x_1)),dots,(x_n,g(x_n)) }\ ]

    若它们的乘积为 (h(x)),则:

    [h(x)={(x_0,f(x_0) imes g(x_0)),(x_1,f(x_1) imes g(x_1)),dots,(x_n,f(x_n) imes g(x_n)) } ]

    目测复杂度为 (O(n))

    但是朴素的系数表示法转点值表示法的算法还是 (O(n^2)) 的,逆过程也是如此,即 ( ext{DFT})( ext{IDFT})

    复数

    对于任意系数多项式转点值,当然可以取任意 (n)(x) 值代入计算,但是暴力计算 (x_k^0,x_k^1,cdots,x_k^{n-1}) 的复杂度还是 (O(n^2)) 的。可以代入一组特殊的 (x) ,用来简化计算。因此,我们不要去算 (x^i),可以发现 (-1)(1) 的幂很好算,但是只有两个完全不够,至少需要 (n) 个,使得 (x) 的若干次方等于 (pm1)。联系到虚数,长度为 (1) 的虚数,不管怎么乘,长度都是 (1),即下图圆(以原点为圆心,(1) 为半径的单位圆)上的点均满足要求。

    同时,在此处要求 (n)(2) 的次幂,且要把圆进行 (n) 等分。

    复数的运算

    设两个复数:(z_1=a+bi,z_2=c+di)

    则:

    [z_1+z_2=(a+c)+(b+d)i\ z_1-z_2=(a-c)+(b-d)i\ z_1 imes z_2=(ac-bd)+(ad+bc)i ]

    复数相加满足平行四边形法则

    复数相乘的小性质

    [(alpha_1, heta_1)*(alpha_2, heta_2)=(alpha_1alpha_2, heta_1+ heta_2) ]

    即模长相乘,极角相加。

    复根

    我们称 (x^n=1) 在复数意义下的解是 (n) 次单位复根,显然这样的解有 (n) 个。设 (omega_n^k=e^{frac{2pi ki}{n}}),则 (x^n=1) 的解集为 ({omega_n^k|k=0,1,cdots,n-1 })。根据复平面的知识,(n) 次单位复根是复平面把单位圆 (n) 等分的第一个角所对应的向量。其他复根均可以用单位复根的幂表示。

    同时,根据欧拉公式,有:

    [omega_n=e^{frac{2pi i}{n}}=cosleft(frac{2pi}{n} ight)+i·sinleft(frac{2pi}{n} ight) ]

    如下图中,(n=4) 时,(omega_n=i) 就是 (4) 次单位复根。

    将圆上的点从 ((1,0)) 开始逆时针进行编号(从 (0) 开始),记编号为 (k) 的点代表的复数值为 (omega_n^k),根据复数相乘的性质,有:

    [(omega^1_n)^k=omega^k_n ]

    其中,(omega^1_n) 称为 (n) 次单位复根,而且每一个 (omega) 都可以求出(欧拉公式)。

    [omega^k_n=cosleft(frac{k}{n}2pi ight)+i· sinleft(frac{k}{n}2pi ight) ]

    那么,(omega_n^0,omega_n^1,omega_n^2,dots,omega_n^{n-1}) 就是要代入 (x_0,x_1,x_2,dots ,x_{n-1}) 的值。

    单位复根的性质

    • (omega_n^k=omega_{2n}^{2k})

    • (omega_n^{k+frac{n}{2}}=-omega_n^k)

    • (omega_n^0=omega_n^n=1)

    上述性质结合图理解应该很好理解,证明的话要利用上面的 (omega_n^k) 的公式。

    以上,我们只是对 (x_i) 取了一些特殊的值,但最后由 (h(x)) 的点表示法求系数表示法时,还是要将值代入 (O(n^2)) 去算。

    分治

    利用分治来进行 ( ext{DFT}) ,就得到了 ( ext{FFT})

    设多项式 (A(x))(n) 为偶数):

    [A(x)=sum_{i=0}^{n-1}{a_ix^i}=a_0+a_1x+a_2x^2+dots+a_{n-1}x^{n-1} ]

    按幂的奇偶来将多项式划分为两部分,然后右边提出一个 (x)

    [f(x)=(a_0+a_2x^2+a_4x^4+dots+a_{n-2}x^{n-2})+x(a_1+a_3x^2+a_5x^4+dots+a_{n-1}x^{n-2}) ]

    可以发现,两边很相似,可以再设两个多项式:(A_1(x),A_2(x)),有:

    [A_1(x)=a_0+a_2x+a_4x^2+dots+a_{n-2}x^{frac{n-2}{2}}\ A_2(x)=a_1+a_3x+a_5x^2+dots+a_{n-1}x^{frac{n-2}{2}} ]

    可以得出:

    [A(x)=A_1(x^2)+xA_2(x^2) ]

    假设 (k<frac{n}{2}),将 (omega_n^k) 作为 (x) 代入到 (A(x)) 中,有:

    [egin{align} A(omega_n^k) &= A_1(omega_n^{2k})+omega_n^kA_2(omega_n^{2k})\ &=A_1(omega_{frac{n}{2}}^k)+omega_{n}^kA_2(omega_{frac{n}{2}}^k) end{align} ]

    再将 (omega_{n}^{k+frac{n}{2}}) 作为 (x) 代入到 (A(x)) 中,有:

    [egin{align} A(omega_{n}^{k+frac{n}{2}}) &=A_1(omega_{n}^{2k+n})+omega_{n}^{k+frac{n}{2}}A_2(omega_{n}^{2k+n})\ &=A_1(omega_{n}^{2k}omega_{n}^{n})-omega_{n}^{k}A_2(omega_{n}^{2k}omega_n^n)\ &=A_1(omega_frac{n}{2}^k)-omega_n^kA_2(omega_{frac{n}{2}}^k) end{align} ]

    对比上述的两个式子可以发现,只是加减的不同,只要求出了 (A_1(omega_frac{n}{2}^k))(omega_n^kA_2(omega_{frac{n}{2}}^k)) 的值,就可以同时获得 (A(omega_n^k))(A(omega_{n}^{k+frac{n}{2}})) 的值。如果采用递归分治的方式来实现,每次回溯的时候只扫前面一半的序列,即可得出后面一半序列的答案。(n=1) 时,只有一个常数,直接( ext{return}) 即可,复杂度:(O(nlog_2n))

    IFFT(快速傅里叶逆变换)

    通过 ( ext{FFT}) ,我们可以得出两个多项式相乘的积的点值表示法,接下来考虑如何将点值表示法转化为系数表示法。

    IDFT

    将单位复根代入多项式后,可得:

    [left[ egin{matrix} y_0\ y_1\ y_2\ y_3\ vdots \ y_{n-1} end{matrix} ight]= left[ egin{matrix} 1 &1 &1 &1 &cdots &1\ 1 &omega_n^1 &omega_n^2 &omega_n^3 &cdots &omega_n^{n-1}\ 1 &omega_n^2 &omega_n^{4} &omega_n^6 &cdots &omega_n^{2(n-1)}\ 1 &omega_n^3 &omega_n^{6} &omega_n^9 &cdots &omega_n^{3(n-1)}\ vdots &vdots &vdots &vdots &ddots &vdots\ 1 &omega_n^{n-1} &omega_n^{2(n-1)} &omega_n^{3(n-1)} &cdots &omega_n^{(n-1)^2}\ end{matrix} ight] left[ egin{matrix} a_0\ a_1\ a_2\ a_3\ vdots\ a_{n-1} end{matrix} ight] ]

    其中,代入的 (x_i=omega_n^i)

    现在,我们已知最左边的值和中间的大矩阵中 (x) 对应的各个值。根据矩阵的知识可以知道,只要在左右两边同时左乘上中间大矩阵的逆矩阵,即可求出系数矩阵。而中间的矩阵的元素十分的特殊,因此其逆矩阵也具有特殊的性质:即每一项取倒数,再乘上 (n) ,就可以得到其逆矩阵(这就是为什么要代入 (omega_n^i) 的原因)。

    为了取倒数,根据单位复根的性质和欧拉公式,有:

    [frac{1}{omega_n}=omega_n^{-1}=e^{-frac{2pi i}{n}}=cosleft(frac{2pi}{n} ight)-i·sinleft(frac{2pi}{n} ight) ]

    其中,(i) 是虚数单位,欧拉公式:(e^{ix}=cos(x)+isin(x))

    取倒数时,每一项乘上单位根的共轭复数即可。然后再分别乘上 (n) 就可以得到逆矩阵。

    结论:一个多项式在分治过程中乘上单位复根的共轭复数,分治完的每一项除以 (n) 即为原多项式的每一项系数。也就说 ( ext{FFT})( ext{IFFT}) 可以同时进行。

    ( ext{C++}) 有自带的复数模板:( ext{complex库})( ext{a.real()}) 即表示复数 (a) 的实部,( ext{a.imag()}) 表示虚部。

    由于递归的写法常数比较大,因此此处只给出非递归的形式。

    观察上图可以发现规律:原来的那个序列,每个数用二进制表示,然后把二进制翻转对称一下,就是最终那个位置的下标。该变换称为 蝴蝶变换。实际上,蝴蝶变换可以 (O(n)) 从小到大递推实现,设 (len=2^k),其中 (k) 表示二进制数的长度,设 (R(x)) 表示长度为 (k) 的二进制数 (x) 翻转后的数(高位补 (0))。那么,我们要求的是:(R(0),R(1),cdots,R(n-1))

    蝴蝶变换

    首先,有 (R(0)=0)

    从小到大求 (R(x)),那么当求 (R(x)) 时,(R(lfloorfrac{x}{2} floor)) 是已知的。因此,我们把 (x) 右移一位(除以 (2)),然后取反,再右移一位,就可以得到 (x) 除了二进制最低位外其它位的翻转结果。考虑最低位翻转的结果,若为 (0) 则,则翻转后最高位是 (0) ;否则为 (1),那么还要加上 (frac{len}{2}=2^{k-1})

    综上所述:

    [R(x)=left lfloor frac{R(lfloor frac{x}{2} floor)}{2} ight floor+(x mod 2) imesfrac{len}{2} ]

    蝴蝶变换模板

    // 需要保证 len 是 2 的幂
    // 记 rev[i] 为 i 翻转后的值
    void change(cp *pn, int len) 
    {
    	for(int i=0;i<len;i++)
            rev[i]=(rev[i>>1]>>1)|((i&1)<<(cnt-1));
        for(int i=0;i<len;i++)
        {
            if(i<rev[i]) swap(pn[i],pn[rev[i]]);
        }
    }
    

    这样的话,就可以预先处理好每个位置的最终位置,再一步步的向上合并,复杂度:(O(n))

    模板

    loj108多项式乘法

    输入 (n)(m),分别表示两个多项式的最高项次数。

    (0leq n,m leq 10^5)

    采用 ( ext{C++}) 自带的 ( ext{complex}) 复数类,但运行没有自己写快。

    #include<bits/stdc++.h>
    #define N 2621450
    #define pi acos(-1) 
    using namespace std;
    typedef complex<double> E;
    int n,m,l,r[N];
    E a[N],b[N];
    void fft(E *a,int f){
        for(int i=0;i<n;i++)if(i<r[i])swap(a[i],a[r[i]]);
        for(int i=1;i<n;i<<=1){
            E wn(cos(pi/i),f*sin(pi/i)); 
            for(int p=i<<1,j=0;j<n;j+=p){
                E w(1,0);
                for(int k=0;k<i;k++,w*=wn){
                    E x=a[j+k],y=w*a[j+k+i];
                    a[j+k]=x+y;a[j+k+i]=x-y;  
                }
            }
        }
    }
    inline int read(){
        int f=1,x=0;char ch;
        do{ch=getchar();if(ch=='-')f=-1;}while(ch<'0'||ch>'9');
        do{x=x*10+ch-'0';ch=getchar();}while(ch>='0'&&ch<='9');
        return f*x;
    }
    int main(){
        n=read();m=read();
        for(int i=0;i<=n;i++)a[i]=read();
        for(int i=0;i<=m;i++)b[i]=read();
        m+=n;for(n=1;n<=m;n<<=1)l++;
        for(int i=0;i<n;i++)r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
        fft(a,1);fft(b,1);
        for(int i=0;i<=n;i++)a[i]=a[i]*b[i];
        fft(a,-1);
        for(int i=0;i<=m;i++)printf("%d ",(int)(a[i].real()/n+0.5));
    }
    

    自己构造结构体表示复数,比直接用库快一点。

    #include <bits/stdc++.h>
    
    using namespace std;
    const int N=1e5+5;
    const double pi=acos(-1);
    int a[N],b[N],rev[N<<2];
    struct cp
    {
        double x,y;
        cp(double tx=0.0,double ty=0.0)
        {
            x=tx;
            y=ty;
        }
        cp operator +(const cp &t)const
        {
            return cp(x+t.x,y+t.y);
        }
        cp operator -(const cp &t)const
        {
            return cp(x-t.x,y-t.y);
        }
        cp operator *(const cp &t)const
        {
            return cp(x*t.x-y*t.y,x*t.y+y*t.x);
        }
    }A[N<<2],B[N<<2];
    void read(int &x)
    {
        x=0;
        int f=1;
        char ch;
        ch=getchar();
        while(!isdigit(ch))
        {
            if(ch=='-') f=-1;
            ch=getchar();
        }
        while(isdigit(ch))
        {
            x=(x<<3)+(x<<1)+ch-'0';
            ch=getchar();
        }
        x*=f;
    }
    void swp(cp &u,cp &v)
    {//交换记得要用引用
        cp t=u;
        u=v;
        v=t;
    }
    //f=1:FFT,f=-1:IFFT
    void FFT(cp *pn,int len,int f)
    {
        for(int i=0;i<len;i++)
            if(i<rev[i]) swp(pn[i],pn[rev[i]]);
        for(int i=1;i<len;i<<=1)//模拟合并过程,根据分治的公式理解
        {
            cp wn(cos(pi/i),f*sin(pi/i));//求单位复根,此时n=2*i
            for(int j=0,d=(i<<1);j<len;j+=d)
            {
                cp w(1,0);
                for(int k=0;k<i;k++)
                {
                    cp u=pn[j+k],v=w*pn[j+k+i];
                    pn[j+k]=u+v,pn[j+k+i]=u-v;
                    w=w*wn;
                }
            }
        }
        if(f==-1)//IFFT
        {
            for(int i=0;i<len;i++)
                pn[i].x/=len;
        }
    }
    int main()
    {
        int n,m;
        read(n),read(m);
        for(int i=0;i<=n;i++) read(a[i]);
        for(int i=0;i<=m;i++) read(b[i]);
        int len=1,cnt=0;
        while(len<n+m+1)//保证len是2的整数次幂
        {
            len<<=1;
            cnt++;
        }
        //先求出变换后位置:
        for(int i=0;i<len;i++)
            rev[i]=(rev[i>>1]>>1)|((i&1)<<(cnt-1));
        for(int i=0;i<len;i++)
        {
            if(i<=n) A[i]=cp(a[i],0);
            else A[i]=cp(0,0);
            if(i<=m) B[i]=cp(b[i],0);
            else B[i]=cp(0,0);
        }
        //系数表示转点值表示:
        FFT(A,len,1);
        FFT(B,len,1);
        //求出最终结果的点值表示:
        for(int i=0;i<len;i++)
            A[i]=A[i]*B[i];
        //点值表示转系数表示:
        FFT(A,len,-1);
        for(int i=0;i<=n+m;i++)
            printf("%d%c",(int)(A[i].x+0.5),i==n+m?'
    ':' ');
        return 0;
    }
    

    参考博客:

    https://blog.csdn.net/enjoy_pascal/article/details/81478582?depth_1-

    https://oi-wiki.org/math/poly/fft/

    FFT 字符串匹配

    FFT还能字符串匹配?

    假设现在又两个字符串:(S,T),令 (n=|S|,m=|T|),且有 (ngeq m),串都从 (0) 开始存。

    (S) 串的从第 (i) 个字符开始和 (T) 串匹配,即:

    [sum_{j=0}^{m-1}{(S[i+j]-T[j])=0} ]

    但是,上式还存在问题。因为受到正负的影响,可能导致 'ab'='ba' 的情况出现。

    因此,对上式进行改变,有:

    [sum_{j=0}^{m-1}{(S[i+j]-T[j])^2} (0leq j<m) ]

    当其为 (0) 时,就可以表示 (S) 的一个子串和 (T) 匹配。

    但是上式仍然没有什么特殊之处,继续变换。将 (T) 串进行前后翻转,那么当 (S) 从位置 (i) 开始和 (T) 串匹配时,就有:(S[i]=T[m-1],S[i+j]=T[m-1-j],cdots) ,因此当满足下列公式:

    [sum_{j=0}^{m-1}{(S[i+j]-T[m-1-j])^2}=0 ]

    可以说明达到匹配。

    可以发现:(i+j+m-1-j=m-i-1) 是一个定值,即多项式乘法 (F=S*T)(F(m-i-1)) 就可以表示 (S[i,i+1,cdots]) 和串 (T[0,1,2,cdots,m-1]) 的匹配情况。将平方拆开,得到 (3) 个多项式的和的形式:

    [sum_{j=0}^{m-1}{(S[i+j]-T[m-1-j])^2}=sum_{j=0}^{m-1}{S[i+j]^2}+sum_{j=0}^{m-1}{T[m-1-j]^2}-2·sum_{j=0}^{m-1}{(S[i+j]*T[m-1-j])} ]

    前两项,可以直接预处理前缀和。对于后面的一项,可以令:

    [g(i)=2*sum_{j=0}^{m-1}{(S[i+j]*T[m-1-j])} ]

    枚举 (i) ,求出每个 (g(i)),这个就可以用 ( ext{FFT}) 来优化了,复杂度:(O(nlog_2n))

    复杂度还没有KMP优秀

    Rock Paper Scissors Lizard Spock.

    https://nanti.jisuanke.com/t/A1676

    枚举胜利的情况,把答案累加起来。此时,只需要计算 (g(i)) 即可(我把整个式子都算出来,死活调不对)。

    代码

    #include <bits/stdc++.h>
    
    using namespace std;
    typedef pair<char,char>pcc;
    const double pi=acos(-1);
    const int N=1e6+5;
    typedef long long ll;
    char s[N],t[N];
    pcc f[13];
    int rev[N<<2],res[N<<2];
    struct cp
    {
        double x,y;
        cp (double tx=0.0,double ty=0.0)
        {
            x=tx;
            y=ty;
        }
        cp operator +(const cp &t)const
        {
            return cp(x+t.x,y+t.y);
        }
        cp operator -(const cp &t)const
        {
            return cp(x-t.x,y-t.y);
        }
        cp operator *(const cp &t)const
        {
            return cp(x*t.x-y*t.y,x*t.y+y*t.x);
        }
    }A[N<<2],B[N<<2];
    void init()
    {//胜利的情况
        f[1]=make_pair('S','P');
        f[2]=make_pair('S','L');
        f[3]=make_pair('P','R');
        f[4]=make_pair('P','K');
        f[5]=make_pair('R','L');
        f[6]=make_pair('R','S');
        f[7]=make_pair('L','P');
        f[8]=make_pair('L','K');
        f[9]=make_pair('K','R');
        f[10]=make_pair('K','S');
    }
    void swp(cp &u,cp &v)
    {
        cp t=u;
        u=v;
        v=t;
    }
    void FFT(cp *pn,int len,int f)
    {
        for(int i=0;i<len;i++)
            if(i<rev[i]) swp(pn[i],pn[rev[i]]);
        for(int i=1;i<len;i<<=1)
        {
            cp wn(cos(pi/i),f*sin(pi/i));
            for(int j=0,d=(i<<1);j<len;j+=d)
            {
                cp w(1,0);
                for(int k=0;k<i;k++)
                {
                    cp u=pn[j+k],v=w*pn[j+k+i];
                    pn[j+k]=u+v,pn[j+k+i]=u-v;
                    w=w*wn;
                }
            }
        }
        if(f==-1)
        {
            for(int i=0;i<len;i++)
                pn[i].x/=len;
        }
    }
    int main()
    {
        init();
        scanf("%s",s);
        scanf("%s",t);
        int n=strlen(s),m=strlen(t);
        int ans=0,len=1,maxn=n+m+1,cnt=0;
        while(len<maxn)
        {
            len<<=1;
            cnt++;
        }
        for(int i=0;i<len;i++)
            rev[i]=(rev[i>>1]>>1)|((i&1)<<(cnt-1));
        reverse(t,t+m);
        for(int i=1;i<=10;i++)
        {
            for(int j=0;j<len;j++)
                A[j]=cp(0,0),B[j]=cp(0,0);
            for(int j=0;j<m;j++)
            {
                if(t[j]==f[i].first)
                    A[j]=cp(1,0);
            }
            for(int j=0;j<n;j++)
            {
                if(s[j]==f[i].second)
                    B[j]=cp(1,0);
            }
            FFT(A,len,1);
            FFT(B,len,1);
            for(int j=0;j<len;j++) A[j]=A[j]*B[j];
            FFT(A,len,-1);
            for(int j=0;j<=len;j++)
                res[j]+=(int)(A[j].x+0.5);
        }
        for(int i=m-1;i<n;i++)//根据公式,i从m-1开始
            ans=max(ans,res[i]);
        printf("%d
    ",ans);
        return 0;
    }
    
  • 相关阅读:
    GNU build system / autoconf 和 automake 生成 Makefile 文件
    使用you-get下载网站内嵌的视频
    ffmpeg偷懒
    GB28181的PS流完全分析(封装 / 分包发送 / 接收组包 / 解析)
    JRE与JDK的区别
    图解SQL的inner join、left join、right join、full outer join、union、union all的区别
    linux常用查看日志命令
    Hadoop2.6.2的Eclipse插件的使用
    path与classpath区别(转)
    在Eclipse上运行Spark(Standalone,Yarn-Client)
  • 原文地址:https://www.cnblogs.com/1024-xzx/p/13757355.html
Copyright © 2011-2022 走看看