快速傅里叶变换(FFT)

FFT(Fast Fourier Transformation)是离散傅氏变换(DFT)的快速算法。即为快速傅氏变换。它是根据离散傅氏变换的奇、偶、虚、实等特性,对离散傅立叶变换的算法进行改进获得的。

Fast Fourier Transform

多项式乘法

假如我们有两个多项式:

A(x)=i=0naixiB(x)=i=0nbixi

我们想求得:

C(x)=i=02n1(j=0iajbij)xi

使用朴素的做法肯定需要 O(n2) 的复杂度。
我们将上述的 A(x) 的系数表示为向量 A ,则称这种表示法为系数表示法。
现在来考虑另一种表示法,即点值表示法。
设向量 A=(A(p1),A(p2),A(p3),...,A(pn)) ,则 A 称作 A(x) 的点值表示。
点值表示有什么优点?首先,n维点值表示与n维系数表示一一对应(证明似乎有点麻烦……),其次,点值表示下的多项式运算得到了化简。
以乘法为例:

C(x)===i=02n1(j=0iajbij)xii=02n1j=0i(ajxj)(bijxij)j=02n1(ajxj)i=02n1(bixi)

这样便转换为了点值表示的两个式子。而且我们可以发现,完成点值表示下的乘法复杂度是O(n) 的。
那么加速多项式乘法的过程,变为了:
系数表示  点值表示  乘法  点值表示  系数表示
而快速傅里叶变换,就是从系数表示转换到点值表示的桥梁。
我们观察到,点值表示下所取的点 p1,p2,...,pn 是可以任取的,那么我们利用一些特殊值,是否能加速运算?

快速傅里叶变换

此处我们引入单位根的概念。即满足:

xn=1

的复数,记为 ωn。它可以看做是复平面上x轴正方向绕逆时针方向旋转 2πn 的复数。
单位根有一些很好的性质,这些性质都可以利用复平面上的向量比较直观地感受。

ω2m2nωm2n==ωmnωm+n2n

假如我们取单位根的幂进行转换,会有什么效果?设 A0(x) 是 A(x) 的偶次项的和, A1(x) 是奇次项的和。那么:

A(ωmn)A(ωm+n2n)====A0((ωmn)2)+ωmnA1((ωmn)2)A0(ωmn2)+ωmnA1(ωmn2)A0((ωmn)2)+ωm+n2nA1((ωmn)2)A0(ωmn2)ωmnA1(ωmn2)

即我们只要有了 A0(x),A1(x) 的点值表示,就能在 O(n) 时间内算出 A(x) 的点值表示。
每一层对于一个确定的 m,我们用对应的两个值来更新出当前的值,称这个操作为“蝴蝶变换”。
分析一下复杂度,A(x) 的规模为 T(n) ,而他的奇、偶次项分别的和的规模都为 T(n2) ,而合并的时间为 O(n) ,即 T(n)=2T(n2)+O(n) ,根据主定理,这个问题的复杂度为O(nlogn).

逆离散傅里叶变换

现在我们已经可以快速从系数表示转换到点值表示了。接下来的问题就是如何从点值表示转换到系数表示。
我们转移到点值表示的过程可以看做是一个矩阵乘法:

⎛⎝⎜⎜⎜⎜⎜ω0nω0nω0n...ω0nω0nω1nω2n...ωn1nω0nω2nω4n...ω2n2n...............ω0nωnnω2nn...ωn2nn⎞⎠⎟⎟⎟⎟⎟⎛⎝⎜⎜⎜⎜a0a1a2...an1⎞⎠⎟⎟⎟⎟=⎛⎝⎜⎜⎜⎜⎜b0b1b2...bn1⎞⎠⎟⎟⎟⎟⎟

那么关键就是找到左边那个矩阵的逆矩阵。这个逆矩阵是存在的……算导上也给出了这个矩阵,即对他的每一个元素取共轭复数,然后除n。为什么会这么想?我们需要逆矩阵的一行乘原矩阵的一列恰为 [i=j] ,由于是单位根所以有:

ωmnωmn=|ωmn|=1i=0n1ωin=[n=1]ωmn=ωm mod nn

代入后发现当 i=j 时,由上性质,正好约去。而 ij 时,我们将所有得到的结果的指数都化到模 n 意义下,由数论知识,恰为一组 0n1 的自然数,由于 n1,值变为0.
这样,我们只需要把带进去的那个 ωmn 的向量翻转一下,然后进行FFT即可。

迭代版FFT

依上述内容便可实现递归版的FFT了,但是这样常数很大。
观察到我们的分治是将奇偶分类,即对于每一层我取这层对应位数的0放在一起,1放在一起。
那么最后元素 i 所处的位置,就是将 i 的二进制表示翻转的位置。
这里我觉得引入 Cooley-Tukey 算法的图比较好:


可以看到,我们每次取对应的两个元素,进行一次“蝴蝶变换”,即完成快速傅里叶变换章节中的对两个元素进行的操作。

Number Theory Transform

复数运算的FFT,常数巨大,精度不高。
当题目所给的模数非常带感时:即 2k|P1,其中 2k>n 时,我们便可以使用数论变换来加速运算。数论变换中的关键为一个等价:

gP1mωm(modP)

其中 g 是 P 的原根。
你可以手动验证一下上述等价的正确性。
原根的求法不赘述了,枚举即可……

Code

说了这么多,理解起来可能也有点困难,倒是代码比较简短。

void FFT_Init() {
    for ( K = 1; K < n << 1; K <<= 1 ); inv_K = inver( K ); w[ 0 ][ 0 ] = w[ 0 ][ K ] = w[ 1 ][ 0 ] = w[ 1 ][ K ] = 1; int G = fpm( g, ( P - 1 ) / K ); FOR( i, 1, K - 1 ) { w[ 0 ][ i ] = (ll)w[ 0 ][ i - 1 ] * G % P; } FOR( i, 0, K ) { w[ 1 ][ i ] = w[ 0 ][ K - i ]; } } void FFT( int X[], int k, int v ) { int i, j, l; for ( i = j = 0; i < k; i++ ) { if ( i > j ) swap( X[ i ], X[ j ] ); for ( l = k >> 1; ( j ^= l ) < l; l >>= 1 ); } for ( i = 2; i <= k; i <<= 1 ) for ( j = 0; j < k; j += i ) for ( l = 0; l < i >> 1; l++ ) { int t = (ll)X[ j + l + ( i >> 1 ) ] * w[ v ][ ( K / i ) * l ] % P; X[ j + l + ( i >> 1 ) ] = ( (ll)X[ j + l ] - t + P ) % P; X[ j + l ] = ( (ll)X[ j + l ] + t ) % P; } } 

猜你喜欢

转载自www.cnblogs.com/guxuanqing/p/9440597.html