FFT Algorithm


用 FFT 计算多项式乘法

给定两个多项式

A(x)=k=0n1akxk,B(x)=k=0m1bkxk,\begin{aligned} A(x)&=\sum_{k=0}^{n-1}a_kx^k,\\ B(x)&=\sum_{k=0}^{m-1}b_kx^k, \end{aligned}

的系数 (a0,a1,,an1)\left(a_0,a_1,\cdots,a_{n-1}\right)(b0,b1,,bm1)\left(b_0,b_1,\cdots,b_{m-1}\right) ,我们希望求出多项式

C(x)=A(x)B(x)=k=0n+m2ckxkC(x)=A(x)B(x)=\sum_{k=0}^{n+m-2}c_kxk

的各项系数

ck=i+j=kaibj,k{0,1,,n+m2}.c_k=\sum_{i+j=k}a_ib_j,\quad k\in\{0,1,\cdots,n+m-2\}.

首先,为了简化问题,我们假定 m=nm=n ,并且 nn22 的正整数次幂。实际情况中,这两点都可以通过给后面补上若干个 00 来完成。

点值表示法 (point-value representation)

通过一个系数向量 (a0,a1,,an1)\left(a_0,a_1,\cdots,a_{n-1}\right) 来表示一个多项式的方式称为系数表示法。系数表示法让我们可以很容易地在 O(n)O(n) 的时间里求出多项式在某一点处的值,但是用它来计算多项式的乘积是比较麻烦的。

注意到,给定平面上的 nn 个点 (x0,y0),(x1,y1),,(xn1,yn1)\left(x_0,y_0\right),\left(x_1,y_1\right),\cdots,\left(x_{n-1},y_{n-1}\right) ,其中所有 xix_i 都不相等,这 nn 个点可以唯一确定一个不超过 n1n-1 次的多项式,比如 22 个点可以确定一条直线, 33 个不共线的点可以确定一条抛物线。因此我们还有一种表示多项式 A(x)A(x) 的方式:

(x0,A(x0)),(x1,A(x1)),,(xn1,A(xn1)),ij    xixj.\left(x_0,A\left(x_0\right)\right),\left(x_1,A\left(x_1\right)\right),\cdots,\left(x_{n-1},A\left(x_{n-1}\right)\right),\quad i\neq j\implies x_i\neq x_j.

这种表示方式称为点值表示法。给定两个多项式 A(x)A(x)B(x)B(x) 的点值表示,我们可以很容易地在 O(n)O(n) 的时间内求出它们的乘积 C(x)C(x) 的点值表示,因为 C(xk)=A(xk)B(xk)C\left(x_k\right)=A\left(x_k\right)B\left(x_k\right)

所以我们的思路就是:给定 A(x)A(x)B(x)B(x) 的系数表示,先快速地求出它们的点值表示,然后花 O(n)O(n) 的时间算出它们的乘积 C(x)C(x) 的点值表示,再快速地求出 C(x)C(x) 的系数表示。

离散傅里叶变换 (Discrete Fourier Transform)

要想快速地求出 A(x)A(x)B(x)B(x) 的点值表示,点 x0,x1,x_0,x_1,\cdots 的选取至关重要。

从现在开始,我们用 ii 表示虚数单位。

首先引入单位根 (root of unity) 的概念。一个 nn 次单位根指的是一个复数 ω\omega ,满足

ωn=1.\omega^n=1.

这个方程在复数域恰好有 nn 个解,所以 nn 次单位根恰好有 nn 个,其中第 kk 个 (0k<n0\leqslant k<n) 是 e2πik/ne^{2\pi ik/n} 。我们可以用著名的欧拉公式 eix=cosx+isinxe^{ix}=\cos x+i\sin x 来理解它:

e2πik/n=cos2πkn+isin2πkn.e^{2\pi ik/n}=\cos\frac{2\pi k}{n}+i\sin\frac{2\pi k}{n}.

它的主辐角恰好为 2πkn2\pi\cdot\dfrac kn ,所以 nn 个单位根均匀地分布在以复平面的原点为圆心的单位圆上,如图所示:

Root of Unity

ωn=e2πi/n\omega_n=e^{2\pi i/n} 称为nn 次单位根,所有其它 nn 次单位根都是 ωn\omega_n 的幂,所以第 kknn 次单位根恰好是 ωnk\omega_n^k

nnnn 次单位根 ωn0,ωn1,,ωnn1\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1} 在乘法意义下构成一个群,这个群和模 nn 意义下的整数加法群 (Zn,+)\left(\mathbb Z_n,+\right) 具有相同的结构,因为 ωnn=ωn0=1\omega_n^n=\omega_n^0=1 意味着 ωnjωnk=ωnj+k=ωn(j+k)modn\omega_n^j\omega_n^k=\omega_n^{j+k}=\omega_n^{(j+k)\bmod n} 。类似地, ωn1=ωnn1\omega_n^{-1}=\omega_n^{n-1}

多项式 A(x)A(x)ωn0,ωn1,,ωnn1\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1}nn 个点处的值

yk=A(ωnk)=j=0n1ajωnjk,0k<ny_k=A\left(\omega_n^k\right)=\sum_{j=0}^{n-1}a_j\omega_n^{jk},\quad 0\leqslant k<n

构成的向量 (y0,y1,,yn1)\left(y_0,y_1,\cdots,y_{n-1}\right) 称为系数向量 (a0,a1,,an1)\left(a_0,a_1,\cdots,a_{n-1}\right)离散傅里叶变换

快速傅里叶变换 (Fast Fourier Transform)

注意,尽管 A(x)A(x)B(x)B(x) 各只需要 nn 个点就能确定,但是 C(x)C(x) 的次数是 2n22n-2 ,它的点值表示至少需要 2n12n-1 个点。为了方便,我们选取 2n2n 个点,并且假设这些多项式都各具有 2n2n 个系数,其中 an=an+1==a2n1=0a_n=a_{n+1}=\cdots=a_{2n-1}=0 以及 bn=bn+1==b2n1=0b_n=b_{n+1}=\cdots=b_{2n-1}=0

考虑求出多项式 A(x)A(x)ω2n0,ω2n1,,ω2n2n1\omega_{2n}^0,\omega_{2n}^1,\cdots,\omega_{2n}^{2n-1}2n2n 个点处的值,即求出 (y0,y1,,y2n1)\left(y_0,y_1,\cdots,y_{2n-1}\right) ,其中

yk=A(ω2nk)=j=02n1ajω2nkj,0k<2n.y_k=A\left(\omega_{2n}^k\right)=\sum_{j=0}^{2n-1}a_j\omega_{2n}^{kj},\quad 0\leqslant k<2n.

如果直接计算,求一个 yky_k 需要 Θ(n)\Theta(n) 的时间,所以求出 (y0,y1,,y2n1)\left(y_0,y_1,\cdots,y_{2n-1}\right) 的时间复杂度为 Θ(n2)\Theta\left(n^2\right)

下面介绍一种快速算法,即快速傅里叶变换。对 0k<2n0\leqslant k<2n ,我们有

A(ω2nk)=j=02n1ajω2nkj=j=0n1a2jω2nk(2j)+j=0n1a2j+1ω2nk(2j+1)=j=0n1a2jω2n2kj+ω2nkj=0n1a2j+1ω2n2kj=j=0n1a2jωnkj+ω2nkj=0n1a2j+1ωnkj.\begin{aligned} A\left(\omega_{2n}^k\right) &= \sum_{j=0}^{2n-1}a_j\omega_{2n}^{kj}\\ &= \sum_{j=0}^{n-1}a_{2j}\omega_{2n}^{k\cdot(2j)}+\sum_{j=0}^{n-1}a_{2j+1}\omega_{2n}^{k(2j+1)}\\ &= \sum_{j=0}^{n-1}a_{2j}\omega_{2n}^{2kj}+\omega_{2n}^k\sum_{j=0}^{n-1}a_{2j+1}\omega_{2n}^{2kj}\\ &= \sum_{j=0}^{n-1}a_{2j}\omega_{n}^{kj}+\omega_{2n}^k\sum_{j=0}^{n-1}a_{2j+1}\omega_{n}^{kj}. \end{aligned}

这里的最后一步是因为 ω2n2kj=ωnkj\omega_{2n}^{2kj}=\omega_n^{kj} 。令 A0(x)A_0(x)A1(x)A_1(x) 为两个各有 nn 个系数的多项式

A0(x)=j=0n1a2jxj=a0+a2x+a4x2++a2n2xn1,A1(x)=j=0n1a2j+1xj=a1+a3x+a5x2++a2n1xn1,\begin{aligned} A_0(x) &= \sum_{j=0}^{n-1}a_{2j}x^j=a_0+a_2x+a_4x^2+\cdots+a_{2n-2}x^{n-1},\\ A_1(x) &= \sum_{j=0}^{n-1}a_{2j+1}x^j=a_1+a_3x+a_5x^2+\cdots+a_{2n-1}x^{n-1}, \end{aligned}

于是

A(ω2nk)=A0(ωnk)+ω2nkA1(ωnk),0k<n.(1)A\left(\omega_{2n}^k\right)=A_0\left(\omega_n^k\right)+\omega_{2n}^kA_1\left(\omega_n^k\right),\quad 0\leqslant k<n.\tag{1}

并且注意到 ω2nn+k=ω2nk\omega_{2n}^{n+k}=-\omega_{2n}^k ,有

A(ω2nn+k)=j=0n1a2jωn(n+k)j+ω2nn+kj=0n1a2j+1ωn(n+k)j=j=0n1a2jωnkjω2nkj=0n1a2j+1ωnkj=A0(ωnk)ω2nkA1(ωnk),0k<n.(2)\begin{aligned} A\left(\omega_{2n}^{n+k}\right) &= \sum_{j=0}^{n-1}a_{2j}\omega_n^{(n+k)j}+\omega_{2n}^{n+k}\sum_{j=0}^{n-1}a_{2j+1}\omega_n^{(n+k)j}\\ &= \sum_{j=0}^{n-1}a_{2j}\omega_n^{kj}-\omega_{2n}^k\sum_{j=0}^{n-1}a_{2j+1}\omega_n^{kj}\\ &= A_0\left(\omega_n^k\right)-\omega_{2n}^kA_1\left(\omega_n^k\right),\quad 0\leqslant k<n. \end{aligned}\tag{2}

也就是说,我们可以先计算 A0(ωn0),A0(ωn1),,A0(ωnn1)A_0\left(\omega_n^0\right),A_0\left(\omega_n^1\right),\cdots,A_0\left(\omega_n^{n-1}\right)A1(ωn0),A1(ωn1),,A1(ωnn1)A_1\left(\omega_n^0\right),A_1\left(\omega_n^1\right),\cdots,A_1\left(\omega_n^{n-1}\right) 这两个规模都只有原来的一半的子问题,然后利用 (1)(1)(2)(2) 式求出 A(ω2n0),A(ω2n1),,A(ω2n2n1)A\left(\omega_{2n}^0\right),A\left(\omega_{2n}^1\right),\cdots,A\left(\omega_{2n}^{2n-1}\right)

T(n)T(n) 为计算 A(ω2n0),A(ω2n1),,A(ω2n2n1)A\left(\omega_{2n}^0\right),A\left(\omega_{2n}^1\right),\cdots,A\left(\omega_{2n}^{2n-1}\right) 所需的时间,则我们有

T(n)=2T(n/2)+Θ(n),T(n)=2T(n/2)+\Theta(n),

所以 T(n)=Θ(nlogn)T(n)=\Theta\left(n\log n\right)

逆变换

现在我们已经能在 Θ(nlogn)\Theta(n\log n) 的时间里求出

A(ω2n0),A(ω2n1),,A(ω2n2n1)A\left(\omega_{2n}^0\right),A\left(\omega_{2n}^1\right),\cdots,A\left(\omega_{2n}^{2n-1}\right)

B(ω2n0),B(ω2n1),,B(ω2n2n1).B\left(\omega_{2n}^0\right),B\left(\omega_{2n}^1\right),\cdots,B\left(\omega_{2n}^{2n-1}\right).

我们可以轻易地在 Θ(n)\Theta(n) 的时间里求出 (ϕ0,ϕ1,,ϕ2n1)\left(\phi_0,\phi_1,\cdots,\phi_{2n-1}\right), 其中

ϕk=C(ω2nk)=A(ω2nk)B(ω2nk),0k<2n.\phi_k=C\left(\omega_{2n}^k\right)=A\left(\omega_{2n}^k\right)B\left(\omega_{2n}^k\right),\quad 0\leqslant k<2n.

现在的问题是,如何从 (ϕ0,ϕ1,,ϕ2n1)\left(\phi_0,\phi_1,\cdots,\phi_{2n-1}\right) 得到 C(x)C(x) 的系数表示?

注意到

ϕk=C(ω2nk)=j=02n1cjω2nkj.\phi_k=C\left(\omega_{2n}^k\right)=\sum_{j=0}^{2n-1}c_j\omega_{2n}^{kj}.

设多项式 Φ(x)=j=02n1ϕjxj\Phi(x)=\sum_{j=0}^{2n-1}\phi_jx^j 。我们发现

Φ(ω2nk)=j=02n1ϕjω2nkj=j=02n1t=02n1ctω2nj(tk)=t=02n1ctj=02n1ω2nj(tk).\begin{aligned} \Phi\left(\omega_{2n}^{-k}\right) &= \sum_{j=0}^{2n-1}\phi_j\omega_{2n}^{-kj}\\ &= \sum_{j=0}^{2n-1}\sum_{t=0}^{2n-1}c_t\omega_{2n}^{j(t-k)}\\ &= \sum_{t=0}^{2n-1}c_t\sum_{j=0}^{2n-1}\omega_{2n}^{j(t-k)}. \end{aligned}

这里 j=02n1ω2nj(tk)\sum_{j=0}^{2n-1}\omega_{2n}^{j(t-k)} 是等比数列求和,它的结果是

j=02n1ω2nj(tk)={1ω2n2n(tk)1ω2ntk=111ω2ntk=0,if ω2ntk1,2n,if ω2ntk=1.\sum_{j=0}^{2n-1}\omega_{2n}^{j(t-k)}=\begin{cases} \dfrac{1-\omega_{2n}^{2n(t-k)}}{1-\omega_{2n}^{t-k}}=\dfrac{1-1}{1-\omega_{2n}^{t-k}}=0,&\text{if }\omega_{2n}^{t-k}\neq 1,\\ 2n,&\text{if }\omega_{2n}^{t-k}=1. \end{cases}

由于这里 0k,t2n10\leqslant k,t\leqslant 2n-1 ,如果 ω2ntk=1\omega_{2n}^{t-k}=1 ,那么 tkt-k 必然为零,即 t=kt=k 。所以

Φ(ω2nk)=ck2n    ck=12nΦ(ω2nk),0k<2n.(3)\Phi\left(\omega_{2n}^{-k}\right)=c_k\cdot 2n\implies c_k=\frac{1}{2n}\Phi\left(\omega_{2n}^{-k}\right),\quad 0\leqslant k<2n.\tag{3}

因此,我们只需对 (ϕ0,ϕ1,,ϕ2n1)\left(\phi_0,\phi_1,\cdots,\phi_{2n-1}\right) 求一次离散傅里叶变换,就能根据 (3)(3) 得出 C(x)C(x) 的各项系数了。这里有两种可能的方式,你既可以使用

ck={12nΦ(ω2n2nk),0<k<2n,12nΦ(ω2n0),k=0,c_k=\begin{cases} \dfrac{1}{2n}\Phi\left(\omega_{2n}^{2n-k}\right),&0<k<2n,\\[1em] \dfrac{1}{2n}\Phi\left(\omega_{2n}^0\right),&k=0, \end{cases}

也可以在求 DFT 的过程中使用 ω2nk\omega_{2n}^{-k} 代替 ω2nk\omega_{2n}^k

实现

C++ 标准库 <complex> 中有一个复数模板 std::complex ,您可以在这里看到它的使用方式。您还可以使用 std::polar 来方便地求单位根。

C++ 标准库 <numbers> 中还有 std::numbers::pi 等数学常量,详见文档

您可以按照上述算法描述实现一个递归版本的 FFT ,也可以采用某些更精妙的非递归版的实现。您可以参考网络上的一些资料,但是您必须真正理解您提交的代码的每一个细节,不能只是从网上复制粘贴一份了事。