• NTT&FFT(快速?变换,以及扩展)


    FFT&NTT(以及扩展)

    预备知识:用于NTT

    NTT/FFT其实本质相同,用途是快速求解 多项式乘积

    前言

    FT: 傅里叶变换:

    这是一个工程上的概念,可以简述为:一个周期性的信号波段可以用 若干个正弦曲线 的带权和表示

    DFT: 离散傅里叶变换,这是傅里叶变换在离散情况下的变种

    FFT: 快速傅里叶变换

    NTT: 快速数论变换

    [ ]

    谈及核心思想

    1.单位根:

    构造(omega_n)(n)阶单位根(不知道(omega_n)的值域),满足性质(omega_n^n=omega_n^0=1)

    对于(2|n),(omega _n^{frac{n}{2}}=-1)

    显然(omega_n)满足一个非常简单的性质:折半引理 $egin{aligned} forall 2|iand 2|n , omega_ni=omega_{frac{n}{2}}{frac{i}{2}}end{aligned} $

    (omega_n)实际上是一个在幂次上呈现(n)元循环的数值

    2.多项式点值式的转化

    一个(n)阶多项式最普通的表示就是(F(x)=sum_{i=0}^{n-1} a_ix^i)

    然而,多项式也可以用(n)互不相关的点表示,即((x_0,y_0),(x_1,y_1),cdots,(x_{n-1},y_{n-1}))

    两者可以互相转化

    对于同(x_i)的点值,两个多项式卷积时,其(y_i)可以直接对应相乘

    FFT/NTT的核心过程是

    多项式(longrightarrow) 点值式(longrightarrow)点值式对应相乘(longrightarrow)多项式

    而用单位根来构造快速的多项式与点值式的转化

    3.分治思想

    用于降低多项式与点值式转换的复杂度

    [ ]

    FFT的单位根

    ((x,y))指复数(i=sqrt{-1},(x,y)=x+yi)

    基本运算((x,y)+(a,b)=(x+a,y+b),(x,y)cdot (a,b)=(ax-by,ay+bx))

    FFT的单位根是:(omega_n)=((cos(frac{2pi}{n}),sin(frac{2pi}{n})))

    (omega _n^i=(cos(frac{2pi}{n}cdot i),sin(frac{2pi}{n}cdot i))) (展开发现就是三角函数求和公式)

    显然满足单位根的性质​

    (实际上可以发现,这个说是点值其实就是信号序列的三角函数表示)

    [ ]

    NTT

    相信您已经了解了原根的一些性质,( ext{NTT})的单位根常用原根构造

    ( ext{NTT})的单位根实际有较大的局限性,对于质数(P)只能构造出(n|P-1,omega_n=g^{frac{P-1}{n}})

    计算在模意义下就能满足单位根的性质

    通常我们(P)(998244353)(2^{23}|(P-1)),它的一个原根是3

    实际上,为了满足下面分治需要,构造的模数通常满足(P-1=scdot 2^t)(t)较大,这类模数我们常称作( ext{NTT})模数

    [ ]

    [ ]

    以上部分均为基础知识,相对来说应该不会太难,下面是主要难点

    [ ]

    多项式转点值式

    接下来我们考虑如何将多项式转化为点值式

    对于点值式,我们构造的点横坐标为(x_i=omega_n^i)

    具体目标是对于函数(F(x)),求出在(x_0,x_1,cdots ,x_{n-1})上的函数值

    即求出(F(x_i)=a_0omega_n^0+a_1omega_n^{i}+a_2omega_n^{2i}+cdots)

    接下来就是核心的分治思想,注意,这里的分治是子问题严格等大

    对于当前问题,分成两部分子问题求解(实际是可以分成多部分的,但是这个是特殊情况暂时不予讨论),即求解

    (m=frac{n}{2})

    $egin{aligned} 2|i,G(x_i)=a_0omega_{m}0+a_2omega_{m}{frac{i}{2}}+a_4omega_{m}^{frac{i}{2}cdot 2}+cdotsend{aligned} $

    $egin{aligned} 2|i,H(x_i)=a_1omega_{m}0+a_3omega_{m}{frac{i}{2}}+a_5omega_{m}^{frac{i}{2}cdot 2}+cdotsend{aligned} $

    更简洁的描述为

    (i<m,G(x_i')=a_0omega_{m}^0+a_2omega_{m}^{i}+a_4omega_{m}^{2i}+cdots)

    (i<m,H(x_i')=a_1omega_{m}^0+a_3omega_{m}^{i}+a_5omega_{m}^{2i}+cdots)

    由于(G(x'_i),H(x'_i))计算的是([0,m-1])项,而求(F(x_i))时用到的是(0,2,4,cdots)项,实际需要访问(G(x^2_i),H(x^2_i))

    (F(x_i))的式子比较,我们得到合并的式子为

    (F(x_i)=G(x^2_i)+x_i H(x^2_i))

    带入折半引理,实际等价于

    (F(x_i)=G(x'_i)+x_i H(x'_i))

    注意(x_i=x'_{imod m})

    为了保证复杂度,尽量使得每次分治的子问题都分为两部分,这样的复杂度为(O(nlog n))

    附:实际上,分为(d)个子问题时,每次合并的复杂度为(O(ncdot d)),因此复杂度为

    保证每次分治为两个严格等大的子问题,可以从一开始就把(n)扩充为(2)的幂次

    int N=1;
    while(N<=n+m) N<<=1;
    

    附:(d)个子问题时,设子问题答案为(G_j(x_i)),则合并的式子为

    (egin{aligned} F(x_i)=sum_{j=0}^{d-1}x_i^jG_j(x_i^d)=sum_{i=0}^{d-1}x_i^jG_j(x'_{imod frac{n}{d}})end{aligned})

    点值式转多项式

    一个简单的性质:单位根反演 (sum_{j=0}^{n-1}omega_n^{ij}= left{egin{aligned} frac{omega_n^{in}-1}{omega_n^i-1}=0 && i e 0\ n && i=0end{aligned} ight.)

    设点值式对应(y_i)的序列为(b_i)

    (ncdot a_i=sum_{j=0}^{n-1}omega_n^{-ij} b_j)

    证明

    $egin{aligned} sum_{j=0}{n-1}omega_n{-ij}b_j=sum_{j=0}^{n-1} omega_n{-ij}(sum_{k=0}{n-1}a_komega_n^{jk})end{aligned} $

    $egin{aligned} sum_{j=0}{n-1}omega_n{-ij}b_j= sum_{k=0}{n-1}a_ksum_{j=0}{n-1}omega_n^{j(k-i)} end{aligned} $

    由上面的式子,发现只有(k-i=0)时右边的求和式有值,所以上式成立

    因此点值式转多项式直接把系数改为(omega_n^{-i})即可

    [ ]

    [ ]

    Tips:

    1.由于单位根的循环特性,溢出会直接溢出到本来的式子里

    因此,如果乘法过后的多项式产生了超过(>n)的项(x^i),会溢出到(x^{imod n})

    2.点值式并不是不满足除法,只是除法得到的多项式并不一定是一个(n)元以内的多项式,除了恰好整除的情况,得到的通常是一个无穷级数的式子,如(egin{aligned} frac{1}{1-x}=frac{1-x^{infty}}{1-x}=sum_{i=0}^{infty}x^iend{aligned})

    真正要求除法,通常是求前(n)项的结果,即需要用到多项式乘法逆

    [ ]

    代码实现与优化

    模板题传送门

    然后我们得到一份优美的代码(FFT)

    (Complex是C++库自带的复数,M_PI是C++自带(pi)常量)

    void FFT(int n,Complex *a,int f) {
    	if(n==1) return;
    	Complex tmp[N];
    	int m=n/2;
    	rep(i,0,m-1) tmp[i]=a[i<<1],tmp[i+m]=a[i<<1|1]; // 按照奇偶分类
    	memcpy(a,tmp,sizeof(Complex) * n);
    	FFT(m,a,f),FFT(m,a+m,f); // 分两半,算g(x),h(x)
    	Complex w(cos(2*M_PI/n),f*sin(2*M_PI/n)),e(1,0); // w=x^1,e=x^i
    	rep(i,0,m-1) {
    		tmp[i]=a[i]+e*a[i+m]; // f(x_i)=g(x_i)+e*h(x_i)
    		e=e*w; 
    	}
    	rep(i,m,n-1) {
    		tmp[i]=a[i-m]+e*a[i];
    		e=e*w;
    	}
    	memcpy(a,tmp,sizeof(T)*n);
    }
    

    由于((omega_n)^{frac{n}{2}}=-1),所以还可以简化为

    	Complex w(cos(2*M_PI/n),f*sin(2*M_PI/n)),e(1,0);
    	rep(i,0,m-1) {
    		tmp[i]=a[i]+e*a[i+m];
    		tmp[i+m]=a[i]-e*a[i+m];
    		e=e*w;
    	}
    

    由于用了double,最后输出要取整

    蝴蝶优化

    我们加一点优化,取代递归的分治过程

    可以看到,分治时我们按照(i mod 2)分成两组,然后继续分

    这个过程中,实际上我们就是将(i)的二进制位前后翻转

    所以我们可以暴力处理出(i)分治底层的位置

    rep(i,0,n-1) {
    	int x=i,s=0;
    	for(int j=1;(j<<c)<=n;++j) {
    		s=(s<<1)|(x&1);
    		x>>=1;
    	} // s就是最终位置
    }
    

    当然也是有(O(n))处理方法的

    int N=1,c=-1;
    while(N<=n+m) N<<=1,c++;
    rep(i,1,N-1) rev[i]=(rev[i>>1]>>1)|((i&1)<<c);
    

    (建议自己模拟一下)

    有了这个翻转数组,我们可以直接从分治底层开始解决整个问题,每次合并操作完全相同

    每次分治问题的大小,依次合并每一个子问题区间即可

    为了在一个数组上完成操作,还需要注意合并顺序

    代码解释(i):分治子问题大小为(2i)(l):合并区间的左端点为(l),右端点为(l+2i)(j)枚举合并位置

    void FFT(int n,Complex *a){
        for(int i=1;i<n;i<<=1) {
            Complex w(cos(2*M_PI/n),f*sin(2*M_PI/n));
            for(int l=0;l<n;l+=i*2) {
                Complex e(1,0);
                for(int j=l;j<l+i;++j,e=e*w) {
                    Complex t=a[j+m]*e;   // a'[j]=a[j]+e*a[j+m]
                                          // a'[j+i]=a[j]-e*a[j+m]
                    a[j+m]=a[j]-t;
                    a[j]=a[j]+t;
                }
            }
        }
    }
    

    事实上我们还有更快的写法,就是将(omega_n^i)预处理出来

    (注意这个( ext{FFT})的预处理很考验double精度,不能每次都直接累乘上去,隔几个就要重新调用依次三角函数)

    当然如果自己写复数会更快

    [ ]

    关于点值式转多项式的优化

    由于每次求得点值是(omega_n^{-i}=omega_n^{n-i})

    所以可以直接用 多项式转点值式的函数, 最后把([1,n-1])这一段翻转,每个数除掉(n)即可

    [ ]

    对于加减运算取模的优化

    三目运算

    a+=b,a=a>=P?a-P:a;
    a-=b,a=a<0?a+P:a;
    

    逻辑运算优化(原理是逻辑预算会在第一个确定表达式值的位置停下)

    a+=b,((a>=P)&&(a-=P));
    a-=b,((a<0)&&(a+=P));
    

    [ ]

    关于系数预处理优化(以NTT为例)

    带入上面已经提到的优化,无预处理系数的( ext{NTT})大概是这样的

    ll qpow(ll x,ll k=P-2){
        ll res=1;
        for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
        return res;
    }
    int a[N];
    void NTT(int n,int *a,int f){
        rep(i,1,n-1) if(i<rev[i]) swap(a[i],a[rev[i]]);
        for(int i=1;i<n;i<<=1) {
            int w=qpow(3,(P-1)/i/2);
            for(int l=0;l<n;l+=i*2) {
                int e=1;
                for(int j=l;j<l+i;++j,e=1ll*e*w%P) {
                    int t=1ll*a[j+i]*e%P;
                    a[j+i]=a[j]-t,((a[j+i]<0)&&(a[j+i]+=P));
                    a[j]+=t,((a[j]>=P)&&(a[j]-=P));
                }
            }
        }
        if(f==-1) {
            reverse(a+1,a+n);
            int Inv=qpow(n);
            rep(i,0,n-1) a[i]=1ll*a[i]*Inv%P;
        }
    }
    

    一种简单的预处理是,每次对于每个分治大小,预处理依次系数

    ll qpow(ll x,ll k=P-2){
        ll res=1;
        for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
        return res;
    }
    int a[N],e[N];
    void NTT(int n,int *a,int f){
        rep(i,1,n-1) if(i<rev[i]) swap(a[i],a[rev[i]]);
        e[0]=1;
        for(int i=1;i<n;i<<=1) {
            int w=qpow(3,(P-1)/i/2);
            for(int j=1;j<i;++j) e[j]=1ll*e[j-1]*w%P;
            //for(int j=i-2;j>=0;j-=2) e[j+1]=1ll*w*(e[j]=e[j>>1])%P;
            //这个版本是沿用上一次预处理的结果,实际(只有)用这种预处理方法可以极大程度上加强FFT的精度
            for(int l=0;l<n;l+=i*2) {
                for(int j=l;j<l+i;++j) {
                    int t=1ll*a[j+i]*e[j-l]%P;
                    a[j+i]=a[j]-t,((a[j+i]<0)&&(a[j+i]+=P));
                    a[j]+=t,((a[j]>=P)&&(a[j]-=P));
                }
            }
        }
        if(f==-1) {
            reverse(a+1,a+n);
            int Inv=qpow(n);
            rep(i,0,n-1) a[i]=1ll*a[i]*Inv%P;
        }
    }
    

    另一种是在一开始就把所有的系数用一个数组存下来,具体过程可以描述为

    对于每个分治长度(n),我们只需要访问(omega_n^{0},omega_n^{1},cdots,omega_n^{frac{n}{2}-1})

    那么对于分治长度(n),我们在(w)数组的第(frac{n}{2}) ~ (n-1)项依次存储这些值

    优化:我们只需要对于最大的分治长度处理,剩下的部分发现可以直接用折半引理访问得到

    ll qpow(ll x,ll k=P-2){
        ll res=1;
        for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
        return res;
    }
    const int N=1<<21;
    int a[N],w[N];
    void Init(){
        w[N>>1]=1;
        int t=qpow(3,(P-1)/N);
        rep(i,(N>>1)+1,N-1) w[i]=1ll*w[i-1]*t%P;
        drep(i,(N>>1)-1,1) w[i]=w[i<<1];
    }
    void NTT(int n,int *a,int f){
        rep(i,1,n-1) if(i<rev[i]) swap(a[i],a[rev[i]]);
        for(int i=1;i<n;i<<=1) {
            int *e=w+i;
            for(int l=0;l<n;l+=i*2) {
                for(int j=l;j<l+i;++j) {
                    int t=1ll*a[j+i]*e[j-l]%P;
                    a[j+i]=a[j]-t,((a[j+i]<0)&&(a[j+i]+=P));
                    a[j]+=t,((a[j]>=P)&&(a[j]-=P));
                }
            }
        }
        if(f==-1) {
            reverse(a+1,a+n);
            int Inv=qpow(n);
            rep(i,0,n-1) a[i]=1ll*a[i]*Inv%P;
        }
    }
    

    三份代码在duck.ac上的评测结果表明,不预处理系数将近慢一倍

    单组数据来看,预处理系数会慢一点

    多组来看,预处理系数会快

    实际差距不大,都可以使用

    但是在某些层面来说,下面这份板子才是最好的(适用NTT,FFT且精度较高),不需要预处理

    ll qpow(ll x,ll k=P-2){
        ll res=1;
        for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
        return res;
    }
    int a[N],e[N];
    void NTT(int n,int *a,int f){
        rep(i,1,n-1) if(i<rev[i]) swap(a[i],a[rev[i]]);
        e[0]=1;
        for(int i=1;i<n;i<<=1) {
            int w=qpow(3,(P-1)/i/2);
            for(int j=i-2;j>=0;j-=2) e[j+1]=1ll*w*(e[j]=e[j>>1])%P;
            for(int l=0;l<n;l+=i*2) {
                for(int j=l;j<l+i;++j) {
                    int t=1ll*a[j+i]*e[j-l]%P;
                    a[j+i]=a[j]-t,((a[j+i]<0)&&(a[j+i]+=P));
                    a[j]+=t,((a[j]>=P)&&(a[j]-=P));
                }
            }
        }
        if(f==-1) {
            reverse(a+1,a+n);
            int Inv=qpow(n);
            rep(i,0,n-1) a[i]=1ll*a[i]*Inv%P;
        }
    }
    

    [ ]

    拓展

    1.分治+NTT

    常用于处理多个计数背包的快速合并 (实际无权值01背包也是可以的)

    我们可以用NTT(nlog n)合并两个大小为(n)的背包

    分治时,每次合并两个分治子问题,总共的时间就是(sum sizelog n)

    每个背包的(size)会被计算(log n)次,所以总共复杂度是(n log ^2 n)

    [ ]

    2.CDQ+NTT

    模板题传送门

    对于形如(dp_i=sum_{j=0}^{i-1}dp_jg_{i-j})(dp)转移(就是dp转移与差值有关)

    由于求(dp_i)时,需要保证(dp_0,dp_1,cdots,dp_{i-1})才能卷积,这个限制,我们可以用CDQ分治解决

    对于当前分治区间([L,R])

    依次考虑([L,mid])内部转移,([L,mid])([mid+1,R])的转移(用FFT/NTT解决),([mid+1,R])内部转移

    算法流程

    void Solve(l,r){
        if(l==r) return;
        mid=(l+r)>>1;
        Solve(l,mid);
        (l,mid)->(mid+1,r);
        Solve(mid+1,r);
    }
    

    [ ]

    3.MTT(任意模数NTT)

    [ ]

    4.(n)元点值式

    [ ]

    练习建议:

    1.高精度乘法

    2.简单应用:HDU-4609 题解

    3.卷积构造模板: BZOJ-3527 题解

    4.拓展卷积构造:HDU-5885 题解

    5.构造卷积的应用:HDU-6061 题解

    6.(CDQ)分治+(FFT)HDU-5730 题解

    7.(CDQ)+NTT/降次前缀和优化(dp)HDU-5332 题解

    8.容斥+(MTT)HDU-6088 题解

    9.图上(dp)

    联通图个数:BZOJ-3456 题解

    带环联通图个数:HDU-5552 题解

    森林数量和带限制森林数量:HDU - 5279 题解

    10.点分治+FFT:CodeChef-PRIMEDST 题解

    [ ]

    [ ]

    [ ]

    更多应用和优化参见毛啸2016论文

    (如:两次FFT做卷积,4次FFT做MTT。。。)

  • 相关阅读:
    java复习计划
    超过16位的字符串装16进制
    《将博客搬至CSDN》
    android设置中文字体样式
    布局文件View和ViewGroup
    创建线程的两种方法,继承Thread,继承Runnable
    本地文件的copy复制
    字节流和字符流完成URL下载,并存入本地
    文本过滤器的用法,FileFilter()和FilenameFilter()
    JavaSE笔记
  • 原文地址:https://www.cnblogs.com/chasedeath/p/12070229.html
Copyright © 2020-2023  润新知