FFT-绝对能看懂的-傅里叶变换-掰开揉碎解释-三个c++代码

tip:本文由typora复制过来并修改了一些显示错误,如果还有显示不正确,或者文章内容有误的地方,欢迎在评论区指出

快速傅里叶变换(FFT)详解

前言

DFT:离散傅里叶变换—>O(n^2)计算多项式乘法

FFT:快速傅里叶变换—>O(n∗log⁡(n))计算多项式乘法

FNTT/NTT:快速傅里叶变换的优化版—>优化常数及误差

FWT:快速沃尔什变换—>利用类似FFT的东西解决一类卷积问题

MTT:毛爷爷的FFT—>非常nb/任意模数

FMT 快速莫比乌斯变化

多项式

系数表示法

设A(x)表示一个n−1次多项式


A ( x ) = ∑ i = 1 n a i ∗ x i A(x)=\sum_{i=1}^{n}{a_i*x^i} A(x)=i=1naixi
利用这种方法计算多项式乘法复杂度为O(n)(第一个多项式中每个系数都需要与第二个多项式的每个系数相乘)
例如: A ( 3 ) = 2 + 3 ∗ x + x 2 例如:A(3)=2+3*x+x^2 例如:A(3)=2+3x+x2

点值表示法

将n互不相同的x带入多项式,会得到n个不同的取值y

则该多项式被这n个点 ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x n , y n ) (x_1,y_1),(x_2,y_2),\dots,(x_n,y_n) (x1,y1),(x2,y2),,(xn,yn)唯一确定

其中
y i = ∑ j = 0 n − 1 a j ∗ x i j y_i=\sum_{j=0}^{n-1} a_j*x_i^j yi=j=0n1ajxij
例如:上面的例子用点值表示法可以为(0,2),(1,5),(2,12)

利用这种方法计算多项式乘法的时间复杂度仍然为
O ( n 2 ) (选点 O ( n ) ,每次计算 O ( n ) ) O(n^2)(选点O(n),每次计算O(n)) O(n2)(选点O(n),每次计算O(n)


我们可以看到,两种方法的时间复杂度都为O*(*n^2),我们考虑对其进行优化

对于第一种方法,由于每个点的系数都是固定的,想要优化比较困难

对于第二种方法,貌似也没有什么好的优化方法,不过当你看完下面的知识,或许就不这么想了


复数

在介绍复数之前,首先介绍一些可能会用到的东西

向量

同时具有大小和方向的量

在几何中通常用带有箭头的线段表示

圆的弧度制

等于半径长的圆弧所对的圆心角叫做1弧度的角,用符号rad表示,读作弧度。用弧度作单位来度量角的制度叫做弧度制

公式:
1 ∘ = π 180 18 0 ∘ = π ∗ r a d 1^{\circ }=\dfrac{\pi}{180} \\ 180^{\circ }=\pi *rad 1=180π180=πrad

平行四边形定则

平行四边形定则:AB+AD=AC

复数

定义

设a,b为实数,i²=-1,形如a+bi的数叫负数,其中ii被称为虚数单位,复数域是目前已知最大的域

在复平面中,x代表实数,y轴(除原点外的点)代表虚数,从原点(0,0)到(a,b)的向量表示复数a+bi

模长:从原点(0,0)到点(a,b)的距离,即 a 2 + b 2 \sqrt {a^{2} + b^{2}} {}{} a2+b2

幅角:假设以逆时针为正方向,从xx轴正半轴到已知向量的转角的有向角叫做幅角

运算法则

加法:

因为在复平面中,复数可以被表示为向量,因此复数的加法与向量的加法相同,都满足平行四边形定则(就是上面那个)

乘法:

几何定义:复数相乘,模长相乘,幅角相加

代数定义:
( a + b i ) ∗ ( c + d i ) = a c + a d i + b c i + b d i 2 = a c + a d i + b c i − b d = ( a c − b d ) + ( b c + a d ) i (a+bi)*(c+di)\\ =ac+adi+bci+bdi^2\\ =ac+adi+bci-bd\\ =(ac-bd)+(bc+ad)i (a+bi)(c+di)=ac+adi+bci+bdi2=ac+adi+bcibd=(acbd)+(bc+ad)i

单位根

下文中,默认 n n n 2 2 2的正整数次幂
在复平面上,以原点为圆心,1为半径作圆,所得的圆叫单位圆。以圆点为起点,圆的 n n n等分点为终点,
做n个向量,设幅角为正且最小的向量对应的复数为 ω n \omega_n ωn,称为 n n n次单位根。
根据复数乘法的运算法则,其余n-1个复数为 ω n 2 , ω n 3 , … , ω n n \omega_n^2,\omega_n^3,\ldots,\omega_n^n ωn2,ωn3,,ωnn
注意 ω n 0 = ω n n = 1 \omega_n^0=\omega_n^n=1 ωn0=ωnn=1(对应复平面上以x轴为正方向的向量)

那么如何计算它们的值呢?这个问题可以由欧拉公式解决
ω n k = cos ⁡   k ∗ 2 π n + i sin ⁡ k ∗ 2 π n \omega_{n}^{k}=\cos\ k *\frac{2\pi}{n}+i\sin k*\frac{2\pi}{n}\\ ωnk=cos kn2π+isinkn2π
单位根的幅角为周角的 1 n \dfrac{1}{n} n1

例如
在这里插入图片描述

图中向量AB表示的复数为8次单位根

  • 在代数中,若 z n = 1 z^n=1 zn=1,我们把z称为n次单位根

单位根的性质

1. ω n k = c o s k 2 π n (即上面的公式) 1.ω_n^{k}=cosk\dfrac{2 \pi}{n}(即上面的公式) 1.ωnk=coskn2π(即上面的公式)
证明:这个第一步到第二步由定义得出,第二步到第三步由欧拉公式得出

2. ω 2 n 2 k = ω n k 2.\omega _{2n}^{2k}=\omega _{n}^{k} 2.ω2n2k=ωnk
证明: ω 2 k 2 n = e 2 π 2 k i 2 n = e 2 π k i n = ω n k ω_{2k}^{2n}=e^{\frac{2π2ki}{2n}}=e^{\frac{2πki}{n}}=ω^k_n ω2k2n=e2n2π2ki=en2πki=ωnk

3. ω n k + n 2 = − ω n k 3.ω^{k+\frac{n}{2}}_n=−ω^k_n 3.ωnk+2n=ωnk
证明: ω n k + n 2 = ω 2 n 2 k ∗ ω 2 n n = ω n k ∗ ( c o s π + i ∗ s i n π ) = − ω n k ω^{k+\frac{n}{2}}_n=ω^{2k}_{2n}∗ω^n_{2n}\\=ω^k_n∗(cosπ+i∗sinπ)=−ω^k_n ωnk+2n=ω2n2kω2nn=ωnk(cosπ+isinπ)=ωnk

4. ω 0 n = ω n n = 1 4.ω0n=ωnn=1 4.ω0n=ωnn=1
证明:不用证了吧……


正文之前

这段话有可能有助于您理解本算法:

傅立叶这个大神仙根本就没见过计算机长什么样,所以他提出的傅立叶变换和逆变换只是一种将系数转点值和将点值转系数的方法,没有任何降低复杂度的功效,至于快速傅立叶变换是后人再研究傅立叶变换发现的一种加速方法,是对 D F T DFT DFT I D F T IDFT IDFT的优化

离散傅立叶变换(DFT)

假设 f ( x ) = ∑ i = 0 n − 1 a i ∗ x i f(x)=\sum_{i=0}^{n−1}a_i∗x^i f(x)=i=0n1aixi

D F T ( a ) = ( f ( 1 ) , f ( ω n ) , f ( ω n 2 ) , … … , f ( ω n n − 1 ) ) DFT(a)=(f(1),f(ω_n),f(ω^2_n),……,f(ω^{n−1}_n)) DFT(a)=(f(1),f(ωn),f(ωn2),……,f(ωnn1))

通俗点说,就是对于一个系数表示法的多项式 f ( x ) f(x) f(x),将 ( 1 , ω n , ω n 2 , … … , ω n n − 1 ) (1,ω_n,ω_n^{2},……,ω^{n−1}_n) (1,ωn,ωn2,……,ωnn1)带入求出该多项式的点值表示法

离散傅立叶逆变换(IDFT)

f ( x ) f(x) f(x) n n n n n n次单位根处的点值表示转化为系数表示

这里就可以回答,为什么我们要让 n n n次单位根作为 x x x代入多项式

假设 ( y 0 , y 1 , y 2 , … … , y n − 1 ) (y_0,y_1,y_2,……,y_{n-1}) (y0,y1,y2,……,yn1)是多项式 A ( x ) = ∑ i = 1 n a i ∗ x i A(x)=\sum_{i=1}^{n}{a_i*x^i} A(x)=i=1naixi的离散傅立叶变换。

我们另有一个多项式 B ( x ) = ∑ i = 1 n y i ∗ x i B(x)=\sum_{i=1}^{n}{y_i*x^i} B(x)=i=1nyixi

将上述 n n n次单位根的倒数 ( 1 , ω − 1 n , ω n − 2 , … … , ω n − ( n − 1 ) ) (1,ω^-1_n,ω^{-2}_n,……,ω^{-(n−1)}_n) (1,ω1n,ωn2,……,ωn(n1))带入 B ( x ) B(x) B(x)

得到新的离散傅立叶变换 ( z 0 , z 1 , z 2 , … … , z n − 1 ) (z_0,z_1,z_2,……,z_{n-1}) (z0,z1,z2,……,zn1)

则我们发现
z k = ∑ i = 0 n − 1 y i ∗ ( ω n − k ) i = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ∗ ( ω n i ) j ) ∗ ( ω n − k ) i = ∑ j = 0 n − 1 a j ( ∑ i = 0 n − 1 ( ω n j − k ) i ) z_k=\sum\limits_{i=0}^{n-1}y_i*(\omega _n^{-k})^i \\ =\sum\limits_{i=0}^{n-1}(\sum\limits_{j=0}^{n-1}a_j*(\omega _n^{i})^j)*(\omega _n^{-k})^i \\ =\sum\limits_{j=0}^{n-1}a_j(\sum\limits_{i=0}^{n-1}(\omega _n^{j-k})^i) zk=i=0n1yi(ωnk)i=i=0n1(j=0n1aj(ωni)j)(ωnk)i=j=0n1aj(i=0n1(ωnjk)i)
我们单独考虑:

j − k = 0 j-k=0 jk=0时 , 答案为 n n n

j ≠ k j\ne k j=k时 , 等比数列求和得到

( ω n j − k ) n − 1 ω n j − k − 1 = ( ω n n ) j − k − 1 ω n j − k − 1 = 1 j − k − 1 ω n j − k − 1 = 0 \frac{(\omega _n^{j-k})^n-1}{\omega _n^{j-k}-1}=\frac{(\omega _n^n)^{j-k}-1}{\omega _n^{j-k}-1}=\frac{1^{j-k}-1}{\omega _n^{j-k}-1}=0 ωnjk1(ωnjk)n1=ωnjk1(ωnn)jk1=ωnjk11jk1=0

所以

∑ j = 0 n − 1 a j ( ∑ i = 0 n − 1 ( ω n j − k ) i ) = n ∗ a k \sum\limits_{j=0}^{n-1}a_j(\sum\limits_{i=0}^{n-1}(\omega _n^{j-k})^i)=n*a_k j=0n1aj(i=0n1(ωnjk)i)=nak

a k = z k n a_k=\frac{z_k}{n} ak=nzk

得出结论:对于以 A ( x ) A(x) A(x)的离散傅立叶变换作为系数的多项式 B ( x ) B(x) B(x),取单位根的倒数 ( 1 , ω n − 1 , ω n − 2 , … … , ω n − ( n − 1 ) ) (1,\omega _{n}^{-1},\omega _{n}^{-2},……,\omega _{n}^{-(n-1)}) (1,ωn1,ωn2,……,ωn(n1))作为 x x x代入,再将结果除以 n n n即为 A ( x ) A(x) A(x)的系数

这个结论实现了将多项式点值转化为系数


讲了这么多,貌似跟我们的正题没啥关系啊。。

OK!各位坐稳了,前方高能!


快速傅里叶变换FFT

我们前面提到过,一个n次多项式可以被n个点唯一确定。

那么我们可以把单位根的0到n−1次幂带入,这样也可以把这个多项式确定出来。但是这样仍然是O(n^2)的呀!

我们设多项式A(x)的系数为(a0,a1,a2,…,an−1),即
A ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 ∗ x 3 + a 4 ∗ x 4 + a 5 ∗ x 5 + ⋯ + a n − 2 ∗ x n − 2 + a n − 1 ∗ x n − 1 A(x)=a_0+a_1x+a_2{x^2}+a_3*{x^3}+a_4*{x^4}+a_5*{x^5}+ \dots+a_{n-2}*x^{n-2}+a_{n-1}*x^{n-1} A(x)=a0+a1x+a2x2+a3x3+a4x4+a5x5++an2xn2+an1xn1
将其下标按照奇偶性分类
A ( x ) = ( a 0 ​ + a 2 ​ ∗ x 2 + a 4 ​ ∗ x 4 + ⋯ + a n − 2 ​ ∗ x n − 2 ) + ( a 1 ​ ∗ x + a 3 ​ ∗ x 3 + a 5 ​ ∗ x 5 + ⋯ + a n − 1 ​ ∗ x n − 1 ) A(x)=(a_0 ​ +a_2 ​ ∗x^ 2 +a_ 4 ​ ∗x ^ 4 +⋯+a _ {n−2} ​ ∗x ^ {n−2} )+(a _ 1 ​ ∗x+a _ 3 ​ ∗x ^ 3 +a _ 5 ​ ∗x ^ 5 +⋯+a_ {n−1} ​ ∗x^{ n−1} ) A(x)=(a0+a2x2+a4x4++an2xn2)+(a1x+a3x3+a5x5++an1xn1)
我们设
A 1 ​ ( x ) = a 0 ​ + a 2 ​ ∗ x + a 4 ​ ∗ x 2 + ⋯ + a n − 2 ​ ∗ x n 2 − 1 A 2 ( x ) = a 1 ∗ x + a 3 ∗ x + a 5 ∗ x 2 + ⋯ + a n − 1 ∗ x n 2 − 1 A _ 1 ​ (x)=a _ 0 ​ +a _ 2 ​ ∗x+a _ 4 ​ ∗x ^ 2 +⋯+a _ {n−2} ​ ∗x^{\frac{n}{2}-1} \\ A_2(x)=a_1*x+a_3*{x}+a_5*{x^2}+ \dots+a_{n-1}*x^{\frac{n}{2}-1} A1(x)=a0+a2x+a4x2++an2x2n1A2(x)=a1x+a3x+a5x2++an1x2n1
那么不难得到
A ( x ) = A 1 ( x 2 ) + x ∗ A 2 ( x 2 ) A(x)=A_1(x^2)+x*A_2(x^2) A(x)=A1(x2)+xA2(x2)

我们将 x = ω n k ( k < n 2 ) 代入得 我们将x=\omega_n^k (k<\frac{n}{2})代入得 我们将x=ωnk(k<2n)代入得

A ( ω n k ) = A 1 ( ω n 2 k ) + ω n k A 2 ( ω n 2 k ) 式子① = A 1 ( ω n 2 k ) + ω n k A 2 ( ω n 2 k ) A(\omega_{n}^{k}) =A_1(\omega_{n}^{2k})+\omega_n^kA_2(\omega_{n}^{2k}) \qquad式子①\\ =A_1(\omega_{\frac{n}{2}}^{k})+\omega_n^kA_2(\omega_{\frac{n}{2}}^{k}) A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)式子=A1(ω2nk)+ωnkA2(ω2nk)

同理,将 x = ω n k + n 2 代入得 同理,将x=\omega_n^{k+\frac{n}{2}}代入得 同理,将x=ωnk+2n代入得

A ( ω n k + n 2 ) = A 1 ( ω n 2 k ∗ ω n n ) − ω n k A 2 ( ω n 2 k ∗ ω n n ) = A 1 ( ω n 2 k ) − ω n k A 2 ( ω n 2 k ) 式子② = A 1 ( ω n 2 k ) − ω n k A 2 ( ω n 2 k ) \begin{align*} &A(\omega_n^{k+{\frac{n}{2}}}) =A_1(\omega_n^{2k}*\omega_n^n)-\omega_n^kA_2(\omega_n^{2k}*\omega_n^n)\\ &=A_1(\omega_n^{2k})-\omega_n^kA_2(\omega_n^{2k})\qquad式子②\\ &=A_1(\omega_{\frac{n}{2}}^{k})-\omega_n^kA_2(\omega_{\frac{n}{2}}^{k}) \end{align*} A(ωnk+2n)=A1(ωn2kωnn)ωnkA2(ωn2kωnn)=A1(ωn2k)ωnkA2(ωn2k)式子=A1(ω2nk)ωnkA2(ω2nk)

大家有没有发现什么规律?

没错!这两个式子只有一个常数项不同!

那么当我们在枚举第一个式子的时候,我们可以O(1)的得到第二个式子的值

又因为第一个式子的k在取遍[0,n/2−1]时,k+n/2取遍了[n/2,n−1]

所以我们将原来的问题缩小了一半!

只要求出 A 1 ( x ) 和 A 2 ( x ) 在 ( ω n 2 0 , ω n 2 1 , … … , ω n 2 n 2 − 1 ) 的点值表示, 就可以 O ( n ) 地求出 A ( x ) 在 ( 1 , ω n , ω n 2 , … … , ω n n − 1 ) 只要求出A_1(x)和A_2(x)在(ω^0_\frac{n}{2},ω^1_\frac{n}{2},……,ω^{\frac{n}{2}-1}_\frac{n}{2})的点值表示,\\ 就可以O(n)地求出A(x)在(1,ω_n,ω^2_n,……,ω^{n−1}_n) 只要求出A1(x)A2(x)(ω2n0,ω2n1,……,ω2n2n1)的点值表示,就可以O(n)地求出A(x)(1,ωn,ωn2,……,ωnn1)

而缩小后的问题仍然满足原问题的性质,所以我们可以递归的去搞这件事情!

直到多项式仅剩一个常数项,这时候我们直接返回就好啦

时间复杂度:

不难看出FFT是类似于线段树一样的分治算法。

因此它的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

快速傅里叶逆变换

不要以为FFT到这里就结束了。

我们上面的讨论是基于点值表示法的。

但是在平常的学习和研究中很少用点值表示法来表示一个多项式。

所以我们要考虑如何把点值表示法转换为系数表示法,这个过程叫做傅里叶逆变换

我们假设 ( y 0 , y 1 , y 2 , … , y n − 1 ) 为 ( a 0 , a 1 , a 2 , … , a n − 1 ) 的傅里叶变换(即点值表示) 我们假设(y_0,y_1,y_2,\dots,y_{n-1})为(a_0,a_1,a_2,\dots,a_{n-1})的傅里叶变换(即点值表示) 我们假设(y0,y1,y2,,yn1)(a0,a1,a2,,an1)的傅里叶变换(即点值表示)

那么变换向量 ( c 0 , c 1 , c 2 , … , c n − 1 ) (c_0,c_1,c_2,\dots,c_{n-1}) (c0,c1,c2,,cn1)满足
c k = ∑ i = 0 n − 1 y i ( ω n − k ) i c_k=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i ck=i=0n1yi(ωnk)i

多项式 B ( x ) = y 0 , y 1 x , y 2 x 2 , … , y n − 1 x n − 1 多项式B(x)=y_0,y_1x,y_2x^2,\dots,y_{n-1}x^{n-1} 多项式B(x)=y0,y1x,y2x2,,yn1xn1 ω n 0 , ω n − 1 , ω n − 2 , … , ω n − 1 − ( n − 1 ) \omega_n^{0},\omega_n^{-1},\omega_n^{-2},\dots,\omega_{n-1}^{-(n-1)} ωn0,ωn1,ωn2,,ωn1(n1)处的点值表示

( c 0 , c 1 , c 2 , … , c n − 1 ) 满足 (c_0,c_1,c_2,\dots,c_{n-1})满足 (c0,c1,c2,,cn1)满足
c k = ∑ i = 0 n − 1 y i ( ω n − k ) i = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n i ) j ) ( ω n − k ) i = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n j ) i ) ( ω n − k ) i = ∑ i = 0 n − 1 ( ∑ j = 0 n − 1 a j ( ω n j ) i ( ω n − k ) i ) = ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ( ω n j ) i ( ω n − k ) i = ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j ( ω n j − k ) i = ∑ j = 0 n − 1 a j ( ∑ i = 0 n − 1 ( ω n j − k ) i ) c_k=\sum_{i=0}^{n-1}y_i(\omega_n^{-k})^i \\ =\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^i)^j)(\omega_n^{-k})^i\\ =\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^j)^i)(\omega_n^{-k})^i\\ =\sum_{i=0}^{n-1}(\sum_{j=0}^{n-1}a_j(\omega_n^j)^i(\omega_n^{-k})^i)\\ =\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j(\omega_n^j)^i(\omega_n^{-k})^i\\ =\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j(\omega_n^{j-k})^i\\ =\sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}(\omega_n^{j-k})^i)\\ ck=i=0n1yi(ωnk)i=i=0n1(j=0n1aj(ωni)j)(ωnk)i=i=0n1(j=0n1aj(ωnj)i)(ωnk)i=i=0n1(j=0n1aj(ωnj)i(ωnk)i)=i=0n1j=0n1aj(ωnj)i(ωnk)i=i=0n1j=0n1aj(ωnjk)i=j=0n1aj(i=0n1(ωnjk)i)
S ( x ) = ∑ i = 0 n − 1 x i S(x)=\sum_{i=0}^{n-1}x^i S(x)=i=0n1xi
ω n k \omega_n^k ωnk代入得
S ( ω n k ) = 1 + ( ω n k ) + ( ω n k ) 2 + … ( ω n k ) n − 1 S(\omega_n^k)=1+(\omega_n^k)+(\omega_n^k)^2+\dots(\omega_n^k)^{n-1} S(ωnk)=1+(ωnk)+(ωnk)2+(ωnk)n1
当k!=0时

等式两边同乘 ω n k \omega_n^k ωnk
ω n k S ( ω n k ) = ω n k + ( ω n k ) 2 + ( ω n k ) 3 + ⋯ + ( ω n k ) n \omega_n^kS(\omega_n^k)=\omega_n^k+(\omega_n^k)^2+(\omega_n^k)^3+\dots+(\omega_n^k)^{n} ωnkS(ωnk)=ωnk+(ωnk)2+(ωnk)3++(ωnk)n
两式相减得
ω n k S ( ω n k ) − S ( ω n k ) = ( ω n k ) n − 1 S ( ω n k ) = ( ω n k ) n − 1 ω n k − 1 S ( ω n k ) = ( ω n n ) k − 1 ω n k − 1 S ( ω n k ) = 1 − 1 ω n k − 1 \omega_n^kS(\omega_n^k)-S(\omega_n^k)=(\omega_n^k)^{n}-1\\ S(\omega_n^k)=\frac{(\omega_n^k)^{n}-1}{\omega_n^k-1}\\ S(\omega_n^k)=\frac{(\omega_n^n)^{k}-1}{\omega_n^k-1}\\ S(\omega_n^k)=\frac{1-1}{\omega_n^k-1} ωnkS(ωnk)S(ωnk)=(ωnk)n1S(ωnk)=ωnk1(ωnk)n1S(ωnk)=ωnk1(ωnn)k1S(ωnk)=ωnk111

观察这个式子,不难看出它分母不为0,但是分子为0

因此,当K!=0时, S ( ω n k ) = 0 S(\omega^{k}_{n})=0 S(ωnk)=0

那当k=0时呢?

很显然,
S ( ω n 0 ) = n S(\omega^{0}_{n})=n S(ωn0)=n
继续考虑刚刚的式子
c k = ∑ j = 0 n − 1 a j ( ∑ i = 0 n − 1 ( ω n j − k ) i ) c_k=\sum_{j=0}^{n-1}a_j(\sum_{i=0}^{n-1}(\omega_n^{j-k})^i) ck=j=0n1aj(i=0n1(ωnjk)i)
j ≠ k 时,值为 0 j \neq k时,值为0 j=k时,值为0
j = k 时,值为 n j=k时,值为n j=k时,值为n
因此,
$$
c_k=na_k\

a_k=\frac{c_k}{n}
$$
这样我们就得到点值与系数之间的表示啦

理论总结

至此,FFT的基础理论部分就结束了。

我们来小结一下FFT是怎么成功实现的

首先,人们在用系数表示法研究多项式的时候遇阻

于是开始考虑能否用点值表示法优化这个东西。

然后根据复数的两条性质(这个思维跨度比较大)得到了一种分治算法。

最后又推了一波公式,找到了点值表示法与系数表示法之间转换关系。

emmmm

其实FFT的实现思路大概就是

系数表示法—>点值表示法—>系数表示法

引用一下远航之曲大佬的图

递归版 FFT

递归实现的方法比较简单。

就是按找我们上面说的过程,不断把要求的序列分成两部分,再进行合并

在c++的STL中提供了现成的complex类,但是我不建议大家用,毕竟手写也就那么几行,而且万一某个毒瘤卡STL那岂不是很GG?

#include<bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define eps (1e-8)
inline int read()
{
	int x = 0;
	char ch, f = 1;
	for (ch = getchar(); (ch < '0' || ch > '9') && ch != '-'; ch = getchar());
	if (ch == '-') f = 0, ch = getchar();
	while (ch >= '0' && ch <= '9')
	{
		x = (x << 1) + (x << 3) + ch - '0';
		ch = getchar();
	}
	return f ? x : -x;
}
const int N = 5e6 + 10;
const double pi = acos(-1.0);
int n, m;
struct Complex
{
	double x, y;
	Complex(double tx = 0, double ty = 0)
	{
		x = tx, y = ty;
	}
	inline Complex operator + (const Complex t) const
	{
		return Complex(x + t.x, y + t.y);
	}
	inline Complex operator - (const Complex t) const
	{
		return Complex(x - t.x, y - t.y);
	}
	inline Complex operator * (const Complex t) const
	{
		return Complex(x * t.x - y * t.y, x * t.y + y * t.x);//不懂的看复数的运算那部分
	}
} a[N], b[N];
inline void fft(int limit, Complex *a, int inv)
{
	if (limit == 1) return;//只有一个常数项
	Complex a1[limit >> 1], a2[limit >> 1];
	for (int i = 0; i < limit; i += 2)//根据下标的奇偶性分类
	{
		a1[i >> 1] = a[i], a2[i >> 1] = a[i + 1];
	}
	fft(limit >> 1, a1, inv);
	fft(limit >> 1, a2, inv);
	Complex Wn = Complex(cos(2.0 * pi / limit), inv * sin(2.0 * pi / limit)), w = Complex(1, 0);//Wn为单位根,w表示幂
	for (int i = 0; i < (limit >> 1); ++i, w = w * Wn) //这里的w相当于公式中的k
	{
		a[i] = a1[i] + w * a2[i];
		a[i + (limit >> 1)] = a1[i] - w * a2[i];//利用单位根的性质,O(1)得到另一部分
	}
}
signed main()
{
	n = read(), m = read();
	for (int i = 0; i <= n; ++i) a[i].x = read();
	for (int i = 0; i <= m; ++i) b[i].x = read();
	int limit = 1;
	while (limit <= n + m) limit <<= 1;
	fft(limit, a, 1);
	fft(limit, b, 1);
    //后面的1表示要进行的变换是什么类型
    //1表示从系数变为点值
    //-1表示从点值变为系数
    //至于为什么这样是对的,可以参考一下c向量的推导过程,
	for (int i = 0; i <= limit; ++i)
	{
		a[i] = a[i] * b[i];
	}
	fft(limit, a, -1);
	for (int i = 0; i <= n + m; ++i) printf("%d ", (int)(a[i].x / limit + 0.5)); //按照我们推倒的公式,这里还要除以n
	return 0;
}

然而我们发现好像有点慢

迭代优化

众所周知递归比较慢,我们有没有什么方法可以用迭代代替递归呢?

通过一顿找规律,我们根本不能发现,每个数字在分治后的位置就是它所在位置的二进制翻转

这个规律也有一个好听的名字,叫蝴蝶定理

那么我们只要预处理出每个数字在最后一次递归中的位置,然后自底向上合并,岂不是可以摆脱递归

我们需要求的序列实际是原序列下标的二进制反转!

因此我们对序列按照下标的奇偶性分类的过程其实是没有必要的

这样我们可以O(n)的利用某种操作得到我们要求的序列,然后不断向上合并就好了

#include<bits/stdc++.h>
using namespace std;
#define eps (1e-8)
inline int read()
{
	int x = 0;
	char ch, f = 1;
	for (ch = getchar(); (ch < '0' || ch > '9') && ch != '-'; ch = getchar());
	if (ch == '-') f = 0, ch = getchar();
	while (ch >= '0' && ch <= '9')
	{
		x = (x << 1) + (x << 3) + ch - '0';
		ch = getchar();
	}
	return f ? x : -x;
}
const int N = 5e6 + 10;
const double pi = acos(-1.0);
int n, m;
int limit = 1, len;
int pos[N];
struct Complex
{
	double x, y;
	Complex(double tx = 0, double ty = 0)
	{
		x = tx, y = ty;
	}
	inline Complex operator + (const Complex t) const
	{
		return Complex(x + t.x, y + t.y);
	}
	inline Complex operator - (const Complex t) const
	{
		return Complex(x - t.x, y - t.y);
	}
	inline Complex operator * (const Complex t) const
	{
		return Complex(x * t.x - y * t.y, x * t.y + y * t.x);
	}
} a[N], b[N], buf[N];
inline void fft(Complex *a, int inv)
{
	for (int i = 0; i < limit; ++i)
		if (i < pos[i]) swap(a[i], a[pos[i]]); //求出要迭代的序列
	for (int mid = 1; mid < limit; mid <<= 1) //待合并区间的长度的一半
	{
		Complex Wn(cos(pi / mid), inv * sin(pi / mid));//单位根
		for (int r = mid << 1, j = 0; j < limit; j += r) //R是区间的长度,j表示前已经到哪个位置了
		{
			Complex w(1, 0);
			for (int k = 0; k < mid; ++k, w = w * Wn) //枚举左半部分
			{
                //complex x = a[j + k], y = w * a[j + mid + k];//蝴蝶效应   
                //a[j + k] = x+y; a[j + k + mid] = x-y; 
                //没有后效性也可以直接赋值给a,快一点
				buf[j + k] = a[j + k] + w * a[j + k + mid];
				buf[j + k + mid] = a[j + k] - w * a[j + k + mid]; 
			}
		}
		for (int i = 0; i < limit; ++i) a[i] = buf[i];
	}
}
signed main()
{
	n = read(), m = read();
	for (int i = 0; i <= n; ++i) a[i].x = read();
	for (int i = 0; i <= m; ++i) b[i].x = read();
	while (limit <= n + m) limit <<= 1, ++len;
	for (int i = 0; i < limit; ++i)
		pos[i] = (pos[i >> 1] >> 1) | ((i & 1) << (len - 1));
     // 在原序列中 i 与 i/2 的关系是 : i可以看做是i/2的二进制上的每一位左移一位得来
    // 那么在反转后的数组中就需要右移一位,同时特殊处理一下奇数
	fft(a, 1);
	fft(b, 1);
	for (int i = 0; i <= limit; ++i) a[i] = a[i] * b[i];
	fft(a, -1);
	for (int i = 0; i <= n + m; ++i) 
        printf("%d ", (int)(a[i].x / limit + 0.5));
}

三次变两次优化

观察到上面的代码我们跑了三次肥肥兔 F F T FFT FFT,现在我们有一种方法可以少跑一次

假设我们求 f ( x ) ∗ g ( x ) f(x)*g(x) f(x)g(x)

设复多项式 h ( x ) = f ( x ) + i ∗ g ( x ) h(x)=f(x)+i*g(x) h(x)=f(x)+ig(x) 实部为 f ( x ) ,虚部为 g ( x ) 实部为f(x),虚部为g(x) 实部为f(x),虚部为g(x)

那么 h ( x ) 2 = ( f ( x ) + i ∗ g ( x ) ) 2 = f ( x ) 2 − g ( x ) 2 + i ∗ 2 ∗ f ( x ) ∗ g ( x ) h(x)^2=(f(x)+i*g(x))^2=f(x)^2-g(x)^2+i*2*f(x)*g(x) h(x)2=(f(x)+ig(x))2=f(x)2g(x)2+i2f(x)g(x)

我们只要把 h ( x ) 2 h(x)^2 h(x)2的虚部除以 2 2 2就得到了结果

完全版:

注意三次变两次优化会令精度误差平方,请根据题目值域考虑是否使用

#include<bits/stdc++.h>
using namespace std;
#define eps (1e-8)
inline int read()
{
	int x = 0;
	char ch, f = 1;
	for (ch = getchar(); (ch < '0' || ch > '9') && ch != '-'; ch = getchar());
	if (ch == '-') f = 0, ch = getchar();
	while (ch >= '0' && ch <= '9')
	{
		x = (x << 1) + (x << 3) + ch - '0';
		ch = getchar();
	}
	return f ? x : -x;
}
const int N = 5e6 + 10;
const double pi = acos(-1.0);
int n, m;
int limit = 1, len;
int pos[N];
struct Complex
{
	double x, y;
	Complex(double tx = 0, double ty = 0)
	{
		x = tx, y = ty;
	}
	inline Complex operator + (const Complex t) const
	{
		return Complex(x + t.x, y + t.y);
	}
	inline Complex operator - (const Complex t) const
	{
		return Complex(x - t.x, y - t.y);
	}
	inline Complex operator * (const Complex t) const
	{
		return Complex(x * t.x - y * t.y, x * t.y + y * t.x);
	}
} a[N];
inline void fft(Complex *a, int inv)
{
	for (int i = 0; i < limit; ++i)
		if (i < pos[i]) swap(a[i], a[pos[i]]);//求出要迭代的序列
	for (int mid = 1; mid < limit; mid <<= 1)//待合并区间的长度的一半
	{
		Complex Wn(cos(pi / mid), inv * sin(pi / mid));//单位根
		for (int r = mid << 1, j = 0; j < limit; j += r)
		{
			Complex w(1, 0);
			for (int k = 0; k < mid; ++k, w = w * Wn)//枚举左半部分
			{
				Complex x = a[j + k], y = w * a[j + k + mid];//蝴蝶效应 
				a[j + k] = x + y;
				a[j + k + mid] = x - y;
			}
		}
	}
}
signed main()
{
	n = read(), m = read();
	for (int i = 0; i <= n; ++i) a[i].x = read();
	for (int i = 0; i <= m; ++i) a[i].y = read();
	while (limit <= n + m) limit <<= 1, ++len;
	for (int i = 0; i < limit; ++i)
		pos[i] = (pos[i >> 1] >> 1) | ((i & 1) << (len - 1));
	fft(a, 1);//三次变两次优化
	for (int i = 0; i <= limit; ++i) a[i] = a[i] * a[i];
	fft(a, -1);//三次变两次优化 
	for (int i = 0; i <= n + m; ++i) printf("%d ", (int)(a[i].y / limit / 2 + 0.5));
}
参考博客

%%%red

%%%自为风月马前卒

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值