先贴个tp(其实是自己不会
呜噜噜多项式普及
呜噜噜多项式提高
关于多项式
它一般以这样的形态出现
A(x)=a0+a1x+a2x2+⋯+anxnA(x)=a0+a1x+a2x2+⋯+anxn
其中 xx 被称作自变量(?),对应的每个 a0,a1,a2⋯a0,a1,a2⋯称为这个多项式的系数,对于这个多项式的最高次数 nn,我们称之为这个多项式的 度degree。
对于每个题目而言,我们知道的同时需要操作的一般也就是它的系数和它的度
多项式加减乘除!
为什么没有除呢?因为我还不会(现在我会啦!。两个多项式的加减即对应项系数的加减,我们可以 O(n)O(n) 简单秒掉,但是多项式相乘呢?
这里我们引入一个新的概念(至少我是从这里才知道的)—— 卷积
形如ci=∑ij=0aj×bi−jci=∑ij=0aj×bi−j 中的 cici 我们称之为 a,ba,b 两个数组的卷积
那么两个多项式相乘,实际上就是求两个多项式的卷积即cixi=∑ij=0ajxj×bi−jxi−jcixi=∑ij=0ajxj×bi−jxi−j,同时原本度都为为 nn 的两个多项式相乘后得到的多项式的度也就变成了 2n2n .
快速傅里叶变换(FFT)
通过定义式,我们知道两个多项式相乘的复杂度是 O(n2)O(n2) 级别的,这对于大多数的题目而言都太慢了,所以我们需要更优秀的算法,多项式的快速变换也就应运而生
对于一个多项式,我们不仅有系数表达(就是以上所用的表达),还有点值表达
就好像 n+1n+1 个点就可以确定一个次数为 nn 的函数图像一样,一个度为 nn 多项式也可以转化为 n+1n+1 个点值
如何转换
首先我们又得引入几个新的概念:
复数
我们一般接触的数都属于实数范围,但还有一些不合法,但是依然存在的数,形如√−1√−1(在下文中,我们称之为 ii ),我们称之为虚数.
而复数则是既含有实数,有含有虚数的数。一般来说,我们称复数的实数部分为实部,虚数部分的系数为虚部
复数之间的运算也同我们熟知的实数一样
单位复数根
我们设度为nn 的多项式的单位复数根为ωn=e2πi/n=cos(2π/n)+isin(2π/n)ωn=e2πi/n=cos(2π/n)+isin(2π/n)
它有着非常好的性质
ωnn=1ωnn=1
ω2d2n=ωdnω2d2n=ωdn
ωd+n2n=−ωd2nωd+n2n=−ωd2n
∑nk=1wkn=0∑nk=1wkn=0
证明: ∑nk=1ωkn=ωn(ωnn−1)ωn−1=0∑nk=1ωkn=ωn(ωnn−1)ωn−1=0
离散傅里叶变换(DFT)
即将一个多项式转换为用表达单位复数根的形式,即
yk=A(ωkn)=n∑j=0ajωkjnyk=A(ωkn)=n∑j=0ajωkjn
两个多项式转换为点值表达之后,我们就可以O(n)O(n)对两个式子相乘
离散傅里叶逆变换(DFT)
这里本来有一个通过矩阵解释的绝妙的证明过程,但矩阵在Markdown中实在太难打了,所以很遗憾
ak=n∑j=0yjω−kjnak=n∑j=0yjω−kjn
这样我们就完成了多项式O(nlogn)O(nlogn)相乘
具体实现代码
inline void Dft(comp *a,int len,int rev)
{
if(len==1)return;
for(register int i=0;i < len;++i)temp[i]=a[i];
for(register int i=0;i<len;++i)
if(i&1)
a[len/2+i/2]=temp[i];
else
a[i/2]=temp[i];
comp *g=a,*h=a+len/2;
Dft(g,len/2,rev),Dft(h,len/2,rev);
comp w(1,0),step(cos(2.0*pi/len),rev*sin(2.0*pi/len));
for(register int k=0;k<len/2;++k)
{
comp t=w*h[k];
temp[k]=g[k]+t;
temp[k+len/2]=g[k]-t;
w*=step;
}
for(register int i=0;i<len;++i)a[i]=temp[i];
}
其中FFT有一个叫做蝴蝶变换的优化,大致即为将递归到最后一层时的得到的新数组序列预先处理好,就是将原数组排序为递归时得到的数组,再一层一层往上更新即可
inline void change(comp *a,int len)
{
for(register int i=0;i<len;++i)
{
re[i]=re[i>>1]>>1;
if(i&1)
re[i]|=len>>1;
}
for(register int i=0;i<len;++FFTi)
if(i<re[i])swap(a[i],a[re[i]]);
}
inline void Dft(comp *a,int len,int rev)
{
change(a,len);
for(register int h=2;h<=len;h<<=1)
{
comp step(cos(2.0*pi/h),rev*sin(2.0*pi/h));
for(register int j=0;j<len;j+=h)
{
comp w(1,0);
for(register int k=j;k<j+h/2;++k,w*=step)
{
comp u=a[k];
comp t=a[k+h/2]*w;
a[k]=u+t;
a[k+h/2]=u-t;
}
}
}
}
快速傅里叶逆变换
前置芝士:逆元,NTT
逆元
即满足 ax≡1modpax≡1modp 其中 a,pa,p 互质,xx 即被称作 aa 关于 pp 的逆元
求解逆元: 一般用 exgedexged 或者 费马小定理
费马小定理求解逆元:
因为 ap−1≡1modpap−1≡1modp
所以 a⋅ap−2≡1modpa⋅ap−2≡1modp
得出 aa 关于 pp 的逆元即为 ap−2ap−2
快速数论变换(NTT)
关于NTT,我们很明显的看出它与FFT之间的关系(通过中文,它可以只在整数域内变换,完成nlognnlogn多项式乘,具体实现我们使用原根 gg 代替了单位复数根 ωω ,一般来说,NTT常用模数为998244353
或者1004535809
,且它们的原根都为3
这里的代替即我们让 g(p−1)/ng(p−1)/n 等价于 e2πi/ne2πi/n并在运算的时候对模数取模运算即可
由于是整数运算,所以正常情况下要比复数运算的FFT快不少
非正常情况:有关字符串匹配的问题上,NTT要比FFT慢许多