写在前面
本文仅面向学习 FFT 算法的 CS 学生。文中关于傅里叶变换的阐述仅用于帮助快速简要地理解算法背后时域/频域变换的意义,逻辑性不会像「信号与系统」课那么强。本人没系统学过「信号与系统」这门课,能写出这篇文章也要感谢来自 FDU、SCUT 的两位同学的帮助。欢迎各位大佬对本文内容进行指正!
要系统学习「信号与系统」的内容,可参阅奥本海默著的《信号与系统》一书。
在我学习 FFT 时,第一个面对的问题背景是「给定两个多项式,如何快速求得二者的乘积?」老师或者博客会先介绍多项式的系数表示和点值表示(在下一节也会再啰嗦一遍),然后引入单位根和分治思想来实现两种表示法间的转换。这篇专栏对算法内容及前置知识的讲解算是非常详尽的了,但它并没有解决一个问题:傅里叶变换的本质是将时域上的卷积转化成频域上的乘法,那么多项式的系数/点值表示为什么能和信号的时域/频域对应?本文将尝试从 FFT 的应用意义出发来探讨这个算法,并尝试揭示其中的关联。
多项式
多项式的表示
给定一个环 R(通常是交换环)和一个未知数 x,则形如:
p(x)=a0+a1x+a2x2+⋯+anxn
的表达式称作 R 上的(一元)n 次多项式(Polynomial of order n)(其中 a0,…,an∈R)。本文默认 R=C,但系数在 R 中取值。上述形式称作多项式 p 的系数表示。
由代数基本定理,上述多项式在 C 上存在 n 个根 x1,x2,…,xn,则 p 可写成以下形式:
p(x)=A(x−x1)(x−x2)…(x−xn)
取 x0∈/{x1,…,xn},则 A 的值可以唯一确定。一般地,一个 n 次多项式可以由 R2 上的 n+1 个点 (x0,y0),…,(xn,yn) 唯一确定/表示。这种表示方法称作多项式 p 的点值表示。
多项式的计算
令:
p(x)q(x)=a0+a1x+⋯+anxn=b0+b1x+⋯+bnxn
其点值表示为:
p(x)q(x):(x0,y0),(x1,y1),…,(xn,yn):(x0,z0),(x1,z1),…,(xn,zn)
其和为:
(p+q)(x)=(a0+b0)+(a1+b1)x+⋯+(an+bn)xn(p+q)(x):(x0,y0+z0),(x1,y1+z1),…,(xn,yn+zn)
积为:
(pq)(x)=a0b0+(a0b1+a1b0)x+⋯+(0≤i≤k∑aibk−i)xk+⋯+anbnx2n(pq)(x):(x0,y0z0),(x1,y1z1),…,(xn,ynzn),…,(x2n,y2nz2n)
(注意做乘法时需要 2n+1 个点,因为乘积次数为 2n)
对某一个值 x 求值时:
p(x)p(x)=a0+a1x+⋯+anxn=a0+(a1+(a2+…(an−1+anx)x…)x)x=k=0∑n∏i=k(xk−xi)∏i=k(x−xi)yk
使用点值表示求值本质上是在解下面这个线性方程组(写成矩阵的形式):
y0y1⋮yn=11⋮1x0x1⋮xn⋯⋯⋱⋯x0nx1n⋮xnna0a1⋮an
然后求:
y=[1x⋯xn]a0a1⋮an=[1x⋯xn]11⋮1x0x1⋮xn⋯⋯⋱⋯x0nx1n⋮xnn−1y0y1⋮yn
关于该式的计算会在“线性代数”速览一节展开。
不难看出,两种表示方式在求积和求值上分别具有优势。其实这里的系数/点值转换和信号的时域/频域转换非常相似——两种表示法下的乘法过程可以和时域上的卷积、频域上的乘法对应起来。事实上在(我见过的)几乎所有的 FFT 博客/讲义都把这个过程叫做 DFT/IDFT。下一节我将简要介绍傅里叶变换(Fourier Transformation,FT)的来由,以及离散傅里叶变换(Discrete Fourier Transformation,DFT)和多项式的联系。
“信号与系统”速览
这一部分实际上省略了大量铺垫内容,部分内容可能阐释不清,如有不明白的地方请参阅奥本海默并以书上内容为准。
冲激函数和线性时不变系统
冲激函数(数学上常称为 Dirac δ 函数,下简称 δ 函数)是定义在 R 上的一个广义函数。其满足:
- δ(x)=0⟺x=0;
- ∫−∞+∞δ(x)dx=1。
离散情形(可认为在 Z 上)下的 δ 函数可视作:
δ[x]={1,0,x=0x=0
线性时不变系统(Linear and Time Invariant System,LTIS)可视作某信号处理函数,顾名思义满足以下性质:
- 时不变性:输出不会随时间变化,若输入信号为平移后的 x(t+t0),则输出也为平移后的 y(t+t0);
- 线性性:输入某些信号的线性组合 ∑kckxk(t),输出也为对应结果的线性组合 ∑kckyk(t)。特别地,若输入为 ∫−∞+∞cωxω(t)dω,对应输出也为 ∫−∞+∞cωyω(t)dω。
LTIS 在连续信号上的作用
观察到 δ 函数的性质,对任一连续信号 f(t),其值可表示为:
f(t)=∫−∞+∞f(τ)δ(t−τ)dτ
将上式中 f(τ) 视为 δ(t−τ) 的系数,则由 LTIS 的性质可知作用后的结果为:
y(t)=∫−∞+∞f(τ)h(t−τ)dτ
其中 h 是 LTIS 作用在 δ 上的结果。
下面观察 f(t)=ejωt(其中 j 为虚数单位,ω 为频率,下同)的情形:
观察 y(t) 的形式,不难发现这其实是一个卷积式,即 y=f∗h,其中 h 表征了作用在 f 上的 LTIS。由卷积式的对称性,上式可写成:
y(t)=∫−∞+∞ejω(t−τ)h(τ)dτ&=ejωt∫−∞+∞e−jωτh(τ)dτ
记 H(ω)=∫−∞+∞e−jωτh(τ)dτ,则上式可写作 y(t)=f(t)H(ω),即 LTIS 作用的结果仅为 ejωt 乘以一个关于 ω 的系数。
为什么要讨论这个函数形式?由欧拉公式 ejx=cosx+jsinx 知,复指数和周期函数有着天然的联系。由于复指数信号在 LTIS 中有上述优秀的性质,人们很自然地想把任意周期函数(信号)表示成一系列 ejωit 的线性组合(这也是一个粗糙的傅里叶级数的雏形):
f(t)=k∑A(ωk)ejωkt
事实上,非周期函数(信号)可以类似地以积分形式表示,可以认为非周期函数是周期趋近无穷大的周期函数:
f(t)=∫−∞+∞A(ω)ejωtdω(1)
对于表征 LTIS 的函数 g(t),有:
B(ω)=∫−∞+∞e−jωτg(τ)dτ(2)
则二者卷积结果为:
(f∗g)(t)=∫−∞+∞A(ω)B(ω)ejωtdω(3)
不难看出,f(t) 和 g(t) 是时域上的函数(以时间 t 为自变量),而 A(ω) 和 B(ω) 是频域上的函数(以频率 ω 为自变量)。上面的 (1) 式和 (2) 式可以证明是互逆的操作(即傅里叶变换和逆傅里叶变换,证明参考 Stein 的《傅里叶分析导论》第五章)。由 (3) 式知,时域上的卷积和频域上的乘法等价。至此,我们应该对傅里叶级数和傅里叶变换有了非常粗浅简略的认识,下面来对傅里叶变换进行拓展。
FT 的拓展:DFT 与多项式表示
下面提出时域函数和频域函数的两条特性,不作证明:
- 时域的周期化对应频域的离散化;
- 时域的离散化对应频域的周期化。
两条特性比较拗口,但是有高度的对称性。第一条特性比较好理解:时域上的周期函数可以用一系列成谐波关系的复指数函数 fk(t)=ejkω0t(k=0,±1,±2,…)的线性组合表示,表现在频域上则是离散的图像;第二条可以结合对称性和逆傅里叶变换理解。
根据这两个特性,下面我们直接拓展到离散傅里叶变换(Discrete Fourier Transformation,DFT)。DFT 处理的是有限离散序列的卷积,该序列可以被认为是一个离散周期信号的主值序列(循环节)。变换后的序列也将是离散周期序列。
上面提到过 δ 函数在离散情形下的形式。由离散周期性,可以将无限求和等价写成有限求和的形式。令信号周期为 N,则可以类似地表示:
f[t]=k=0∑N−1f[k]δ[t−k]
其中 t=0,1,…,N−1。LTIS 的作用结果为:
y[t]=k=0∑N−1f[k]h[t−k]
上文提到过,周期信号可由一系列谐波函数叠加表示。下面讨论 f[t]=ejN2πit 的情形:
y[t]=k=0∑N−1ejN2πi(t−k)h[k]&=ejN2πitk=0∑N−1e−jN2πikh[k]
令 H[i]=∑k=0N−1e−jN2πikh[k],则 y[t]=f[t]H[i]。
由于 ejN2πit 在离散情形下只有 N 个不同取值,一般的离散周期信号可以表示为:
f[t]=i=0∑N−1ciejN2πit
对 f[t] 做 DFT 的结果为:
A[i]=t=0∑N−1f[t]e−jN2πit
令 ωi=e−jN2πit(其中 i=0,1,…,N−1),则 DFT 写成矩阵的形式为:
A[0]A[1]⋮A[N−1]=11⋮1ω0ω1⋮ωN−1⋯⋯⋱⋯ω0N−1ω1N−1⋮ωN−1N−1f[0]f[1]⋮f[N−1]
因此 IDFT 的形式为:
f[0]f[1]⋮f[N−1]=11⋮1ω0ω1⋮ωN−1⋯⋯⋱⋯ω0N−1ω1N−1⋮ωN−1N−1−1A[0]A[1]⋮A[N−1]=N111⋮1ω0−1ω1−1⋮ωN−1−1⋯⋯⋱⋯ω0−(N−1)ω1−(N−1)⋮ωN−1−(N−1)A[0]A[1]⋮A[N−1]
上式的 ωi 实际就是单位根。关于该式的计算会在“线性代数”速览一节展开。
到这一步为止,我们可以揭示出多项式和离散周期信号的关系:将多项式的系数视作周期信号在时域上一个周期内的取值,则频域上的值就是该多项式在 N 个单位根上的取值。这样一来,DFT 和 IDFT 这两个词应用在多项式计算上也就不显生硬了。
光揭示了多项式和 DFT 的联系还不够,按上式进行变换的时间复杂度仍为 O(N2)。下面我们先把遗留的矩阵计算问题啰嗦一下,再来讲 FFT 如何提高计算速度。
“线性代数”速览
这一节只是对范特蒙德矩阵相关计算的解说,不感兴趣的话可以跳过。
行列式快速复习
对于 R 上的 n×n 方阵 M=(mij),令 Sn 为 {1,2,…,n} 上的置换群,则 M 的行列式为:
detM=σ∈Sn∑i=1∏nmi,σ(i)
行列式有以下性质:
- 转置矩阵的行列式和原矩阵相同;
- 矩阵某一行(列)乘以某个倍数,行列式乘以相同的倍数;
- 矩阵某一行(列)加上另一行(列)的某个倍数,行列式不变;
- 矩阵行列式非零当且仅当矩阵可逆。
矩阵 M 的伴随矩阵记作 M∗=(Mij)T,其中 Mij 代表 M 关于 mij 的代数余子式(即去掉第 i 行第 j 列后矩阵的行列式乘以 (−1)i+j)。若 M 可逆,则 M−1=detM1M∗。
范特蒙德矩阵
范特蒙德(Vandermonde)矩阵指形如下面形式的矩阵(有时也指它的转置):
V=11⋮1x1x2⋮xn⋯⋯⋱⋯x1n−1x2n−1⋮xnn−1
不难看出,若存在 i=j 使得 xi=xj,则 detV=0。
定理 范特蒙德矩阵的行列式为 detV=∏1≤i<j≤n(xj−xi)。
证明 记 V1=V,将 V1 从右向左每列依次减去左边一列的 x1 倍,则:
V1′=11⋮10x2−x1⋮xn−x1⋯⋯⋱⋯0x2n−2(x2−x1)⋮xnn−2(xn−x1)
对第一行做拉普拉斯展开,则:
detV=detV1=detV1′&=detx2−x1⋮xn−x1⋯⋱⋯x2n−2(x2−x1)⋮xnn−2(xn−x1)=i=2∏n(xi−x1)det1⋮1⋯⋱⋯x2n−2⋮xnn−2
记 V2 为上面最后一步得到的矩阵,类似地有:
detV2=i=3∏n(xi−x2)detV3
依次类推,行列式得证。
“点值表示法”的求值
令:
X=11⋮1x0x1⋮xn⋯⋯⋱⋯x0nx1n⋮xnn
下面计算该式:
y=[1x⋯xn]X−1y0y1⋮yn
讨论 yk 的系数,由拉普拉斯展开式的形式,有:
∏0≤i<j≤n(xj−xi)1[1x⋯xn]Xk0Xk1⋮Xkn=∏0≤i<j≤n(xj−xi)1det1⋮1⋮1x0⋮x⋮xn⋯⋱⋯⋱⋯x0n⋮xn⋮xnn=∏0≤i<j≤n(xj−xi)∏0≤i<j≤n,i=k,j=k(xj−xi)∏0≤i<k(x−xi)∏k<j≤n(xj−x)=∏i=k(xk−xi)∏i=k(x−xi)
IDFT 矩阵的计算
DFT 矩阵为:
F=11⋮1ω0ω1⋮ωN−1⋯⋯⋱⋯ω0N−1ω1N−1⋮ωN−1N−1
F−1 也可用上述方法暴力计算,但观察到 N1F 是厄米(Hermitian)矩阵,即 N1FFˉT=I,其中 Fˉ 表示 F 的元素取共轭复数的结果,很自然地就有:
F−1=N1Fˉ
FFT 如何提速?
下面介绍快速傅里叶变换(Fast Fourier Transformation,FFT)的核心思路。
FFT 的提速得益于分治的思想。考虑下列 n 次多项式(n 为奇数):
p(x)=a0+a1x+⋯+an−1xn−1anxn
将 p 的系数按奇偶次项分开,构造下列多项式:
p0(x)p1(x)=a0+a2x+⋯+an−1x2n−1=a1+a3x+⋯+anx2n−1
则有:
p(x)p(−x)=p0(x2)+p1(x2)⋅x=p0(x2)−p1(x2)⋅x
这样可将多项式的计算拆分成两个长度为原来一半的多项式的计算。为保证每次恰好能将多项式分成长度相等的两个多项式,可以将原多项式补全成 2k−1 次(k 为某个正整数),高次项系数补 0。
接下来探讨单位复根的使用:
记 ωnk=ejn2πk,则有 ωnk=ω2n2k=(ω2nk)2,−ωnk=ωnk+n/2,证明略。
代入上式则有:
p(ωnk)p(ωnk+n/2)=p0(ωn/2k)+p1(ωn/2k)⋅ωnk=p0(ωn/2k)−p1(ωn/2k)⋅ωnk
取 k=0,1,2,…,2n−1,则可以通过拆分后的式子计算原多项式的值,总时间复杂度降为 O(nlogn)。
IDFT 的过程和 DFT 基本一致,只有细节上的差距,这里略去,详情可见下面的代码模板。
至此,在多项式乘法中应用 DFT/IDFT 才真正具有意义:傅里叶变换不是无用功,它能帮助显著缩短计算时间。
代码模板
实现细节这里不再赘述,请参考这篇专栏或搜索 Cooley-Tukey 算法(其中使用了一些位运算技巧来减小分治的空间开销)。实际使用中调用带template的两个函数即可。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <complex>
using namespace std;
namespace fft {
typedef complex<double> cd;
const int MAXL = 3600180;
const double PI = acos(-1.0);
cd a[MAXL], b[MAXL];
int rev[MAXL];
inline void get_rev(int bit) {
for (int i = 0; i < (1 << bit); i++) {
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
}
}
inline void fft(cd* a, int n, int dft) {
for (int i = 0; i < n; i++) {
if (i < rev[i]) {
swap(a[i], a[rev[i]]);
}
}
for (int step = 1; step < n; step <<= 1) {
cd omega = exp(cd(0, dft * PI / step));
for (int j = 0; j < n; j += (step << 1)) {
cd omk(1, 0);
for (int k = j; k < j + step; k++) {
cd x = a[k];
cd y = omk * a[k + step];
a[k] = x + y;
a[k + step] = x - y;
omk *= omega;
}
}
}
if (dft == -1) {
for (int i = 0; i < n; i++) {
a[i] /= n;
}
}
}
inline void init() {
memset(a, 0, sizeof(a));
memset(b, 0, sizeof(b));
memset(rev, 0, sizeof(rev));
}
template <class T>
inline void get_conv(T* arr1, int len1, T* arr2, int len2) {
init();
int bit = 1;
while ((1 << bit) < len1 + len2 - 1) bit++;
int s = 1 << bit;
for (int i = 0; i < len1; i++) {
a[i] = double(arr1[i]);
}
for (int i = 0; i < len2; i++) {
b[i] = double(arr2[i]);
}
get_rev(bit);
fft(a, s, 1);
fft(b, s, 1);
for (int i = 0; i < s; i++) {
a[i] *= b[i];
}
fft(a, s, -1);
}
template <class T>
inline void get_ans(T* ans, int st, int ed) {
for (int i = st; i < ed; i++) {
ans[i - st] = T(a[i].real() + 0.5);
}
}
} // namespace fft
后记
以上内容为一个学了一年 FFT 没学明白的蒟蒻试图在一个下午之内参透 FT 的含义的产物。其中有很多内容可能存在疏漏,如果有更加准确却不失简洁性的解释,欢迎联系指正。
不过估计也不会有人会像我一样钻牛角尖钻出这么个四不像吧?