快速数论变换(NTT)
前置芝士:快速傅立叶变换(FFT)
垃圾\(FFT\)
我们已经知道\(FFT\)这个东西他很\(nb\)了,但是仍然存在很多局限性
比如说
\(1.\)要用到复数,所以它很慢
\(2.\)要用到复数,所以是\(double\)类型,有精度误差
\(3.\)要用到复数,不能取模
\(……\)
我们发现就是这个垃圾复数拖了算法后退,要想办法摆脱它(´༎ຶД༎ຶ`)!
原根
聪明的珂学家们发现了一种奇妙的东西:原根
它可以代替复数进行变换,而且具有优良的性质
阶
若\(gcd(a,p)==1\),且\(p>1\)
则对于\(a^x≡1(mod\ p)\)最小的\(x\),我们称为\(a\)的\(p\)的阶,记作\(\delta _p(a)\)
相关定理
定理一:若\(p>1\)且\(gcd(a,p)==1\)且\(a^n≡1(mod\ p)\),则\(\delta _p(a)|n\)
证明:根据定义理解一下吧……
定理二:\(\delta _p(a)|\phi(m)\)
证明:由欧拉定理:\(a^{\phi(p)}≡1(mod\ p)\),所以\(\delta _p(a)≤\phi(p)\),根据定理一得到\(\delta _p(a)|\phi(m)\)
原根
若\(\delta _p(a)=\phi(p)\),则称\(a\)为模\(p\)的一个原根
如\(\delta _9(2)=6=\phi(9)\),称\(2\)为模\(9\)的一个原根
相关定理
定理一: 一个正整数\(p\)存在原根,当且仅当\(p=2,4,p^s,2p^s\),其中\(p\)为奇素数,\(s\)为正整数
定理二: 一个数\(p\)如果存在原根,则原根个数为\(\phi(\phi(p))\)
定理三: 若\(p\)是一个素数,\(g\)是\(p\)的一个原根,那么\(g^i\ mod\ p (1<g<p,0<i<p)\)的结果互不相同
证明咱也不会嘛……\(qwq\)
一些性质
回想一下为什么我们要用单位根作为系数带入多项式?是因为它具有很多优良性质,我们康康这些性质原根有没有!
我们假设一个前提\(p=a*2^k+1\),其中\(k\)很大,沿用\(fft\)的方法,只考虑\(n\)为\(2\)的整次幂的情况
设\(\omega _n^1=g^{\frac{p-1}{n}}\)
首先可以确保\(\omega _n^1\)不再是恶心的复数,因为\(p=a*2^k+1\),所以\(\frac{p-1}{n}\)一定是整数
性质一: \(\omega_ n^k(0\le k \le n-1)\)互不相同
证明:根据原根的定理三可知
性质二:\(\omega _{2n}^{2k} = \omega _n^k\)
证明:\(\omega _{2n}^{2k}=(g^{\frac{2k(p-1)}{2n}})=g^{\frac{k(p-1)}{n}}=\omega _n^k\)
性质三:\(\omega _{n}^{k+\frac{n}{2}} = -\omega _n^k\)
证明:\(\omega _{n}^{k+\frac{n}{2}} = \omega _n^k *\omega _n^{\frac{n}{2}}\)
因为\((\omega _n^{\frac{n}{2}})^2=\omega _n^n=1\)
所以\(\omega _n^{\frac{n}{2}}=1\)或\(-1\)
又因为\(\omega _n^{\frac{n}{2}}与\omega _n^0\)不同
所以\(\omega _n^{\frac{n}{2}}=-1\)
所以\(\omega _{n}^{k+\frac{n}{2}} = -\omega _n^k\)
性质四:\(\sum\limits_{i=0}^{n-1}\omega_ n^i=0\)
证明:和单位根一模一样,懒得打了……
性质五: \(\omega _n^k=\omega _n^{n\%k}\)
证明:\(\omega _n^k=g^{\frac{k(p-1)}{n}}\)
由费马小定理可知,当\(p\)为质数时,\(a^{p-1}\equiv1(mod\ p)\)
所以\(\omega _n^k=g^{\frac{k(p-1)}{n}\% (p-1)}\)
考虑留下的应该是\(\frac{k}{n}\)的小数部分\(*(p-1)\),得到\(\omega _n^k=g^{\frac{k\%n(p-1)}{n}}=\omega _n^{k\%n}\)
我们发现傅立叶变换中需要用到的单位根的性质原根统统满足!那我们还要垃圾单位根干啥!直接上原根啊!
不过也有一定的限制,就在于模数\(p\)必须满足\(p=a*2^k+1\),且不能包含小数
怎么找原根?
暴力枚举(雾
由于一个质数原根有\(\phi(p-1)\)个,最小的原根一般很小
枚举\(g\)和\(k\)复杂度约为\(O(plogp)\)
但是跟具阶的定理二,我们发现如果存在其他\(k\ne \phi(p)\)使得\(g^k\equiv 1(mod\ p)\),那么\(k\)一定是\(\phi(p)\)的约数,所以其实只要枚举\(\phi(p)\)的约数即可
复杂度期望\(O(log^2p)\)
\(code\)
然后我们快来用原根草多项式吧!
#include<bits/stdc++.h>
using namespace std;
namespace red{
#define int long long
#define eps (1e-8)
inline int read()
{
int x=0;char ch,f=1;
for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
if(ch=='-') f=0,ch=getchar();
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return f?x:-x;
}
const int N=5e6+10,p=998244353,g=3,gi=332748118;
int n,m;
int a[N],b[N];
int limit,len;
int pos[N];
inline int fast(int x,int k)
{
int ret=1;
while(k)
{
if(k&1) ret=ret*x%p;
x=x*x%p;
k>>=1;
}
return ret;
}
inline void ntt(int *a,int inv)
{
for(int i=0;i<limit;++i)
if(i<pos[i]) swap(a[i],a[pos[i]]);
for(int mid=1;mid<limit;mid<<=1)
{
int Wn=fast(inv?g:gi,(p-1)/(mid<<1));
for(int r=mid<<1,j=0;j<limit;j+=r)
{
int w=1;
for(int k=0;k<mid;++k,w=w*Wn%p)
{
int x=a[j+k],y=w*a[j+k+mid]%p;
a[j+k]=(x+y)%p;
a[j+k+mid]=(x-y)%p;
if(a[j+k+mid]<0) a[j+k+mid]+=p;
}
}
}
}
inline void main()
{
n=read(),m=read();
for(int i=0;i<=n;++i) a[i]=read();
for(int i=0;i<=m;++i) b[i]=read();
for(limit=1;limit<=n+m;limit<<=1) ++len;
for(int i=0;i<limit;++i) pos[i]=(pos[i>>1]>>1)|((i&1)<<(len-1));
ntt(a,1);ntt(b,1);
for(int i=0;i<limit;++i) a[i]=a[i]*b[i]%p;
ntt(a,0);
int inv=fast(limit,p-2);
for(int i=0;i<=n+m;++i) printf("%lld ",a[i]*inv%p);
}
}
signed main()
{
red::main();
return 0;
}