FFT 快速傅里叶变换

前置知识

复数

多项式

形如

F(x)=i=0naixiF(x)=\sum_{i=0}^n a_i x^i

的式子。其中 nn 为非负整数。我们只需知道所有 aia_i,就可以确定一个多项式。这就是多项式的系数表示法。

为了方便运算,我们引入多项式的点值表示法。

对于一个 nn 次多项式 F(x)F(x),我们可以用 n+1n+1 个互不相同的点 {(x0,F(x0)),(x1,F(x1)),,(xn+1,F(xn+1))}\{(x_0,F(x_0)),(x_1,F(x_1)),\cdots,(x_{n+1},F(x_{n+1}))\} 来确定这个多项式。我们下文称这个集合为 SF(x)S_{F(x)}

系数表示法转为点值表示法的过程叫做 DFT,反之叫 IDFT。

卷积

H(x)=F(x)×G(x)H(x)=F(x)\times G(x) 表示 H(x)H(x)F(x)F(x)G(x)G(x) 的卷积。

刚才的多项式乘法求得的式子并不是标准的多项式形式。我们设 H(x)=F(x)×G(x)=k=0n+mckxkH(x)=F(x)\times G(x)=\sum_{k=0}^{n+m}c_kx^k。则每项的系数 ck=i+j=kaibjc_k=\sum_{i+j=k}a_ib_j,变为更容易计算的形式

ck=i=max(0,km)min(n,k)aibkic_k=\sum_{i=\max(0,k-m)}^{\min(n,k)}a_ib_{k-i}

若使用点值表示法,设

SF(x)={(xi,yi)}S_{F(x)}=\{(x_i,y_i)\}

SG(x)={(xi,yi)}S_{G(x)}=\{(x_i,y'_i)\}

则有

SH(x)={(xi,yiyi)}S_{H(x)}=\{(x_i,y_iy'_i)\}

单位根

根据代数基本定理[1]xn=1x^n=1nn 个根,这 nn 个根都称为单位根。记作 {ωnkk=0,1,,n1}\{\omega_n^k\mid k=0,1,\cdots,n-1\},其中,ωn0=1\omega_n^0=1。在复平面上,它们刚好将单位圆 nn 等分。一般说的单位根 ωn\omega_n,指从 (1,0)(1,0) 开始逆时针方向上的第一个根。

一些下面会用到的小式子:

  • 对于偶数次单位根,有 ωni=ωni+n2\omega_n^i=-\omega_n^{i+\frac{n}{2}}(其实就是在复平面上关于原点中心对称)。
  • ω2n2k=ωnk\omega_{2n}^{2k}=\omega_n^k
  • (ωnk)2=ωn2k(\omega_n^k)^2=\omega_n^{2k}

FFT

显然,朴素算法求解 F(x)G(x)F(x) * G(x) 的时间复杂度为 O(nm)O(nm),而 FFT 可以让我们在 O(nlogn)O(n\log n) 的时间复杂度内计算两个 nn 次多项式的乘法。基本思想是分治。

我们只需对 F(x)F(x)G(x)G(x) 进行 DFT,计算 SH(x)=SF(x)SG(x)S_{H(x)}=S_{F(x)}*S_{G(x)},最后再对 SH(x)S_{H(x)} IDFT 即得 H(x)H(x)

接下来说 DFT 的过程。

对于 F(x)F(x),将其划分为奇次与偶次两部分。

F(x)=i=0n2a2ix2i+i=0n2a2i+1x2i+1F(x)=\sum_{i=0}^{\frac{n}{2}}a_{2i}x^{2i}+\sum_{i=0}^{\frac{n}{2}}a_{2i+1}x^{2i+1}

将右半部分提出一个 xx

F(x)=i=0n2a2ix2i+xi=0n2a2i+1x2iF(x)=\sum_{i=0}^{\frac{n}{2}}a_{2i}x^{2i}+x\sum_{i=0}^{\frac{n}{2}}a_{2i+1}x^{2i}

将前后两部分用新的多项式表示

F1(x)=i=0n2a2ixiF_1(x)=\sum_{i=0}^{\frac{n}{2}}a_{2i}x^{i}

F2(x)=i=0n2a2i+1xiF_2(x)=\sum_{i=0}^{\frac{n}{2}}a_{2i+1}x^{i}

F(x)=F1(x2)+xF2(x2)F(x)=F_1(x^2)+xF_2(x^2)

这时我们代入 ωnk\omega_n^k,可得

F(ωnk)=F1((ωnk)2)+ωnkF2((ωnk)2)=F1(ωn2k)+ωnkF2(ωn2k)=F1(ωn2k)+ωnkF2(ωn2k)\begin{aligned} F(\omega_n^k)&=F_1((\omega_n^k)^2)+\omega_n^kF_2((\omega_n^k)^2)\\ &=F_1(\omega_n^{2k})+\omega_n^kF_2(\omega_n^{2k})\\ &=F_1(\omega_\frac{n}{2}^k)+\omega_n^kF_2(\omega_\frac{n}{2}^k) \end{aligned}

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

F(ωnk+n2)=F1(ωn2k)ωnkF2(ωn2k)F(\omega_n^{k+\frac{n}{2}})=F_1(\omega_\frac{n}{2}^k)-\omega_n^kF_2(\omega_\frac{n}{2}^k)

所以,我们可以根据 F1(ωn2k)F_1(\omega_\frac{n}{2}^k)F2(ωn2k)F_2(\omega_\frac{n}{2}^k) 求出 F(ωnk)F(\omega_n^k)F(ωnk+n2)F(\omega_n^{k+\frac{n}{2}})。这种做法只能处理长度为 22 的正整次幂的多项式,所以我们要把高次系数补为 00

接下来是 IDFT。它的操作与 DFT 极像,就是将 ωnk\omega_n^k 变为 ωnk\omega_n^{-k},并在最后乘 1n\dfrac{1}{n}

现实计算中,递归处理效率较低,我们使用位逆序置换优化和蝶形运算优化,直接将值排列为特定的顺序,避免了递归和额外的临时数组。

位逆序置换与蝶形运算

我们要想避免递归,就要将需要一起计算的部分放在一起。

我们以 77 次多项式为例,有 88aia_i,具体划分方式如图

规律很难注意到,我直接说了,就是将每个下标的二进制反转,以反转后的数为新下标。例如,33 的二进制是 011011,反转后为 110110,即 66,从图上来看,a3a_3 确实到了原 a6a_6 的位置。

位逆序置换后,我们可以直接计算 F1(x)F_1(x)F2(x)F_2(x) 而无需临时数组,因为计算要用到的数与计算完成后的数应当被存在相同的下标内,直接覆盖原数就行了。具体地,计算 F(ωnk)F(\omega_n^k)F(ωnk+n2)F(\omega_n^{k+\frac{n}{2}})F1(ωn2k)F_1(\omega_\frac{n}{2}^k) 的值在下标 kkF2(ωn2k)F_2(\omega_\frac{n}{2}^k) 的值在下标 k+n2k+\dfrac{n}{2}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
constexpr int N=1<<22;//注意这里 n 需要开到第一个大于 n+m 的 2 的整次幂
const double PI=4*atan(1);//这个 π 的精确度非常高
struct Complex{
double real,imag;
friend Complex operator+(const Complex &x,const Complex &y){
return {x.real+y.real,x.imag+y.imag};
}
friend Complex operator-(const Complex &x,const Complex &y){
return {x.real-y.real,x.imag-y.imag};
}
friend Complex operator*(const Complex &x,const Complex &y){
return {x.real*y.real-x.imag*y.imag,x.real*y.imag+y.real*x.imag};
}
};
void DFT(vector<Complex> &f,int n,int rev){
for(int i=0,j=0;i<n;i++){
if(i<j) swap(f[i],f[j]);
for(int k=n>>1;(j^=k)<k;k>>=1);
}
for(int k=2;k<=n;k<<=1){
double arg=2*PI*rev/k;
Complex wn={cos(arg),sin(arg)};
for(int i=0;i<n;i+=k){
Complex w={1,0};
for(int j=0;j<k/2;j++){
Complex f1=f[i+j];
Complex f2=f[i+j+k/2];
f[i+j]=f1+w*f2;
f[i+j+k/2]=f1-w*f2;
w=w*wn;
}
}
}
if(rev==-1)
for(int i=0;i<n;i++)
f[i]={f[i].real/n,f[i].imag/n};
}
int n,m,len;
vector<Complex> a,b,c;
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=0,x;i<=n;i++){
cin>>x;
a.push_back({double(x),0});
}
for(int i=0,x;i<=m;i++){
cin>>x;
b.push_back({double(x),0});
}
len=1;while(len<=n+m) len<<=1;
a.resize(len),b.resize(len),c.resize(len);
DFT(a,len,1);
DFT(b,len,1);
for(int i=0;i<len;i++)
c[i]=a[i]*b[i];
DFT(c,len,-1);
for(int i=0;i<=n+m;i++)
cout<<int(round(c[i].real))<<' ';
return 0;
}

代码实现的一些注意事项:

  • std::complex 提供复数运算,但是跑得非常慢,建议手写。

  • 如果涉及多次 FFT 或者多项式次数特别高,建议预处理单位根以提升效率和精度。

参考资料

https://www.cnblogs.com/Kenma/p/18813688

https://oi-wiki.org/math/poly/fft/

特别鸣谢,解决了我在数学上的一些疑惑。



FFT 快速傅里叶变换
https://headless-piston.github.io/2025/04/19/FFT 快速傅里叶变换/
作者
headless-piston
发布于
2025年4月19日
许可协议