用 FFT 计算多项式乘法
给定两个多项式
A(x)B(x)=k=0∑n−1akxk,=k=0∑m−1bkxk,
的系数 (a0,a1,⋯,an−1) 和 (b0,b1,⋯,bm−1) ,我们希望求出多项式
C(x)=A(x)B(x)=k=0∑n+m−2ckxk
的各项系数
ck=i+j=k∑aibj,k∈{0,1,⋯,n+m−2}.
首先,为了简化问题,我们假定 m=n ,并且 n 是 2 的正整数次幂。实际情况中,这两点都可以通过给后面补上若干个 0 来完成。
点值表示法 (point-value representation)
通过一个系数向量 (a0,a1,⋯,an−1) 来表示一个多项式的方式称为系数表示法。系数表示法让我们可以很容易地在 O(n) 的时间里求出多项式在某一点处的值,但是用它来计算多项式的乘积是比较麻烦的。
注意到,给定平面上的 n 个点 (x0,y0),(x1,y1),⋯,(xn−1,yn−1) ,其中所有 xi 都不相等,这 n 个点可以唯一确定一个不超过 n−1 次的多项式,比如 2 个点可以确定一条直线, 3 个不共线的点可以确定一条抛物线。因此我们还有一种表示多项式 A(x) 的方式:
(x0,A(x0)),(x1,A(x1)),⋯,(xn−1,A(xn−1)),i=j⟹xi=xj.
这种表示方式称为点值表示法。给定两个多项式 A(x) 和 B(x) 的点值表示,我们可以很容易地在 O(n) 的时间内求出它们的乘积 C(x) 的点值表示,因为 C(xk)=A(xk)B(xk) 。
所以我们的思路就是:给定 A(x) 和 B(x) 的系数表示,先快速地求出它们的点值表示,然后花 O(n) 的时间算出它们的乘积 C(x) 的点值表示,再快速地求出 C(x) 的系数表示。
要想快速地求出 A(x) 和 B(x) 的点值表示,点 x0,x1,⋯ 的选取至关重要。
从现在开始,我们用 i 表示虚数单位。
首先引入单位根 (root of unity) 的概念。一个 n 次单位根指的是一个复数 ω ,满足
ωn=1.
这个方程在复数域恰好有 n 个解,所以 n 次单位根恰好有 n 个,其中第 k 个 (0⩽k<n) 是 e2πik/n 。我们可以用著名的欧拉公式 eix=cosx+isinx 来理解它:
e2πik/n=cosn2πk+isinn2πk.
它的主辐角恰好为 2π⋅nk ,所以 n 个单位根均匀地分布在以复平面的原点为圆心的单位圆上,如图所示:
值 ωn=e2πi/n 称为主 n 次单位根,所有其它 n 次单位根都是 ωn 的幂,所以第 k 个 n 次单位根恰好是 ωnk 。
n 个 n 次单位根 ωn0,ωn1,⋯,ωnn−1 在乘法意义下构成一个群,这个群和模 n 意义下的整数加法群 (Zn,+) 具有相同的结构,因为 ωnn=ωn0=1 意味着 ωnjωnk=ωnj+k=ωn(j+k)modn 。类似地, ωn−1=ωnn−1 。
多项式 A(x) 在 ωn0,ωn1,⋯,ωnn−1 这 n 个点处的值
yk=A(ωnk)=j=0∑n−1ajωnjk,0⩽k<n
构成的向量 (y0,y1,⋯,yn−1) 称为系数向量 (a0,a1,⋯,an−1) 的离散傅里叶变换。
注意,尽管 A(x) 和 B(x) 各只需要 n 个点就能确定,但是 C(x) 的次数是 2n−2 ,它的点值表示至少需要 2n−1 个点。为了方便,我们选取 2n 个点,并且假设这些多项式都各具有 2n 个系数,其中 an=an+1=⋯=a2n−1=0 以及 bn=bn+1=⋯=b2n−1=0 。
考虑求出多项式 A(x) 在 ω2n0,ω2n1,⋯,ω2n2n−1 这 2n 个点处的值,即求出 (y0,y1,⋯,y2n−1) ,其中
yk=A(ω2nk)=j=0∑2n−1ajω2nkj,0⩽k<2n.
如果直接计算,求一个 yk 需要 Θ(n) 的时间,所以求出 (y0,y1,⋯,y2n−1) 的时间复杂度为 Θ(n2) 。
下面介绍一种快速算法,即快速傅里叶变换。对 0⩽k<2n ,我们有
A(ω2nk)=j=0∑2n−1ajω2nkj=j=0∑n−1a2jω2nk⋅(2j)+j=0∑n−1a2j+1ω2nk(2j+1)=j=0∑n−1a2jω2n2kj+ω2nkj=0∑n−1a2j+1ω2n2kj=j=0∑n−1a2jωnkj+ω2nkj=0∑n−1a2j+1ωnkj.
这里的最后一步是因为 ω2n2kj=ωnkj 。令 A0(x) 和 A1(x) 为两个各有 n 个系数的多项式
A0(x)A1(x)=j=0∑n−1a2jxj=a0+a2x+a4x2+⋯+a2n−2xn−1,=j=0∑n−1a2j+1xj=a1+a3x+a5x2+⋯+a2n−1xn−1,
于是
A(ω2nk)=A0(ωnk)+ω2nkA1(ωnk),0⩽k<n.(1)
并且注意到 ω2nn+k=−ω2nk ,有
A(ω2nn+k)=j=0∑n−1a2jωn(n+k)j+ω2nn+kj=0∑n−1a2j+1ωn(n+k)j=j=0∑n−1a2jωnkj−ω2nkj=0∑n−1a2j+1ωnkj=A0(ωnk)−ω2nkA1(ωnk),0⩽k<n.(2)
也就是说,我们可以先计算 A0(ωn0),A0(ωn1),⋯,A0(ωnn−1) 及 A1(ωn0),A1(ωn1),⋯,A1(ωnn−1) 这两个规模都只有原来的一半的子问题,然后利用 (1) 和 (2) 式求出 A(ω2n0),A(ω2n1),⋯,A(ω2n2n−1) 。
设 T(n) 为计算 A(ω2n0),A(ω2n1),⋯,A(ω2n2n−1) 所需的时间,则我们有
T(n)=2T(n/2)+Θ(n),
所以 T(n)=Θ(nlogn) 。
逆变换
现在我们已经能在 Θ(nlogn) 的时间里求出
A(ω2n0),A(ω2n1),⋯,A(ω2n2n−1)
和
B(ω2n0),B(ω2n1),⋯,B(ω2n2n−1).
我们可以轻易地在 Θ(n) 的时间里求出 (ϕ0,ϕ1,⋯,ϕ2n−1), 其中
ϕk=C(ω2nk)=A(ω2nk)B(ω2nk),0⩽k<2n.
现在的问题是,如何从 (ϕ0,ϕ1,⋯,ϕ2n−1) 得到 C(x) 的系数表示?
注意到
ϕk=C(ω2nk)=j=0∑2n−1cjω2nkj.
设多项式 Φ(x)=∑j=02n−1ϕjxj 。我们发现
Φ(ω2n−k)=j=0∑2n−1ϕjω2n−kj=j=0∑2n−1t=0∑2n−1ctω2nj(t−k)=t=0∑2n−1ctj=0∑2n−1ω2nj(t−k).
这里 ∑j=02n−1ω2nj(t−k) 是等比数列求和,它的结果是
j=0∑2n−1ω2nj(t−k)=⎩⎨⎧1−ω2nt−k1−ω2n2n(t−k)=1−ω2nt−k1−1=0,2n,if ω2nt−k=1,if ω2nt−k=1.
由于这里 0⩽k,t⩽2n−1 ,如果 ω2nt−k=1 ,那么 t−k 必然为零,即 t=k 。所以
Φ(ω2n−k)=ck⋅2n⟹ck=2n1Φ(ω2n−k),0⩽k<2n.(3)
因此,我们只需对 (ϕ0,ϕ1,⋯,ϕ2n−1) 求一次离散傅里叶变换,就能根据 (3) 得出 C(x) 的各项系数了。这里有两种可能的方式,你既可以使用
ck=⎩⎨⎧2n1Φ(ω2n2n−k),2n1Φ(ω2n0),0<k<2n,k=0,
也可以在求 DFT 的过程中使用 ω2n−k 代替 ω2nk 。
实现
C++ 标准库 <complex>
中有一个复数模板 std::complex
,您可以在这里看到它的使用方式。您还可以使用 std::polar
来方便地求单位根。
C++ 标准库 <numbers>
中还有 std::numbers::pi
等数学常量,详见文档。
您可以按照上述算法描述实现一个递归版本的 FFT ,也可以采用某些更精妙的非递归版的实现。您可以参考网络上的一些资料,但是您必须真正理解您提交的代码的每一个细节,不能只是从网上复制粘贴一份了事。