NTT数论变换
数论变换NTT
前置知识
- 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 |