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{2\pi i}{N}nk}} \ \ \ \ \ \ \ \ \ \ \ \ (k=0,1,...,N-1) \]

和逆变换公式

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

我们只需要用原根\(g^{\frac{P-1}{N}} (\mod P)\)替代单位复根\(e^{-\frac{-2\pi 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{-2\pi 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
posted @ 2019-07-31 16:03  guoshaoyang  阅读(775)  评论(1编辑  收藏  举报