题解:P3803 【模板】多项式乘法(FFT)
derderhaoyue · · 题解
快速傅里叶变换
序言 & FFT 简介
FFT,即快速傅里叶变换,是离散傅里叶变换的快速算法,可以用来显著的加快多项式乘法(将原来朴素算法的
FFT 计算乘法的主要流程是先将系数表示法转化为点值表示法,然后再通过点值表示法计算完之后转化回系数表示法。
FFT 作为数论的一个重要板块,有着很多的前置知识,也有一定的思考难度,接下来就让我为大家分部分讲解 FFT 的前置知识与他本身。
前置知识
多项式的表示法
-
系数表示法 这种方法算得上是最常见的多项式表示法,一般的时候我们也用得最多,形如
f(x)=\sum^{n}_{k=0}{a_kx^k} 。 以这种方式计算多项式乘法时间复杂度为\Theta(n^2) 。 -
点值表示法 这种方法类似于初中的给几个点求函数解析式的情景,一个
n 次多项式可以使用n+1 个点来表示出来,即这个多项式被点(x_1,y_1),(x_2,y_2),...,(x_{n+1},y_{n+1}) 唯一表示。 但是这样计算复杂度仍然为\Theta(n^2) 。
单位根
在讲单位根之前,我们先讲一下单位圆,单位圆就是以坐标轴为原点,半径为 1 的圆。
而单位根就是把一个圆
根据定义,找单位根的方法就呼之欲出啦:
先将一个圆
注:为了便于实现,下文中的
而根据复数的乘法法则可得:
根据每个复数的幅角,可以计算出这个点的坐标,例如
接下来是单位根的性质:
-
\omega^{2k}_{2n}=\omega^{k}_{n}
证明:
-
\omega^{k+n}_{n}=\omega^{k}_{n}
证明:
-
\omega^{k+\frac{n}{2}}_{n}=-\omega^{k}_{n}
证明:
FFT 推导(正确性证明)
假设一个
将它的下标按奇偶性分类,并设:
则有
分别将
和
这两个式子长得极为相像,仅有一个常数的差距,意味着我们只需要求一半便可得知另一半的结果,如果我们将
这不就是将分组前的数组每个数给翻转了吗,这样,我们就可以将翻转后的数组计算出来啦。
但,为什么可以这样呢?
证明:
设
-
若
k 为奇数,则将其右移一位,并将它的二进制表示下的第n-1 位修改成1 。 -
若
k 为偶数,则将其右移一位,并将它的二进制表示下的第n-1 位修改成0 。
于是可得:
即将
for(int mid=1;mid<lim;mid<<=1){
complex wn(cos(pie/mid),oper*sin(pie/mid));
for(int R=mid<<1,j=0;j<lim;j+=R){
complex w(1,0);
for(int k=0;k<mid;k++,w=w*wn){
complex y=x[j+k],z=w*x[j+mid+k];
x[j+k]=y+z;
x[j+mid+k]=y-z;
}
}
}
但这还没完,还有要将点值表示法转回点值表示法的步骤没有完成,这就需要 IFFT(快速傅里叶逆变换)登场了。 使用矩阵来理解 IFFT 是一种比较简单的理解方式,从矩阵的角度看我们刚才的计算,实际上就是在进行如下操作:
现在,我们已经完成了求左边式子的值这个任务,而中间的值在多项式的点值表示法中也是一一对应的,接下来只需要在两边左乘中间矩阵的逆矩阵即可。(因为矩阵乘法不满足交换律,所以把一个矩阵乘在另一个矩阵的左边和右边结果不同,乘在左边就是左乘)
由于这个矩阵的元素非常特殊,它的逆矩阵也有特殊的性质,就是每一项取倒数,再除以变换的长度
n ,就能得到它的逆矩阵。
——引用自 oi wiki。
在代码实现上就是将原来 FFT 的单位根替换为它的倒数,然后计算出结果再除以
实现代码
#include<bits/stdc++.h>
using namespace std;
constexpr int M=1e7+5e3;
const double pie=acos(-1.0);
struct comp{// 手打复数,避免被卡(其实一般也没有人来卡这个吧)。
double real,img;
comp(double xx=0,double yy=0){
real=xx;
img=yy;
}
comp operator + (const comp b)const{
return comp(real+b.real , img+b.img);
}
comp operator - (const comp b)const{
return comp(real-b.real , img-b.img);
}
comp operator * (const comp b)const{
return comp(real*b.real-img*b.img , real*b.img+img*b.real);
}
}f[M],g[M];
int n,m,l,lim=1;
int r[M];
void fft(comp *F,int oper){// oper 是操作,传入 1 是FFT,-1 是 IFFT。
for(int i=0;i<lim;i++){
if(i>r[i]-1){
continue;
}
swap(F[i],F[r[i]]);
}
for(int mid=1;mid<lim;mid<<=1){
comp wn(cos(pie/mid),oper*sin(pie/mid));
for(int R=mid<<1,j=0;j<lim;j+=R){
comp w(1.0,0.0);
for(int k=0;k<mid;k++,w=w*wn){
comp y=F[j+k],z=w*F[j+mid+k];
F[j+k]=y+z;
F[j+mid+k]=y-z;
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++){
scanf("%lf",&f[i].real);
}
for(int i=0;i<=m;i++){
scanf("%lf",&g[i].real);
}
while(lim<n+m+1){
lim<<=1;
l++;
}
for(int i=0;i<lim;i++){
r[i]=((r[i>>1]>>1)|((i&1)<<(l-1)));
}
fft(f,1);
fft(g,1);
for(int i=0;i<lim;i++){
f[i]=f[i]*g[i];
}
fft(f,-1);
for(int i=0;i<n+m+1;i++){
printf("%d ",(int)(f[i].real/lim+0.5));// 防止精度爆炸。
}
return 0;
}
参考资料
oi wiki (部分内容参考与代码参考)。
attcak大佬的文章 (代码参考)。
_不会dp不改名_大佬的文章 (位逆序置换证明参考)。