前置芝士:快速傅立叶变换(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(0le 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)
性质四:(sumlimits_{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{n}{k})的小数部分(*(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 e phi(p))使得(g^kequiv 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;
}