拆系数FFT学习笔记

拆系数FFT学习笔记

拆系数FFT

当题目中取模的数不是NTT模数的时候,我们无法利用原根来进行快速数论变换,这个时候就要用到毛啸论文里提到的拆系数FFT。

大致思路

拆系数FFT实际上是将多项式卷积之后的值具体算出来,普通的FFT由于精度误差较大,当然无法胜任。

于是可以考虑将一个较大的数拆成两个较小的数,即将 x x x拆成 a × t + b a \times t + b a×t+b,这样相当于将 x x x t t t进制划分,在常数 t t t取根号级别的时候, a , b a,b a,b的大小也是根号级别的,这样我们再用精度较高的浮点数就可以直接FFT来运算了。

例如:

x = a 1 t + b 1 y = a 2 t + b 2 x y = ( a 1 t + b 1 ) ( a 2 t + b 2 ) = a 1 a 2 t 2 + ( a 1 b 2 + a 2 b 1 ) t + a 2 b 2 \begin{aligned} x&=a_1t+b_1\\ y&=a_2t+b_2\\ xy&=(a_1t+b_1)(a_2t + b_2)\\ &=a_1a_2t^2 + (a_1b_2+a_2b_1)t+a_2b_2 \end{aligned} xyxy=a1t+b1=a2t+b2=(a1t+b1)(a2t+b2)=a1a2t2+(a1b2+a2b1)t+a2b2

a 1 b 1 , ( a 1 b 2 + a 2 b 1 ) , a 2 b 2 a_1b_1,(a_1b_2+a_2b_1),a_2b_2 a1b1,(a1b2+a2b1),a2b2对应算出来再各自乘上相应的系数即可。

这样需要做8次dft,发现中间有两个部分系数相同,对于这两个系数相同的部分我们可以直接点值操作之后再做一次dft即可,这样dft的次数就降到了7次了。

优化

DFT

上面的方法好像常数很大,很多时候需要用到两次DFT合并为一次的方法。

考虑一个多项式A的DFT本质上求的是 A ( ω n k ) = ∑ j = 0 n − 1 A j × ω n k j A(\omega_n^k)=\sum_{j=0}^{n-1}A_{j}\times \omega_n^{kj} A(ωnk)=j=0n1Aj×ωnkj,现在我们需要同时求出A和B,也就是算出 A ( ω n k ) , B ( ω n k ) A(\omega_n^k),B(\omega_n^k) A(ωnk),B(ωnk)

假设我们知道了 F p [ k ] = A ( ω n k ) + i B ( ω n k ) , F q [ k ] = A ( ω n k ) − i B ( ω n k ) F_p[k]=A(\omega_n^k)+iB(\omega_n^k),F_q[k]=A(\omega_n^k)-iB(\omega_n^k) Fp[k]=A(ωnk)+iB(ωnk),Fq[k]=A(ωnk)iB(ωnk),我们就可以通过 F p [ k ] F_p[k] Fp[k] F q [ k ] F_q[k] Fq[k]来直接求出 A ( ω n k ) , B ( ω n k ) A(\omega_n^k),B(\omega_n^k) A(ωnk),B(ωnk)了。

于是为了合并两次DFT,我们希望 F p F_p Fp F q F_q Fq之间能够存在某一种关系,使得求出了某一个之后可以快速求出另外一个。

于是我们来推导 F p F_p Fp F q F_q Fq的关系,为了方便下面的公式的书写,我们约定 X = 2 π k j n X=\frac{2\pi kj}{n} X=n2πkj

F q [ k ] = A ( ω n k ) − i B ( ω n k ) = ∑ j = 0 n − 1 A j ω n k j − i B j ω n k j = ∑ j = 0 n − 1 ( A j − i B j ) ( cos ⁡ X + i sin ⁡ X ) = ∑ j = 0 n − 1 ( A j cos ⁡ X + B j sin ⁡ X ) + i ( A j sin ⁡ X − B j cos ⁡ X ) F p [ n − k ] = A ( ω n − k ) + i B ( ω n − k ) = ∑ j = 0 n − 1 A j ω n − k j + i B j ω n − k j = ∑ j = 0 n − 1 ( A j + i B j ) ( cos ⁡ X − i sin ⁡ X ) = ∑ j = 0 n − 1 ( A j cos ⁡ X + B j sin ⁡ X ) − i ( A j sin ⁡ X − B j cos ⁡ X ) \begin{aligned} F_q[k]&=A(\omega_n^k)-iB(\omega_n^k)\\ &=\sum_{j=0}^{n-1}A_j\omega_n^{kj}-iB_j\omega_n^{kj}\\ &=\sum_{j=0}^{n-1}(A_j-iB_j)(\cos X + i\sin X)\\ &=\sum_{j=0}^{n-1}(A_j\cos X + B_j\sin X)+i(A_j\sin X-B_j\cos X)\\ F_p[n-k]&=A(\omega_n^{-k})+iB(\omega_n^{-k})\\ &=\sum_{j=0}^{n-1}A_j\omega_n^{-kj}+iB_j\omega_n^{-kj}\\ &=\sum_{j=0}^{n-1}(A_j+iB_j)(\cos X -i\sin X)\\ &=\sum_{j=0}^{n-1}(A_j\cos X + B_j\sin X)-i(A_j\sin X-B_j\cos X)\\ \end{aligned} Fq[k]Fp[nk]=A(ωnk)iB(ωnk)=j=0n1AjωnkjiBjωnkj=j=0n1(AjiBj)(cosX+isinX)=j=0n1(AjcosX+BjsinX)+i(AjsinXBjcosX)=A(ωnk)+iB(ωnk)=j=0n1Ajωnkj+iBjωnkj=j=0n1(Aj+iBj)(cosXisinX)=j=0n1(AjcosX+BjsinX)i(AjsinXBjcosX)

我们发现 F p [ n − k ] F_p[n-k] Fp[nk] F q [ k ] F_q[k] Fq[k]在本质上就是共轭的,于是通过 F p F_p Fp来计算 F q F_q Fq就非常方便了。

简单分析一下原因,由于 ω n n − k = ω n − k \omega_{n}^{n-k}=\omega_n^{-k} ωnnk=ωnk,而 ω n k \omega_{n}^{k} ωnk ω n − k \omega_n^{-k} ωnk实部和虚部的元素都是相同的,只是符号相反而已, F p [ k ] F_p[k] Fp[k] F q [ k ] F_q[k] Fq[k]的系数刚好也是符号相反,也就导致 F p [ k ] , F p [ n − k ] , F q [ k ] , F q [ n − k ] F_p[k],F_p[n-k],F_q[k],F_q[n-k] Fp[k],Fp[nk],Fq[k],Fq[nk]四个项的它们本质的结果只是一些元素的线性组合,这样从 F p F_p Fp推出 F q F_q Fq也就不是那么难了。

IDFT

求出了点值之后我们可以快速将点值乘起来,但是要怎么IDFT回去?

对于两个点值序列 A , B A,B A,B,我们仍然可以用刚才提到的方法,将每一项合并成 A + i B A+iB A+iB,将新的多项式IDFT之后得到的系数的实部和虚部就对应了真正 A A A B B B了。

/*=======================================
 * Author : ylsoi
 * Time : 2019.3.14
 * Problem : luogu4245
 * E-mail : ylsoi@foxmail.com
 * ====================================*/
#include<bits/stdc++.h>

#define REP(i,a,b) for(int i=a,i##_end_=b;i<=i##_end_;++i)
#define DREP(i,a,b) for(int i=a,i##_end_=b;i>=i##_end_;--i)
#define debug(x) cout<<#x<<"="<<x<<" "
#define fi first
#define se second
#define mk make_pair
#define pb push_back
#define double long double
typedef long long ll;

using namespace std;

void File(){
    freopen("luogu4245.in","r",stdin);
    freopen("luogu4245.out","w",stdout);
}

template<typename T>void read(T &_){
    _=0; T fl=1; char _c=getchar();
    for(;!isdigit(_c);_c=getchar())if(_c=='-')fl=-1;
    for(;isdigit(_c);_c=getchar())_=(_<<1)+(_<<3)+(_c^'0');
    _*=fl;
}

const int maxn=1e5+10;
const double pi=acos(-1);
int n,m,mod,a[maxn<<2],b[maxn<<2],c[maxn<<2];

namespace Poly{
    struct cp{
        double x,y;
        cp(double xx=0,double yy=0){x=xx,y=yy;}
        cp conj(){return cp(this->x,-this->y);}
        friend cp operator + (cp a,cp b){return (cp){a.x+b.x,a.y+b.y};}
        friend cp operator - (cp a,cp b){return (cp){a.x-b.x,a.y-b.y};}
        friend cp operator * (cp a,cp b){return (cp){a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x};}
        friend cp operator * (cp a,double b){return cp(a.x*b,a.y*b);}
        friend cp operator / (cp a,double b){return cp(a.x/b,a.y/b);}
    }om[maxn<<2],aa[maxn<<2],bb[maxn<<2],cc[maxn<<2],dd[maxn<<2];
    int lim,cnt,dn[maxn<<2];
    inline void dft(cp *A,int ty){
        if(ty==-1)reverse(A+1,A+lim);
        REP(i,0,lim-1)if(i<dn[i])swap(A[i],A[dn[i]]);
        for(int len=1;len<lim;len<<=1){
            cp w=om[len<<1];
            for(int L=0;L<lim;L+=len<<1){
                cp wk=cp(1,0);
                REP(i,L,L+len-1){
                    cp u=A[i],v=A[i+len]*wk;
                    A[i]=u+v;
                    A[i+len]=u-v;
                    wk=wk*w;
                }
            }
        }
        if(ty==-1)REP(i,0,lim-1)A[i]=A[i]/lim;
    }
    void mtt(int *A,int *B,int *C,int la,int lb){
        REP(i,0,lim-1){
            dn[i]=dn[i>>1]>>1|((i&1)<<(cnt-1));
            aa[i]= i<=la ? cp(A[i]>>15,A[i]&32767) : cp(0,0);
            bb[i]= i<=lb ? cp(B[i]>>15,B[i]&32767) : cp(0,0);
        }
        dft(aa,1),dft(bb,1);
        REP(i,0,lim-1){
            int j=(lim-i)&(lim-1);
            cp a1=(aa[i]+aa[j].conj())*cp(0.5,0);
            cp a0=(aa[i]-aa[j].conj())*cp(0,-0.5);
            cp b1=(bb[i]+bb[j].conj())*cp(0.5,0);
            cp b0=(bb[i]-bb[j].conj())*cp(0,-0.5);
            cc[i]=a1*b1+a1*b0*cp(0,1);
            dd[i]=a0*b1+a0*b0*cp(0,1);
        }
        dft(cc,-1),dft(dd,-1);
        REP(i,0,lim-1){
            int a1=(ll)(cc[i].x+0.5)%mod;
            int b1=(ll)(cc[i].y+0.5)%mod;
            int c1=(ll)(dd[i].x+0.5)%mod;
            int d1=(ll)(dd[i].y+0.5)%mod;
            C[i]=(((ll)a1<<30)+((ll)(b1+c1)<<15)+(ll)d1)%mod;
        }
    }
}

int main(){
    File();

    read(n),read(m),read(mod);
    REP(i,0,n)read(a[i]);
    REP(i,0,m)read(b[i]);

    using namespace Poly;

    lim=1,cnt=0;
    while(lim<=n+m)lim<<=1,++cnt;
    if(!cnt)cnt=1;
    REP(i,0,lim-1)dn[i]=dn[i>>1]>>1|((i&1)<<(cnt-1));
    for(int i=lim;i;i>>=1)
        om[i]=cp(cos(2.0*pi/i),sin(2.0*pi/i));

    Poly::mtt(a,b,c,n,m);
    REP(i,0,n+m)printf("%d ",(c[i]+mod)%mod);

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值