【算法笔记】多项式快速算法——快速数论变换

  • 万年大坑终于填上了
  • 本文为博主原创,未经许可不得转载

原根的应用

单位复数根的局限

使用单位复数根进行快速傅里叶变换,总会产生精度误差

而当题目要求取模时,单位复数根就GG了

这时我们只能放弃单位复数根,去寻找新的东西

万幸的是,我们找到了原根,完美地解决了这个问题,或许这就是数学的美吧

  

单位复数根的替代

由费马小定理我们知道素数$G$满足$G^{p-1}\equiv 1 (mod p)$

所以我们取$w_n=G^{\frac{p-1}{n}}$,就能满足$w_n^n=G^{p-1} \equiv 1 (mod p)$

考虑在FFT中我们利用的单位复数根$\omega_n^k=\cos\frac{2\pi}{n}k+i\sin\frac{2\pi}{n}k$的那些性质

性质一:$\omega_n^k$互不相同,用于保证点值合法

而原根$G$满足$1,G,G^2,…,G^{p-1}$互不相同,满足该性质

性质二:消去引理$\omega_{2n}^{2k}=\omega_{n}{k}$,用于分治

而原根$G$满足$\omega_{2n}^{2k}=G^{\frac{(p-1)2k}{2n}}=G^{\frac{(p-1)k}{n}}=\omega_n^k$,满足该性质

性质三:$\omega_n^{k+\frac{n}{2}}=-\omega_n^k$,用于分治

而原根$G$满足$\omega_n^{\frac{p-1}{n}(k+\frac{n}{2})}=G^{\frac{(p-1)k}{n}}\times G^{\frac{p-1}{2}}$

而$G^{p-1} \equiv 1$,所以$G^{\frac{p-1}{2}}\equiv -1$(由于G是原根,+1值舍去),所以该性质成立

性质四:求和引理$\sum_{i=0}^{n-1}(\omega_{n}^{k})^i=0$

而原根$G$满足$\sum_{i=0}^{n-1}G^{\frac{(p-1)ki}{n}}=\frac{1-G^{(p-1)k}}{1-G^{\frac{(p-1)k}{n}}}=0$,满足该性质(等比数列求和)

综上所述,原根可以完美地代替单位复数根,作为点值进行FFT运算

我们把使用原根的这种运算称为快速数论变换,也就是NTT

  

  

NTT素数

我们注意到$w_n=G^{\frac{p-1}{n}}$中,$n$代表多项式的次数界,是$2$的整次幂

而$n|(p-1)$,这就要求$n=r*2^k+1$

我们最常见的就是$UOJ$模数$998244353=119\times 2^{23}+1$(原根G=3)

再额外记住两个$NTT$模数用于三模数$NTT$(后面会讲到):

$1004535809=479\times 2^{21}+1$(原根G=3)

$2281701377=17\times 2^{27}+1$(原根G=3)

  

  

代码实现

实现技巧

我用的是mzx大爷的板子

具体来讲就是预处理所有要用到的原根

其中数组$wn[0][n]$表示$w_{2^n}$,$wn[1][n]$表示$wn[0][n]$的逆元,用于逆变换

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<bits/stdc++.h>
#define FILE "read"
#define MAXN 300010
#define cadd(a,b) a=add(a,b)
#define csub(a,b) a=sub(a,b)
#define cmul(a,b) a=mul(a,b)
using namespace std;
const int mod=998244353;
int n,m,G(3),A[MAXN],B[MAXN],R[MAXN],w[MAXN],wn[2][20];
inline int read(){
int x=0,f=1; char ch=getchar();
while(ch>'9'||ch<'0') {if(ch=='-') f=-1; ch=getchar();}
while(ch>='0'&&ch<='9') {x=x*10+ch-'0'; ch=getchar();}
return x*f;
}
inline int add(int a,int b){return (a+=b)>=mod?a-mod:a;}
inline int sub(int a,int b){return (a-=b)<0?a+mod:a;}
inline int mul(int a,int b){return 1LL*a*b%mod;}
inline int power(int a,int b){
int ret(1);
while(b){
if(b&1) ret=mul(ret,a);
a=mul(a,a); b>>=1;
}return ret;
}
void Init(){
wn[0][18]=power(G,(mod-1)/(1<<18));
wn[1][18]=power(wn[0][18],mod-2);
for(int i=17;i>=0;--i){
wn[0][i]=mul(wn[0][i+1],wn[0][i+1]);
wn[1][i]=mul(wn[1][i+1],wn[1][i+1]);
}w[0]=1;
}
void NTT(int *a,int H,int f){
int L=(1<<H);
for(int i=0;i<L;++i) if(i<R[i]) swap(a[i],a[R[i]]);
for(int e=1;e<=H;++e){
int len=(1<<e),l=(len>>1);
for(int i=1;i<l;++i) w[i]=mul(w[i-1],wn[f][e]);
for(int st=0;st<L;st+=len) for(int k=0;k<l;++k){
int x=a[st+k],y=mul(w[k],a[st+k+l]);
a[st+k]=add(x,y); a[st+k+l]=sub(x,y);
}
}
if(f==1) for(int i=0;i<L;++i) cmul(a[i],power(L,mod-2));
}
void solve(){
Init(); int L=1,H=0; while(L<n+m-1) L<<=1,++H;
for(int i=0;i<L;++i) R[i]=(R[i>>1]>>1)|((i&1)<<(H-1));
NTT(A,H,0); NTT(B,H,0);
for(int i=0;i<L;++i) A[i]=mul(A[i],B[i]);
NTT(A,H,1);
for(int i=0;i<n+m-1;++i) printf("%d ",A[i]);
}
int main(){
freopen(FILE".in","r",stdin);
freopen(FILE".out","w",stdout);
n=read(); m=read();
for(int i=0;i<n;++i) A[i]=read();
for(int i=0;i<m;++i) B[i]=read();
solve();
return 0;
}

  

  

模数不符合要求的情况

三模数NTT

以此题为例,由于所给模数是23333333,显然不是一个NTT模数

假设我们选取了三个$NTT$模数$m_1,m_2,m_3$,并且计算出了在这三个模数意义下的答案$a_1,a_2,a_3$

那么我们就得到了三个同余方程:

$Ans \equiv a_1 (mod m_1)$

$Ans \equiv a_2 (mod m_2)$

$Ans \equiv a_3 (mod m_3)$

考虑使用中国剩余定理合并

由于卷积之后每个数达到了$10^{23}$的数量级,所以需要满足$m_1 m_2 m_3> 10^{23}$

由于$10^{23}>2^{64}$,所以直接合并会炸longlong,需要一些$tricky$

我们先用中国剩余定理合并前两个式子得到:

$Ans \equiv A(mod M)$

$Ans \equiv a_3(mod m_3)$

不妨设$Ans=KM+A=km_3+a_3$,我们可以在模$m_3$意义下求解:

$KM \equiv a_3-A(mod m_3)$

$K \equiv (a_3-A)\times M^{-1} (mod m_3)$

将$K$回带$Ans=KM+A$就可以求出$Ans$,然后对23333333取模即可

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include<bits/stdc++.h>
#define FILE "annona_squamosa"
#define MAXN 300100
using namespace std;
typedef long long ll;
const int M[3]={998244353,1004535809,469762049};
const int G[3]={3,3,3};
const int mod=23333333;
const ll MOD=(ll)M[0]*M[1];
int n,a[MAXN],b[MAXN],R[MAXN],_a[3][MAXN],_b[3][MAXN];
inline int read(){
int x=0,f=1; char ch=getchar();
while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();}
while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();}
return x*f;
}
struct node{
int P,G,w[MAXN],wn[2][MAXN];
inline void swap(int &a,int &b){a=a^b,b=a^b,a=a^b;}
inline int add(int a,int b){return (a+=b)>=P?a-P:a;}
inline int sub(int a,int b){return (a-=b)<0?a+P:a;}
inline int mul(int a,int b){return 1LL*a*b%P;}
inline int fast(int a,int b){int ret(1);for(;b;b>>=1,a=mul(a,a))if(b&1)ret=mul(ret,a);return ret;}
void pre(int p,int g){
P=p; G=g; w[0]=1;
wn[0][18]=fast(G,(P-1)/(1<<18));
wn[1][18]=fast(wn[0][18],P-2);
for(int i=17;i>=0;--i)for(int k=0;k<=1;++k)wn[k][i]=mul(wn[k][i+1],wn[k][i+1]);
}
void NTT(int *a,int H,int f){
int L=(1<<H);
for(int i=0;i<L;++i) if(i<R[i]) swap(a[i],a[R[i]]);
for(int e=1;e<=H;++e){
int len=(1<<e),l=(len>>1);
for(int i=1;i<l;++i) w[i]=mul(w[i-1],wn[f][e]);
for(int st=0;st<L;st+=len)for(int k=0;k<l;++k){
int x=a[st+k],y=mul(w[k],a[st+k+l]);
a[st+k]=add(x,y); a[st+k+l]=sub(x,y);
}
}
if(f) for(int i=0,temp=fast(L,P-2);i<L;++i) a[i]=mul(a[i],temp);
}
}ntt[3];
inline ll mul(ll a,ll b,ll p){ll ret(0);for(;b;b>>=1,a=(a+a)%p)if(b&1)ret=(ret+a)%p;return ret;}
inline ll fast(ll a,ll b,ll p){ll ret(1);for(;b;b>>=1,a=mul(a,a,p))if(b&1)ret=mul(ret,a,p);return ret;}
int inv1=fast(M[0],M[1]-2,M[1]),inv2=fast(M[1],M[0]-2,M[0]),inv3=fast(MOD,M[2]-2,M[2]);
inline ll CRT(ll a1,ll a2,ll a3){
ll A=(mul(a1*M[1]%MOD,inv2,MOD)+mul(a2*M[0]%MOD,inv1,MOD))%MOD;
ll k=(a3+M[2]-A%M[2])*inv3%M[2];
return (k*(MOD%mod)%mod+A)%mod;
}
int main(){
freopen(FILE".in","r",stdin);
freopen(FILE".out","w",stdout);
n=read();
for(int i=0;i<n;++i) a[i]=read();
for(int i=0;i<n;++i) b[i]=read();
int H=0,L=1; while(L<=n+n) L<<=1,++H;
for(int i=0;i<L;++i) R[i]=(R[i>>1]>>1)|((i&1)<<(H-1));
for(int i=0;i<3;++i) ntt[i].pre(M[i],G[i]);
for(int i=0;i<3;++i){
for(int j=0;j<L;++j) _a[i][j]=a[j];
for(int j=0;j<L;++j) _b[i][j]=b[j];
ntt[i].NTT(_a[i],H,0); ntt[i].NTT(_b[i],H,0);
for(int j=0;j<L;++j) _a[i][j]=ntt[i].mul(_a[i][j],_b[i][j]);
ntt[i].NTT(_a[i],H,1);
}
for(int i=0;i<L;++i) a[i]=CRT(_a[0][i],_a[1][i],_a[2][i]);
for(int i=0;i<n;++i) printf("%d ",a[i]);
return 0;
}

  

  

拆系数FFT

留坑……

  

  

参考文献


文章目录
  1. 1. 原根的应用
    1. 1.1. 单位复数根的局限
    2. 1.2. 单位复数根的替代
    3. 1.3. NTT素数
  2. 2. 代码实现
    1. 2.1. 实现技巧
    2. 2.2. 参考代码
  3. 3. 模数不符合要求的情况
    1. 3.1. 三模数NTT
    2. 3.2. 拆系数FFT
  4. 4. 参考文献
,