FFT其实在很早的时候就已经接触到了,但是那个时候学起来有点仙,感觉这东西离实际解题的距离有点远,不如那些其他的数据结构那么直接。但是半年多下来的做题,发现FFT其实应用的十分广泛,并且很多数学题推出公式之后就可以套用FFT进行计算。所以对于FFT的理解也不能仅仅只是停留于背板子的阶段了,而应该更加深入的去理解它。
复数的运算及性质,多项式
首先讲什么是FFT,FFT的全称为快速傅里叶变换(Fast Fourier Transformation),是基于离散傅里叶变换的快速求法。最基础的运用就是解决多项式相乘的问题,可以将朴素算法的 O(n2) O ( n 2 ) 优化成 O(nlogn) O ( n l o g n ) ,是一种比较高效的方法。
我们假设我们现在有两个多项式:
首先,我们需要知道,多项式的一个表示方法为系数表示法,即一个n次的多项式可以表示为 ∑ni=0aixi ∑ i = 0 n a i x i 。这里的每一个 ai a i 表示每一项的系数。然后我们发现,对于一个多项式,我们只会关心这个多项式的系数,而并不需要真正的记录下它的指数,因为在系数表示法中, ai a i 的指数就是 i i 。所以一个多项式可以表示成这个样子:
我们在一般的计算当中都不会对 −1‾‾‾√ − 1 进行定义,然而在复数中, −1‾‾‾√ − 1 等于一个神奇的数: i i ,这个数在复数的定义下相当于1 1 的作用。下面列举一些有关 i i 的计算:
R=(ac−bd)2+(ad+bc)2‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾√=a2c2+b2d2−2abcd+b2c2+a2d2+2abcd‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾√=(a2+b2)(b2+d2)‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾√=r1×r2 R = ( a c − b d ) 2 + ( a d + b c ) 2 = a 2 c 2 + b 2 d 2 − 2 a b c d + b 2 c 2 + a 2 d 2 + 2 a b c d = ( a 2 + b 2 ) ( b 2 + d 2 ) = r 1 × r 2
Θ=arctan(bc+adac−bd)=arctan(bc+adacac−bdac)=arctan(ba+dc1−bdac)=arctan(ba+dc1−ba⋅dc)=arctan(ba)+arctan(dc)=θ1+θ2 Θ = a r c t a n ( b c + a d a c − b d ) = a r c t a n ( b c + a d a c a c − b d a c ) = a r c t a n ( b a + d c 1 − b d a c ) = a r c t a n ( b a + d c 1 − b a ⋅ d c ) = a r c t a n ( b a ) + a r c t a n ( d c ) = θ 1 + θ 2
所以 (r1,θ1)×(r2,θ2)=(r1×r2,θ1+θ2) ( r 1 , θ 1 ) × ( r 2 , θ 2 ) = ( r 1 × r 2 , θ 1 + θ 2 ) ,总结下来就是两个复数相乘,长度相乘,极角相加。由于c++自带的 complex c o m p l e x 函数有点菜菜的,所以我们可以自己手写 complex c o m p l e x 函数。然后我们可以想象一下,如果两个复数到原点的距离都为1的话,那么这两个复数相乘就相当于绕着以原点为圆心的,半径为1的圆进行旋转。
struct comp {
double r,i;
comp() {}
comp(double r,double i):r(r),i(i) {}
};
comp operator + (comp a,comp b) {
return comp(a.r+b.r,a.i+b.i);
}
comp operator - (comp a,comp b) {
return comp(a.r-b.r,a.i-b.i);
}
comp operator * (comp a,comp b) {
return comp(a.r*b.r-a.i*b.i,a.i*b.r+a.r*b.i);
}
我们现在已经知道了答案多项式的点值表达式,我们现在所需要做的任务就是把这个点值表达式转变成系数表达式。最明显的一种方法就是用高斯消元法,暴力去解除这个方程的解,可是这样的复杂度又会变成 O(n2) O ( n 2 ) 了,因为我们在计算 x0,x20,x30⋯xn0 x 0 , x 0 2 , x 0 3 ⋯ x 0 n 的时候会重复计算很多次,而这个计算在实数域中似乎是不可避免的,所以我们就可以回到复数域中。在复数中,我们需要的数是 ωk=1 ω k = 1 的数,而由于复数的乘法的定义,我们可以发现,对于所有的 (1,θ) ( 1 , θ ) 的复数,他的 i i 次方总是在同一个圆上,并且经过一定次数的乘方之后一定是可以等于1的,所以我们就可以有效的利用这种数,将这个数带入,就可以很妙妙的求值了。
我们定义这种ωk=1 ω k = 1 的数为 k k 次单位复根,计作ωnk ω k n ,而这个 n n 实际上是一个序号,表示的是将所有的k k 次单位根按极角序进行排序之后从零开始编号的单位根。
而由于 ω0k=1 ω k 0 = 1 ,所以我们只要知道 ω1k ω k 1 ,就可以计算出所有的 k k 次单位根了。
有关单位复根有很多比较有用的性质,这里对这些性质进行介绍并证明。
既然单位复根相乘满足“长度相乘,极角相加”,而单位复根的长度又是1,所以我们可以发现所有的k k 次单位根是均匀的排布在一个半径为1,以原点为圆心的圆上的。根据欧拉公式 eπi=−1 e π i = − 1 ,而 ωnn=1 ω n n = 1 ,所以 ωnn=(−1)2=(eπi)2=e2πi ω n n = ( − 1 ) 2 = ( e π i ) 2 = e 2 π i ,所以:
先给出引理:
引理:
引理:
首先我们先回到原本的系数表示法:
在这个分治上,我们采用的是递归的形式,然而由于每一次递归都要下传数组,导致这样的常数非常大,空间也有了一些无意义上的消耗,所以我们希望的是能够采用迭代的形式。然后我们开始思考,这个分治的实际结果是什么。我们每次将数组中的数按奇偶分组,实际上就是对于二进制下的这一位进行1与0的分组。并且我们可以观察一下最后分组的结果,以长度为8为例:
void GetRev() {
for(int i=0;i>1]>>1)|((i&1)<<(len-1));
}
}
这样就得出了我们在翻转之后,每一位的序号了,接下来就是简单的迭代了。
处理到这里,我们的FFT就只剩下最后的一步了,就是用IDFT把点值表达式转化成系数表达式。首先我们先考虑我们是如何从系数表达式变成点值表达式的。我们是将单位复根带入原来的系数表达式来计算点值的,我们可以将单位复根带入之后的n个多项式的系数用一个矩阵来表示:
void FFT(comp *a,int IDFT) {//IDFT传进来时为-1,DFT传进来时为1
for(int i=0;iif(ifor(int mid=1;mid1) {
comp w=comp(cos(PI/mid),IDFT*sin(PI/mid));//IDFT是单位复根应该取负,而DFT时单位复根应该取正的。
for(int l=mid<<1,j=0;jcomp wn=comp(1.0,0.0);
for(int k=0;kcomp x=a[k+j];
comp y=a[k+j+mid]*wn;
a[k+j]=x+y;
a[k+j+mid]=x-y;
wn=wn*w;
}
}
}//这里没有除以n是因为在最后除了
}
到这里FFT就已经讲的差不多了,如果仍然没有听明白的话,那么可以移步这里。
#include
using namespace std;
typedef long long ll;
bool Finish_read;
template<class T>inline void read(T &x){Finish_read=0;x=0;int f=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-')f=-1;if(ch==EOF)return;ch=getchar();}while(isdigit(ch))x=x*10+ch-'0',ch=getchar();x*=f;Finish_read=1;}
template<class T>inline void print(T x){if(x/10!=0)print(x/10);putchar(x%10+'0');}
template<class T>inline void writeln(T x){if(x<0)putchar('-');x=abs(x);print(x);putchar('\n');}
template<class T>inline void write(T x){if(x<0)putchar('-');x=abs(x);print(x);}
/*================Header Template==============*/
const int maxn=5e6+500;
const double PI=acos(-1);
int n,m;
int rev[maxn];
int lim=1,len;
/*==================Define Area================*/
struct comp {
double r,i;
comp() {}
comp(double r,double i):r(r),i(i) {}
}a[maxn],b[maxn];
comp operator + (comp a,comp b) {
return comp(a.r+b.r,a.i+b.i);
}
comp operator - (comp a,comp b) {
return comp(a.r-b.r,a.i-b.i);
}
comp operator * (comp a,comp b) {
return comp(a.r*b.r-a.i*b.i,a.i*b.r+a.r*b.i);
}
void GetRev() {
for(int i=0;i>1]>>1)|((i&1)<<(len-1));
}
}
void FFT(comp *a,int IDFT) {
for(int i=0;iif(ifor(int mid=1;mid1) {
comp w=comp(cos(PI/mid),IDFT*sin(PI/mid));
for(int l=mid<<1,j=0;j1.0,0.0);
for(int k=0;kint main() {
read(n);read(m);
while(lim<=n+m) lim<<=1,len++;
GetRev();
for(int i=0;i<=n;i++) scanf("%lf",&a[i].r);
for(int i=0;i<=m;i++) scanf("%lf",&b[i].r);
FFT(a,1);
FFT(b,1);
for(int i=0;i1);
for(int i=0;i<=m+n;i++) {
printf("%d ",(int)(a[i].r/lim+0.5));
}
return 0;
}