• NTT数论变换


    数论变换NTT

    前置知识

    1. FFT:NTT的思想和FFT一样([FFT介绍](%3Ca href="https://www.cnblogs.com/guoshaoyang/p/10957854.html"%3Ehttps://www.cnblogs.com/guoshaoyang/p/10957854.html%3C/a%3E))

    概述

    数论变换,即NTT(Number Theory Transformation?),是基于数论域的FFT,一般我们默认FFT为负数域上的快速傅里叶变换,和NTT区分。

    我们知道,FFT是利用单位复根的周期性,以(Theta(N log N))的复杂度计算(N)组多项式的值。NTT其实就是在数论域上的FFT,它利用素数取模的周期性,达到了和FFT一样的效果。

    原根

    (a)(P)的阶等于(φ(P)),则称(a)为模(P)的一个原根(这个定义不重要)。

    对于质数(P),若(g)(P)的原根,那么(g^i mod P)的结果两两不同;或者说(g^t = 1 (mod P))当且仅当指数为(t=P-1)的时候成立。

    我们发现,原根的性质和单位负数根的性质一一对应,都满足周期性和周期内互异性,故可以用来代入和插值。

    NTT

    我们有离散傅里叶变换公式

    [X_k=sum^{N-1}_{n=0}{x_ne^{-frac{2pi i}{N}nk}} (k=0,1,...,N-1) ]

    和逆变换公式

    [X_k=frac{1}{N}sum^{N-1}_{n=0}{x_ne^{frac{2pi i}{N}nk}} (k=0,1,...,N-1) ]

    我们只需要用原根(g^{frac{P-1}{N}} (mod P))替代单位复根(e^{-frac{-2pi i}{N}}),就可以得到

    [X_k=sum^{N-1}_{n=0}{x_ng^{frac{P-1}{N}nk}}(mod P) (k=0,1,...,N-1) ]

    [X_k=frac{1}{N}sum^{N-1}_{n=0}{x_ng^{-frac{P-1}{N}nk}}(mod P) (k=0,1,...,N-1) ]

    实现

    和FFT的模板几乎一样,用原根(g^{frac{P-1}{N}} (mod P))替代单位复根(e^{-frac{-2pi i}{N}})即可

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long LL;
    const int INF=1e9+7,MAXN=3e6+10/*Min:2^20+10*/;
    const LL BASE=3,MOD=998244353,INV=332748118;
    inline LL fpm(LL base,LL p){
    	LL ret=1;
    	while(p){
    		if(p&1)
    			ret=ret*base%MOD;
    		base=base*base%MOD;
    		p>>=1;
    	}
    	return ret%MOD;
    }
    int N,M,limit=1,lg,rev[MAXN];
    inline void NTT(LL *a,int type){
    	for(int i=0;i<limit;i++)
    		if(i<rev[i])
    			swap(a[i],a[rev[i]]);
    	for(int mid=1;mid<limit;mid<<=1){
    		int len=mid<<1/*n*/;
    		LL Wn=fpm(type==1?BASE:INV/*BASE^type*/,(MOD-1)/len);
    		for(int j=0;j<limit;j+=len){
    			LL w=1;
    			for(int k=0;k<mid;k++){
    				int x=a[j+k],y=w*a[j+k+mid]%MOD;
    				a[j+k]=(x+y)%MOD;
    				a[j+k+mid]=(x-y+MOD)%MOD;
    				w=w*Wn%MOD;
    			}
    		}
    	}
    }
    LL a[MAXN],b[MAXN];
    int main(){
    	scanf("%d%d",&N,&M);
    	for(int i=0;i<=N;i++){
    		scanf("%lld",a+i);
    		a[i]=(a[i]+MOD)%MOD;
    	}
    	for(int i=0;i<=M;i++){
    		scanf("%lld",b+i);
    		b[i]=(b[i]+MOD)%MOD;
    	}
    	while(limit<=N+M){
    		limit<<=1;
    		lg++;
    	}
    	for(int i=0;i<limit;i++)
    		rev[i]=(rev[i>>1]>>1)|((i&1)<<(lg-1));
    	NTT(a,1);
    	NTT(b,1);
    	for(int i=0;i<limit;i++)
    		a[i]=a[i]*b[i]%MOD;
    	NTT(a,-1);
    	LL limit_inv=fpm(limit,MOD-2);
    	for(int i=0;i<=N+M;i++)
    		printf("%lld ",a[i]*limit_inv%MOD);
    	return 0;
    }
    

    复杂度

    时间

    和FFT一样,是(Theta(N log N)),在数据较小的情况下比FFT快,但因为其中有快速幂运算,如果数字太大则会导致TLE

    空间

    和FFT一样,是(Theta(N)),但注意数组大小不是(N+M),而是不小于(N+M)的最小的(2)的幂

    范围(素数选取)

    我们需要一个大素数来取模,这个素数要大于最终输出的答案(但如果太大,就需要使用快速乘)
    下面给出常用的几个素数,原根及原根的逆元

    素数 原根 逆元
    65537 3 21846
    786433 10 235930
    5767169 3 1922390
    7340033 3 2446678
    23068673 3 7689558
    104857601 3 34952534
    167772161 3 55924054
    469762049 3 156587350
    998244353 3 332748118
    1004535809 3 334845270
    2013265921 31 64944062
    2281701377 3 760567126
    3221225473 5 1932735284
    75161927681 3 25053975894
    77309411329 7 22088403237
    206158430209 22 84337539631
    2061584302081 7 589024086309
    2748779069441 3 916259689814
    6597069766657 5 2638827906663
    39582418599937 5 15832967439975
    79164837199873 5 47498902319924
    263882790666241 7 150790166094995
    1231453023109120 3 410484341036374
    1337006139375610 3 445668713125206
    3799912185593850 5 1519964874237540
    4222124650659840 19 888868347507335
    7881299347898360 6 1313549891316390
    31525197391593400 3 10508399130531100
    180143985094819000 6 30023997515803300
    1945555039024050000 5 1167333023414430000
    4179340454199820000 3 1393113484733270000
  • 相关阅读:
    JavaScript节点属性
    main函数的参数
    CGI
    open()函数 linux中open函数使用
    海伦公式
    C语言字符串操作函数
    makefile(一)
    makefile
    第一天
    时间小憩
  • 原文地址:https://www.cnblogs.com/guoshaoyang/p/11276965.html
Copyright © 2020-2023  润新知