题解 P3803 【【模板】多项式乘法(FFT)】
前言
众所周知,这FFT是用来算多项式乘法的。
对于这种常人思维难以理解的东西,就少些理解,多背板子吧!
因此只总结一下思路和代码,什么概念和推式子就靠巨佬们吧
推荐自为风月马前卒巨佬的概念和定理都非常到位的总结
推荐ppl巨佬的简明易懂的总结
多项式乘法的蹊径——点值表示法
一般我们把两个长度为
如何优化?直接看是没有思路的,只好另辟蹊径了。
多项式除了我们常用的系数表示法
所谓点值表示法,就是相当于用函数图像上
我们可以把系数表示转化成点值表示。点值表示下的多项式怎么相乘呢?就是选相同的
当然,两个长度为
举一个例子
↓
↓(点值)
↓
可是,随意选
所以,如果选的点很普通,这只是一条蹊径,并不是一条捷径。
神奇的单位根
介绍一个神奇的东西——
它是
普及一下欧拉公式,
于是可以看出,满足
这
介绍消去引理
DFT&IDFT
单位根有什么用呢?
看看我们把
假设
那么
注意,下面的变化用到了
首先带入单位根
那
也就是说,如果我们要求一个长度为
可以由算法写出DFT的复杂度
当然,别忘了还原成系数表示,这个过程叫做IDFT。
蒟蒻觉得这里理解太麻烦了,因此不再证明IDFT的过程,想了解的参见其它的总结。
只需要记住,IDFT的
void FFT(R complex<double>*a,R int n,R int op){//op=1为DFT,-1为IDFT
if(!n)return;//为了方便,n的意义与上面不一样,这里的n是a0、a1的长度
complex<double>a0[n],a1[n];
for(R int k=0;k<n;++k)
a0[k]=a[k<<1],a1[k]=a[k<<1|1];//奇偶项分离
FFT(a0,n>>1,op);FFT(a1,n>>1,op);//递归处理
R complex<double>wn(cos(PI/n),sin(PI/n)*op),w(1,0);//单位根
for(R int k=0;k<n;++k,w*=wn)//k从到n/2
a[k]=a0[k]+w*a1[k],a[k+n]=a0[k]-w*a1[k];
}
递归版过程
引入例题:洛谷P3803 【模板】多项式乘法(FFT)
由于系数很小,我们不必担心精度的问题了(当然float是使不得的)
递归版代码:
#include<cstdio>
#include<cmath>
#include<complex>
#include<iostream>
#define R register
#define G c=getchar()
using namespace std;
const int N=4.2e6;
const double PI=acos(-1);//自定义π
complex<double>f[N],g[N];
inline int in(){
R char G;
while(c<'-')G;
return c&15;
}
void FFT(R complex<double>*a,R int n,R int op){//同上
if(!n)return;
complex<double>a0[n],a1[n];
for(R int i=0;i<n;++i)
a0[i]=a[i<<1],a1[i]=a[i<<1|1];
FFT(a0,n>>1,op);FFT(a1,n>>1,op);
R complex<double>W(cos(PI/n),sin(PI/n)*op),w(1,0);
for(R int i=0;i<n;++i,w*=W)
a[i]=a0[i]+w*a1[i],a[i+n]=a0[i]-w*a1[i];
}
int main(){
R int n,m,i;
scanf("%d%d",&n,&m);
for(i=0;i<=n;++i)f[i]=in();
for(i=0;i<=m;++i)g[i]=in();
for(m+=n,n=1;n<=m;n<<=1);//把长度补到2的幂,不必担心高次项的系数,因为默认为0
FFT(f,n>>1,1);FFT(g,n>>1,1);//DFT
for(i=0;i<n;++i)f[i]*=g[i];//点值直接乘
FFT(f,n>>1,-1);//IDFT
for(i=0;i<=m;++i)printf("%.0f ",fabs(f[i].real())/n);//注意结果除以n,小心“-0”
puts("");
return 0;
}
好记又好写的递归版结果怎样呢?
Fast Fast TLE!只有77分。
最主要的原因在于,空间调用太多了。
蝴蝶操作和Rader排序
针对效率太低的问题,我们继续观察FFT实现过程中的更多特点。
观察这一句代码a[k]=a0[k]+w*a1[k],a[k+n]=a0[k]-w*a1[k];,在操作过程中,取出了a0[k]和a1[k]的值,通过计算又求出了a[k]和a[k+n]的值。我们把这样的一次运算叫做“蝴蝶操作”。
这样的操作有什么特点呢?我们试着将a0和a1合并成一个数组a,每一次蝴蝶操作后,取出了a[k]和a[k+n]的值,又求出了a[k]和a[k+n]的值。最后,整个数组都完成了求值,而并没有用到两个数组!
以
其实,我们完全可以递推求解!假设
剩下的问题就是把初始的数组变成最后一层的样子了。先别急着写一个递归函数暴力把位置换过去。来观察一下最后序列的编号的二进制表示000,100,010,110,001,101,011,111,是不是与原来000,001,010,011,100,101,110,111相比,每个位置上的二进制位都反过来了?这样的变化叫做Rader排序。
我们可以
#include<cmath>
#include<cstdio>
#define R register
#define I inline
using namespace std;
const int N=4.2e6;
const double PI=acos(-1);
int n,r[N];
struct C{//手写complex,比STL快一点点
double r,i;
I C(){r=i=0;}
I C(R double x,R double y){r=x;i=y;}
I C operator+(R C&x){return C(r+x.r,i+x.i);}
I C operator-(R C&x){return C(r-x.r,i-x.i);}
I C operator*(R C&x){return C(r*x.r-i*x.i,r*x.i+i*x.r);}
I void operator+=(R C&x){r+=x.r;i+=x.i;}
I void operator*=(R C&x){R double t=r;r=r*x.r-i*x.i;i=t*x.i+i*x.r;}
}f[N],g[N];
I int in(){
R char c=getchar();
while(c<'-')c=getchar();
return c&15;
}
I void FFT(R C*a,R int op){
R C W,w,t,*a0,*a1;
R int i,j,k;
for(i=0;i<n;++i)//根据映射关系交换元素
if(i<r[i])t=a[i],a[i]=a[r[i]],a[r[i]]=t;
for(i=1;i<n;i<<=1)//控制层数
for(W=C(cos(PI/i),sin(PI/i)*op),j=0;j<n;j+=i<<1)//控制一层中的子问题个数
for(w=C(1,0),a1=i+(a0=a+j),k=0;k<i;++k,++a0,++a1,w*=W)
t=*a1*w,*a1=*a0-t,*a0+=t;//蝴蝶操作
}
int main(){
R int m,i,l=0;
scanf("%d%d",&n,&m);
for(i=0;i<=n;++i)f[i].r=in();
for(i=0;i<=m;++i)g[i].r=in();
for(m+=n,n=1;n<=m;n<<=1,++l);
for(i=0;i<n;++i)r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));//递推求r
FFT(f,1);FFT(g,1);
for(i=0;i<n;++i)f[i]*=g[i];
FFT(f,-1);
for(i=0;i<=m;++i)printf("%.0f ",fabs(f[i].r)/n);
puts("");
return 0;
}