分治法(5):快速傅立叶变换(FFT)

Last updated on May 5, 2025 pm

本文介绍了多项式乘法的快速傅立叶变换算法,分为插值、乘法、恢复三个步骤,将乘法的时间复杂度降到O(nlogn)。

我们之前提到,要想将大整数乘法的时间复杂度降低到O(nlogn)\bm{O(n \log n)},可以采用快速傅立叶变换(Fast Fourier Transform,FFT)。这次,我们从一个类似的问题——多项式乘法入手,学习这个神奇的算法。

多项式乘法

基本问题

给定两个d1d - 1阶多项式p(x)p(x)q(x)q(x),我们要计算他们的乘积r(x)=p(x)q(x)r(x) = p(x)q(x)

每个d1d - 1阶多项式都可以表示为一个dd维向量。设

  • p(x)=i=0d1aixi(a0,a1,,ad1)p(x) = \sum_{i=0}^{d-1} a_i x^i \rightarrow (a_0, a_1, \ldots, a_{d - 1})
  • q(x)=i=0d1bixi(b0,b1,,bd1)q(x) = \sum_{i=0}^{d-1} b_i x^i \rightarrow (b_0, b_1, \ldots, b_{d - 1})

那么,我们就是要计算r(x)=i=02d2cixir(x) = \sum_{i=0}^{2d-2} c_i x^i的系数ci=k=0iakbikc_i = \sum_{k=0}^{i} a_k b_{i-k}

朴素算法的时间复杂度为O(d2)O(d^2),因为我们要计算系数的两两乘积再相加。所以,我们应该比O(d2)\bm{O(d^2)}做得更好

回想Karatsuba算法

如何改进?我们可以回想大整数乘法的Karatsuba算法。在这里,我们尝试类似的算法。

假设dd是2的整数次幂,我们先将p(x)p(x)写成

p(x)=p1(x)+p2(x)xd2p(x) = p_1(x) + p_2(x) \cdot x^{\frac{d}{2}}

其中

p1(x)=a0+a1x++ad21p_1(x) = a_0 + a_1 x + \cdots + a_{\frac{d}{2} - 1}

p2(x)=ad2+ad2+1x++ad1xd21p_2(x) = a_{\frac{d}{2}} + a_{\frac{d}{2} + 1} x + \cdots + a_{d - 1} x^{\frac{d}{2} - 1}

这里p1(x)p_1(x)相当于大整数乘法中的低位,p2(x)p_2(x)相当于大整数乘法中的高位。

类似地,我们将q(x)q(x)写成

q(x)=q1(x)+q2(x)xd2q(x) = q_1(x) + q_2(x) \cdot x^{\frac{d}{2}}

从而有

r=p1q1+(p1q2+p2q1)xd2+p2q2xdr = p_1 q_1 + (p_1 q_2 + p_2 q_1) x^{\frac{d}{2}} + p_2 q_2 x^d

让我们应用Karatsuba算法!我们要计算p1q1p_1 q_1p2q2p_2 q_2p1q2+p2q1p_1 q_2 + p_2 q_1,但并不需要计算四次乘法,因为p1q2+p2q1p_1 q_2 + p_2 q_1可以由下式得到。

p1q2+p2q1=(p1+p2)(q1+q2)p1q1p2q2p_1 q_2 + p_2 q_1 = (p_1 + p_2)(q_1 + q_2) - p_1 q_1 - p_2 q_2

这样,我们只需要计算三次乘法,即计算

  • p1q1p_1 q_1
  • p2q2p_2 q_2
  • (p1+p2)(q1+q2)(p_1 + p_2)(q_1 + q_2)

于是,我们把一个规模为dd的乘法问题,分解为了三个规模d2\dfrac{d}{2}的乘法问题,也就是

T(n)=3T(d2)+O(d)T(n)=O(nlog23)T(n) = 3 T\left(\frac{d}{2}\right) + O(d) \Rightarrow T(n) = O(n^{\log_2 3})

与大整数乘法的联系和区别

你一定发现了,多项式乘法和大整数乘法联系紧密。例如,

多项式乘法和大整数乘法的联系

可以看出,大整数乘法似乎就是x=10x=10的多项式乘法。但是,他们也有区别!

主要的区别是进位,多项式乘法中不存在大整数乘法中的进位,这会简化我们的问题。

事实上,我们今天使用多项式乘法来讲解FFT,主要就是为了避免进位问题。

插值定理

我们先做一些准备工作。为了表示一个多项式,我们可以它的系数表示,也可以用它曲线上的几个点表示。这就是多项式插值的想法。

那么,要几个不同的点才能确定一个多项式呢?我们从简单情况考虑,确定一条直线需要2个点,确定一条抛物线需要3个点。由此我们猜测,需要 d\bm{d} 个不同的点 (x0,p(x0)),(x1,p(x1)),,(xd1,p(xd1))(x_0, p(x_0)), (x_1, p(x_1)), \ldots, (x_{d - 1}, p(x_{d - 1})) 来唯一确定一个 d1\bm{d - 1} 维多项式

  • 插值定理:给定dd个点(x0,y0),(x1,y1),,(xd1,yd1)(x_0, y_0), (x_1, y_1), \ldots, (x_{d - 1}, y_{d - 1}),其中对任意iji \neq j都有xixjx_i \neq x_j,那么存在一个唯一的不超过d1d - 1阶的多项式p(x)p(x),使得对每个ii都有p(xi)=yip(x_i) = y_i

我们可以用线性代数的知识证明这个定理。设p(x)=t=0d1atxtp(x) = \sum_{t=0}^{d-1} a_t x^t,那么我们有yi=t=0d1atxity_i = \sum_{t=0}^{d-1} a_t x_i^t,即

[y0y1yd1]=[1x0x02x0d1x1x12x1d1xd1xd12xd1d][a0a1ad1]\begin{bmatrix} y_0 \\ y_1 \\ \vdots \\ y_{d-1} \end{bmatrix} = \begin{bmatrix} 1 & x_0 & x_0^2 & \cdots & x_0^{d} \\ 1 & x_1 & x_1^2 & \cdots & x_1^{d} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_{d-1} & x_{d-1}^2 & \cdots & x_{d-1}^{d} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{d-1} \end{bmatrix}

我们想要证明,满足等式的a=(a0,a1,,ad1)\bm{a} = (a_0, a_1, \ldots, a_{d - 1})是唯一的。

注意到,中间的方阵是一个范德蒙德矩阵,其行列式等于0i<jd1(xjxi)\prod_{0 \leq i < j \leq d-1} (x_j - x_i)。又因为xixjx_i \neq x_j,因此行列式不为零,即该矩阵可逆,从而满足等式的a\bm{a}有唯一解。

FFT的整体框架

FFT的核心思路就是我们刚才说的多项式插值,其算法主要分为以下三步:

  1. 插值:选取2d12d-1个不同的数x0,x1,,x2d2x_0, x_1, \ldots, x_{2d-2},计算

    • p(x0),p(x1),,p(x2d2)p(x_0), p(x_1), \ldots, p(x_{2d-2})
    • q(x0),q(x1),,q(x2d2)q(x_0), q(x_1), \ldots, q(x_{2d-2})
  2. 乘法:对于每个i=0,1,,2d2i = 0, 1, \ldots, 2d-2,计算 r(xi)=p(xi)q(xi)r(x_i) = p(x_i) q(x_i)。这其实获得了r(x)r(x)的插值(x0,r(x0)),(x1,r(x1)),,(x2d2,r(x2d2))(x_0, r(x_0)), (x_1, r(x_1)), \ldots, (x_{2d-2}, r(x_{2d-2}))

  3. 恢复:从前一步获得的插值中恢复出r(x)=i=02d2r(x) = \sum_{i=0}^{2d-2}的系数(c0,c1,,c2d2)(c_0, c_1, \ldots, c_{2d-2})

整个FFT的过程相当于“用插值绕了一圈”,主要的目的是将乘法的次数从 d2\bm{d^2} 次减少到 2d1\bm{2d-1} ,因为我们只需要完成2d12d-1个整数乘法。这样,我们期望能降低整体的时间复杂度。

FFT算法框架

步骤一:插值

要想在O(dlogd)O(d \log d)时间内做完多项式乘法,就要求每一步的时间复杂度不超过O(dlogd)O(d \log d)。我们先来看第一步——插值。

最简单的想法

最直接的做法是,随意找2d12d-1个点x0,x1,,x2d2x_0, x_1, \ldots, x_{2d-2},再分别算出p(x0),p(x1),,p(x2d2)p(x_0), p(x_1), \ldots, p(x_{2d-2})q(x0),q(x1),,q(x2d2)q(x_0), q(x_1), \ldots, q(x_{2d-2})

假设我们能在O(1)O(1)时间内算出xdx^d,那么算出每个p(xi)p(x_i)q(xi)q(x_i)需要O(d)O(d)的时间,而我们一共要算4d24d-2个。因此,总时间复杂度为O(d2)O(d^2)。这显然是不让人满意的,因为我们要求每一步都要在O(dlogd)O(d \log d)以内完成。

那么,我们可以用分治法做得更好吗?

引入分治法

我们首先做一些规定和假设。令D=2d1D = 2d-1,并假设DD是2的整数次幂(因为我们将会对DD分治)。假设我们能在O(1)O(1)的时间内算出xdx^d

回到正题,我们如何能加快插值的速度呢?目前的时间花销来源是,要计算O(d)O(d)p(xi)p(x_i)q(xi)q(x_i),而每个需要花费O(d)O(d)时间。所以,我们有两种优化思路:

  • 内部优化:用分治法更快地算出单个p(xi)p(x_i)
  • 协同优化:在算不同的p(xi)p(x_i)时分治

内部优化的尝试

首先,我们尝试内部优化的思路。为了用分治法更快算出p(xi)p(x_i),我们将p(xi)p(x_i)分成高位和低位,即

p(xi)=p1(xi)+p2(xi)xD2p(x_i) = p_1(x_i) + p_2(x_i) \cdot x^{\frac{D}{2}}

内部优化的尝试

然后,我们分别递归地求出p1(xi)p_1(x_i)p2(xi)p_2(x_i)

相信你已经意识到,这样做不可能降低时间复杂度。验证一下,我们有

T(D)=2T(D2)+O(1)T(D)=O(D)T(D) = 2T\left(\frac{D}{2}\right) + O(1) \Rightarrow T(D) = O(D)

这和直接计算没有区别,因为我们只是改变了计算顺序,并没有减少任何计算。事实上,将xix_i代入多项式求值的过程并没有什么优化空间。

协同优化的尝试

我们尝试在计算不同的p(xi)p(x_i)时使用分治。如果简单地将DDp(xi)p(x_i)分成两半,再递归地计算每一部分,这不会和之前有任何区别,因为不同的p(xi)p(x_i)之间毫无关系,我们无法减少任何计算。

协同优化的尝试

  • 不同的p(xi)p(x_i)之间没有关联,我们要单独算O(D)O(D)p(xi)p(x_i)
  • 单个p(xi)p(x_i)的各项之间没有关联,我们要单独算DD个单项式的值。

这么看,我们对这个O(d2)O(d^2)的算法根本无法优化!

单次求多值

让我们再仔细思考一下。计算每个p(xi)p(x_i)需要O(D)O(D)的时间,这确实无法优化,因为多项式的系数并不由我们决定,我们只能逐项代入计算。

但是计算不同的p(xi)p(x_i)呢?刚才我们说,不能优化是因为不同的 p(xi)\bm{p(x_i)} 之间没有关联。但其实,这DD个点只是要求出r(x)r(x)的系数,选择什么点完全取决于我们!

那么,能不能通过选取特殊的 x0,x1,,xD1\bm{x_0, x_1, \ldots, x_{D-1}} 来减少计算呢?例如,选取x1=1x_1 = 1x2=1x_2 = -1,我们能不能同时计算p(x1)p(x_1)p(x2)p(x_2)

奇偶分治

x1=1x_1 = 1x2=1x_2 = -1为例,我们观察到

  • p(1)=a0+a1+a2+a3+p(1) = a_0 + a_1 + a_2 + a_3 + \cdots
  • p(1)=a0a1+a2a3+p(-1) = a_0 - a_1 + a_2 - a_3 + \cdots

这么看,我们可以分开计算奇数项和偶数项,再分别相加和相减,就能同时得到p(xi)p(x_i)p(xi)p(-x_i)!也就是说,我们用dd次运算解决了原来2d2d次的计算!

更一般地,我们有如下奇偶分治的思路:设

p(x)=pe(x2)+xpo(x2)p(x) = p_e(x^2) + x \cdot p_o(x^2)

其中

pe(x)=a0+a2x+a4x2++aD2xD22p_e(x) = a_0 + a_2 x + a_4 x^2 + \cdots + a_{D-2} x^{\frac{D-2}{2}}

po(x)=a1+a3x+a5x2++aD1xD22p_o(x) = a_1 + a_3 x + a_5 x^2 + \cdots + a_{D-1} x^{\frac{D-2}{2}}

我们选择互为相反数x1x_1x2x_2,那么有

pe(x12)=pe(x22)andpo(x12)=po(x22)p_e(x_1^2) = p_e(x_2^2) \quad \mathrm{and} \quad p_o(x_1^2) = p_o(x_2^2)

进而有

p(x1)=pe(x12)+x1po(x12)p(x_1) = p_e(x_1^2) + x_1 \cdot p_o(x_1^2)

p(x2)=pe(x22)+x2po(x22)p(x_2) = p_e(x_2^2) + x_2 \cdot p_o(x_2^2)

原本我们计算p(x1)p(x_1)p(x2)p(x_2),需要计算pe(x12),po(x12),pe(x22),po(x22)p_e(x_1^2), p_o(x_1^2), p_e(x_2^2), p_o(x_2^2)这4项,但现在,我们只需要计算2项。也就是说,我们原先将2个规模为DD的问题,分解为4个规模D2\dfrac{D}{2}的问题,而现在,我们只需要分解为2个规模D2\dfrac{D}{2}的问题!

分治法思路

这样,我们就设计出了一种基本的分治算法。

  1. 选择 x0,x1,,xD1x_0, x_1, \ldots, x_{D-1},使得 x0=x1,x2=x3,,xD2=xD1x_0 = -x_1, x_2 = -x_3, \ldots, x_{D-2} = -x_{D-1}

  2. 划分:分别计算奇部分和偶部分。

    • pe(x02),pe(x22),,pe(xD22)p_e(x_0^2), p_e(x_2^2), \ldots, p_e(x_{D-2}^2)

    • po(x02),po(x22),,po(xD22)p_o(x_0^2), p_o(x_2^2), \ldots, p_o(x_{D-2}^2)

  3. 合并:计算 p(xi)=pe(xi2)+xipo(xi2)p(x_i) = p_e(x_i^2) + x_i \cdot p_o(x_i^2)

分治法设计思路示意图

这样,我们每次就将计算DDDDp(x)p(x)的问题,转换为了计算DDD2\dfrac{D}{2}p(x)p(x)的问题,即分解为了2个“计算D2\dfrac{D}{2}D2\dfrac{D}{2}p(x)p(x)的问题”。如果T(D)T(D)表示计算DDDD阶的p(x)p(x)的时间代价,那么我们有

T(D)=2T(D2)+O(D)T(D)=O(DlogD)T(D) = 2T\left(\frac{D}{2}\right) + O(D) \Rightarrow T(D) = O(D \log D)

这似乎非常巧妙。但这个算法中存在着很大的问题

引入复数

分治法中的问题

请注意,我们的算法是否能够递归调用。在第一轮中,我们取了互为相反数的整数,如x0=x1,x2=x3x_0 = -x_1, x_2 = -x_3等。但在第二次调用中,要代入的整数变为了x02,x22,x42,x_0^2, x_2^2, x_4^2, \ldots,全部是正数,无法再次采用相反数的性质进行递归

分治法中的问题示意图

因此,为了让我们的递归进行下去,就要保证每次要代入的整数都两两互为相反数。而想要让x02=x22x_0^2 = -x_2^2,我们必须引入复数

复数基础复习

复数有三种基本的表示形式

  1. 直角坐标:z=a+biz = a + bi,其中aa为实部,bb为虚部,i=1i = \sqrt{-1}为虚数单位。

  2. 极坐标:z=r(cosθ+isinθ)z = r(\cos \theta + i \sin \theta),其中rr是向量(a,b)(a, b)的长度,θ\theta是向量(a,b)(a, b)与实轴的夹角。

  3. 指数形式:z=reθi=r(cosθ+isinθ)z = r \cdot e^{\theta i} = r(\cos \theta + i \sin \theta),由欧拉公式可得。

接下来,我们都采用指数形式表示复数,并且默认r=1r = 1,因为我们在FFT中只会用到r=1r = 1(即单位圆上)的复数。

复数的相反数和平方运算示意图

  1. 复数的平方:eθie^{\theta i}的平方是e2θie^{2 \theta i},只需要把eθie^{\theta i}旋转角度θ\theta

  2. 复数的相反数:互为相反数的复数关于原点中心对称(图中两个红色向量),并且平方相等。

(e(θ+π)i)2=e2θie2πi=e2θi=(eθi)2\left(e^{(\theta + \pi)i}\right)^2 = e^{2 \theta i} \cdot e^{2 \pi i} = e ^{2 \theta i} = \left(e^{\theta i}\right)^2

  1. 复数的平方根:eθie^{\theta i}的平方根是eθ2ie^{\frac{\theta}{2} i}e(θ2+π)ie^{(\frac{\theta}{2} + \pi) i}

用复数实现递归

有了复数,我们如何解决刚才的问题?也就是,如何让平方后的数还两两互为相反数

我们先考虑原先有4个数的情况,即怎么样取这4个数,才能让他们平方后得到的2个数仍然是相反数?不难想到,可以取单位圆与坐标轴的四个交点(或者说从实轴正方向起取单位圆的四等分点),即

x0=1,x1=1,x2=i,x3=ix_0 = 1, \quad x_1 = -1, \quad x_2 = i, \quad x_3 = -i

这样平方后,有

x02=1,x22=1x_0^2 = 1, \quad x_2^2 = -1

仍为相反数。

类似地,考虑 D=8D=8 的情况,我们取从实轴正方向起取单位圆的八等分点,即

ω0=1,ω1=eπ4i,ω2=eπ2i,ω3=e3π4i\omega_0 = 1, \quad \omega_1 = e^{\frac{\pi}{4}i}, \quad \omega_2 = e^{\frac{\pi}{2}i}, \quad \omega_3 = e^{\frac{3\pi}{4}i}

ω4=eπi,ω5=e5π4i,ω6=e3π2i,ω7=e7π4i\omega_4 = e^{\pi i}, \quad \omega_5 = e^{\frac{5\pi}{4}i}, \quad \omega_6 = e^{\frac{3\pi}{2}i}, \quad \omega_7 = e^{\frac{7\pi}{4}i}

D=8时起始点选取及递归中的点的变化示意图

可以验证,这8个数平方后得到的4个数两两互为相反数,这4个数平方后得到的2个数仍两两互为相反数。这样,我们分治法中的递归就可以进行了

D=8情况下的递归过程示意图

那么一般地,对于任意的DD,我们如何找到起始的DD个点呢?

和之前一样,从实轴正方向起取单位圆的 D\bm{D} 等分点。设 ω=e2πDi\omega = e^{\frac{2\pi}{D}i},那么我们就取ω0,ω1,ω2,,ωD1\omega^0, \omega^1, \omega^2, \ldots, \omega^{D-1}。可以看到我们只需要一个参数 ω\bm{\omega} 来表示 D\bm{D} 个点

那么在下一阶段,要计算的点就变为了 ω0,ω2,ω4,,ωD2\omega^0, \omega^2, \omega^4, \ldots, \omega^{D-2}。也就是说,我们可以用 ω2\omega^2 来表示下一阶段的所有点。

插值整体算法

至此,我们得到了第一步——插值的整体算法。

给定一个D1D-1阶多项式 ppω=e2πDi\omega = e^{\frac{2\pi}{D}i}

  • 基本情况:如果 ω=1\omega = 1,此时问题规模D=1D = 1,直接返回 p(1)p(1)

  • p(x)p(x),构造偶多项式 po(x)p_o(x) 和奇多项式 pe(x)p_e(x)

  • po(x)p_o(x)pe(x)p_e(x),分别在点 ω0,ω2,,ωD2\omega^0, \omega^2, \ldots, \omega^{D-2} 上递归计算。

  • 依次合并 DD 个点的运算结果,即

p(ωt)=pe(ω2t)+ωtpo(ω2t)p(\omega^t) = p_e(\omega^{2t}) + \omega^t \cdot p_o(\omega^{2t})

  • 返回 DD 个点的计算结果。

T(D)T(D) 表示计算 FFT(pp, ω\omega) 的时间复杂度,其中 pp 的阶数为 D1D - 1。之前已经分析过,我们有

T(D)=2T(D2)+O(D)T(D)=O(DlogD)T(D) = 2T\left(\frac{D}{2}\right) + O(D) \Rightarrow T(D) = O(D \log D)

所以,插值这一步的时间复杂度 T(D)=O(dlogd)T(D) = \bm{O(d \log d)}

插值算法设计

步骤二:乘法

在这一步中,我们要对每个 xi(0i2d2)x_i \,(0 \le i \le 2d-2),计算 r(xi)=p(xi)q(xi)r(x_i) = p(x_i) q(x_i)

当我们完成了插值,这一步是简单的,我们只需要做 2d12d-1 次数值乘法即可,故乘法这一步的时间复杂度为 O(d)\bm{O(d)}

步骤三:恢复

第三步,我们需要从 2d12d-1(xi,r(xi))(x_i, r(x_i)) 中,恢复出 r(x)r(x) 的系数 (c0,c1,,c2d1)(c_0, c_1, \ldots, c_{2d-1})

最简单的想法

恢复其实是插值的逆过程,本质上是要解如下的方程

[r(1)r(ω)r(ω2)r(ωD1)]=[11111ωω2ωD11ω2ω4ω2(D1)1ωD1ω2(D1)ω(D1)(D1)][c0c1c2cD1]\begin{bmatrix} r(1) \\ r(\omega) \\ r(\omega^2) \\ \vdots \\ r(\omega^{D-1}) \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega & \omega^2 & \cdots & \omega^{D-1} \\ 1 & \omega^2 & \omega^4 & \cdots & \omega^{2(D-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{D-1} & \omega^{2(D-1)} & \cdots & \omega^{(D-1)(D-1)} \end{bmatrix} \begin{bmatrix} c_0 \\ c_1 \\ c_2 \\ \vdots \\ c_{D-1} \end{bmatrix}

我们已知 (1,r(1)),(ω,r(ω)),(ω2,r(ω2)),,(ωD1,r(ωD1))(1, r(1)), (\omega, r(\omega)), (\omega^2, r(\omega^2)), \ldots, (\omega^{D-1}, r(\omega^{D-1})),其中 ω=e2πDi\omega = e^{\frac{2\pi}{D}i},现在要求系数 (c0,c1,,cD)(c_0, c_1, \ldots, c_D)

A=[11111ωω2ωD11ω2ω4ω2(D1)1ωD1ω2(D1)ω(D1)(D1)]A = \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega & \omega^2 & \cdots & \omega^{D-1} \\ 1 & \omega^2 & \omega^4 & \cdots & \omega^{2(D-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{D-1} & \omega^{2(D-1)} & \cdots & \omega^{(D-1)(D-1)} \end{bmatrix}

那么,最简单的想法是

[c0c1c2cD1]=A1[r(1)r(ω)r(ω2)r(ωD1)]\begin{bmatrix} c_0 \\ c_1 \\ c_2 \\ \vdots \\ c_{D-1} \end{bmatrix} = A^{-1} \begin{bmatrix} r(1) \\ r(\omega) \\ r(\omega^2) \\ \vdots \\ r(\omega^{D-1}) \end{bmatrix}

即直接解矩阵方程,或者说求 A1A^{-1}

但是,这种做法的时间复杂度是 O(D3)O(D^3),这是我们不可接受的。

因此,我们需要利用 AA 矩阵的特殊性质,来更快地解出方程。

复矩阵基础复习

  1. 复数的共轭:z=a+biz = a + bi 的共轭复数是 zˉ=abi\bar{z} = a - bi

  2. 复向量的内积:两个复向量 a,bCn\bm{a}, \bm{b} \in \mathbb{C}^n,它们的内积定义为

<a,b>=j=1najbj\left<\bm{a}, \bm{b}\right> = \sum_{j=1}^{n} \overline{a_j} \cdot b_j

  1. 复向量的正交:如果 <a,b>=0\left<\bm{a}, \bm{b}\right> = 0,那么 a\bm{a}b\bm{b} 是正交的。

  2. 复向量的标准正交:如果 <a,b>=0\left<\bm{a}, \bm{b}\right> = 0,且 <a,a>=<b,b>=1\left<\bm{a}, \bm{a}\right> = \left<\bm{b}, \bm{b}\right> = 1,那么 a\bm{a}b\bm{b} 是标准正交的。

  3. 酉矩阵(标准正交复矩阵):如果复方阵 AA 的任意两列都标准正交,那么 AA 是酉矩阵(标准正交复矩阵)。

  4. 复矩阵的共轭转置:复矩阵 AA 的共轭转置 AA^*,定义为 (A)i,j=Aj,i(A^*)_{i, j} = \overline{A_{j, i}}

举个例子,比如

A=(acbd)A=(abcd)A = \begin{pmatrix} a & c \\ b & d \end{pmatrix} \quad A^* = \begin{pmatrix} \overline{a} & \overline{b} \\ \overline{c} & \overline{d} \end{pmatrix}

  1. 酉矩阵的重要性质:如果 AA 是酉矩阵,那么 AA 可逆,且 A1=A\bm{A^{-1} = A^*}

接着上面的例子,如果 AA 是酉矩阵,那么

AA=(acbd)(abcd)=(1001)A A^* = \begin{pmatrix} a & c \\ b & d \end{pmatrix} \begin{pmatrix} \overline{a} & \overline{b} \\ \overline{c} & \overline{d} \end{pmatrix} = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}

因此我们发现,如果 AA 是一个酉矩阵,那么求它的逆矩阵会非常方便。

A矩阵的性质

那么我们会想去验证,恢复步骤中的 A(ω)\bm{A(\omega)} 矩阵是一个酉矩阵吗

还记得,

A=[11111ωω2ωD11ω2ω4ω2(D1)1ωD1ω2(D1)ω(D1)(D1)]A = \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega & \omega^2 & \cdots & \omega^{D-1} \\ 1 & \omega^2 & \omega^4 & \cdots & \omega^{2(D-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{D-1} & \omega^{2(D-1)} & \cdots & \omega^{(D-1)(D-1)} \end{bmatrix}

注意到,ωω=1\overline{\omega} \omega = 1ωD=e2πi=1\omega^D = e^{2\pi i} = 1

首先验证正交性。挑选两列,

ci=(1,ω,ω2,,ωD1)\bm{c_i} = (1, \omega, \omega^2, \ldots, \omega^{D-1})

cj=(1,ω2,ω4,,ω2(D1))\bm{c_j} = (1, \omega^2, \omega^4, \ldots, \omega^{2(D-1)})

那么有

<ci,cj>=1+ω+ω2++ωD1=0\left<\bm{c_i}, \bm{c_j}\right> = 1 + \omega + \omega^2 + \cdots + \omega^{D-1} = 0

一般地,对于任意不同的两列 ci,cj\bm{c_i}, \bm{c_j}

ci,cj=k=1Dω(k1)(i1)ω(k1)(j1)=k=1Dω(k1)(ji)=1ω(ji)D1ωji=0\begin{aligned} \langle \bm{c}_i, \bm{c}_j \rangle &= \sum_{k=1}^{D} \overline{\omega^{(k-1)(i-1)}} \, \omega^{(k-1)(j-1)} \\ &= \sum_{k=1}^{D} \omega^{(k-1)(j-i)} = \frac{1 - \omega^{(j-i)D}}{1 - \omega^{j-i}} = 0 \end{aligned}

再验证标准性。但是,AA 矩阵明显不满足标准性(即每一列和自己的内积为1),因为第1列为全1。

事实上,对任意一列 ci\bm{c_i},我们有

ci,ci=k=1Dω(k1)(i1)ω(k1)(i1)=D\langle \bm{c_i}, \bm{c_i} \rangle = \sum_{k=1}^{D} \overline{\omega^{(k-1)(i-1)}} \, \omega^{(k-1)(i-1)} = D

因此,AA 矩阵并不是酉矩阵!但是,我们可以通过缩放来使得它标准正交

我们已经计算过,A(ω)A(\omega) 矩阵不同的两列内积为0,相同的两列内积为DD。可以猜测,1DA(ω)\bm{\dfrac{1}{\sqrt{D}}A(\omega)} 应该是标准正交的!以下是对于这个命题的证明,可以跳过。

命题:矩阵1DA(ω)\dfrac{1}{\sqrt{D}} A(\omega) 对于 ω=e2πiD\omega = e^{\frac{2\pi i}{D}} 是标准正交的

证明:设 ci,cj\bm{c}_i, \bm{c}_j1DA(ω)\dfrac{1}{\sqrt{D}} A(\omega) 的任意两列,那么有

ci,cj=k=1D1Dω(k1)(i1)ω(k1)(j1)=1Dk=1Dω(k1)(ji)\langle \bm{c}_i, \bm{c}_j \rangle = \sum_{k=1}^D \frac{1}{D} \overline{\omega^{(k-1)(i-1)}} \omega^{(k-1)(j-1)} = \frac{1}{D} \sum_{k=1}^D \omega^{(k-1)(j-i)}

  • 如果 i=ji = j,那么有

ci,cj=1Dk=1Dω0=1\langle \bm{c}_i, \bm{c}_j \rangle = \frac{1}{D} \sum_{k=1}^D \omega^0 = 1

  • 如果 iji \neq j,那么有

ci,cj=1Dk=1Dω(k1)(ji)=1D1ω(ji)D1ωji=0\langle \bm{c}_i, \bm{c}_j \rangle = \frac{1}{D} \sum_{k=1}^D \omega^{(k-1)(j-i)} = \frac{1}{D} \frac{1 - \omega^{(j-i)D}}{1 - \omega^{j-i}} = 0

因此, 1DA(ω)\dfrac{1}{\sqrt{D}} A(\omega) 是标准正交的。

接下来,我们要求 A(ω)1\bm{A(\omega)^{-1}}。简单来说,由于

A(ω)A(ω)=DIA(\omega) A(\omega)^* = D I

因此

A(ω)1=1DA(ω)=1DA(ω1)A(\omega)^{-1} = \frac{1}{D} \cdot A(\omega)^{*} = \frac{1}{D} \cdot A(\omega^{-1})

由于 AA 对称,转置后不变。又 ω=ω1\overline{\omega} = \omega^{-1},故 AA 的共轭转置 A(ω)=A(ω1)A(\omega)^* = A(\omega^{-1})

当然,我们还可以严谨地证明,此部分可以跳过。

A(ω)1=(D1DA(ω))1=D(1DA(ω))1=D(1DA(ω))=1DA(ω)\begin{aligned}A(\omega)^{-1} =& \left(\sqrt{D} \cdot \frac{1}{\sqrt{D}} \cdot A(\omega) \right)^{-1} = \sqrt{D} \left(\frac{1}{\sqrt{D}} \cdot A(\omega) \right)^{-1} \\=& \sqrt{D} \left(\frac{1}{\sqrt{D}} \cdot A(\omega) \right)^* = \frac{1}{D} A(\omega)^*\end{aligned}

因此,

(A(ω)1)i,j=1D(A(ω)1)j,i=1Dω(i1)(j1)=1D(ω1)(i1)(j1)(A(\omega)^{-1})_{i, j} = \frac{1}{D} \overline{(A(\omega)^{-1})_{j, i}} = \frac{1}{D} \cdot \omega^{-(i-1)(j-1)} = \frac{1}{D}(\omega^{-1})^{(i-1)(j-1)}

也就是说,

A(ω)1=1DA(ω1)A(\omega)^{-1} = \frac{1}{D} \cdot A(\omega^{-1})

因此,

A1=1D[11111ω1ω2ω(D1)1ω2ω4ω2(D1)1ω(D1)ω2(D1)ω(D1)(D1)]A^{-1} = \frac{1}{D} \cdot \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega^{-1} & \omega^{-2} & \cdots & \omega^{-(D-1)} \\ 1 & \omega^{-2} & \omega^{-4} & \cdots & \omega^{-2(D-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{-(D-1)} & \omega^{-2(D-1)} & \cdots & \omega^{-(D-1)(D-1)} \end{bmatrix}

恢复整体算法

现在,我们就得到了

[c0c1c2cD1]=1D[11111ω1ω2ω(D1)1ω2ω4ω2(D1)1ω(D1)ω2(D1)ω(D1)(D1)][r(1)r(ω)r(ω2)r(ωD1)]\begin{bmatrix} c_0 \\ c_1 \\ c_2 \\ \vdots \\ c_{D-1} \end{bmatrix} = \frac{1}{D} \cdot \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega^{-1} & \omega^{-2} & \cdots & \omega^{-(D-1)} \\ 1 & \omega^{-2} & \omega^{-4} & \cdots & \omega^{-2(D-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{-(D-1)} & \omega^{-2(D-1)} & \cdots & \omega^{-(D-1)(D-1)} \end{bmatrix} \begin{bmatrix} r(1) \\ r(\omega) \\ r(\omega^2) \\ \vdots \\ r(\omega^{D-1}) \end{bmatrix}

然而,直接计算这个矩阵乘法仍然需要 O(D2)O(D^2) 的时间,我们还是不能接受。

该如何改进呢?我们发现,现在要做的事情和第一步很类似。我们都是在求一个多项式在 D\bm{D} 个点处的值,只不过现在的多项式是

s(x)=r(1)+r(ω)x+r(ω2)x2++r(ωD1)xD1s(x) = r(1) + r(\omega) \cdot x + r(\omega^2) \cdot x^2 + \cdots + r(\omega^{D - 1}) \cdot x^{D - 1}

而现在的 DD 个数分别是 ω0,ω1,ω2,,ω(D1)\omega^0, \omega^{-1}, \omega^{-2}, \ldots, \omega^{-(D-1)}

这可以使用第一步的分治法吗?

现在和第一步的区别是:在第一步中,我们选择了 DD 个很好的数 ω0,ω1,,ωD1\omega^0, \omega^1, \ldots, \omega^{D-1},而在第三步中,我们无法选择数字,现在的数字就是 ω0,ω1,ω2,,ω(D1)\omega^0, \omega^{-1}, \omega^{-2}, \ldots, \omega^{-(D-1)}

那么,这 DD 个数是否也很好呢?也就是说,它们是否有平方后两两互为相反数的良好性质,使得我们的递归可以很好进行下去呢?

直观上看,是有的。因为我们在第一步在单位圆上取 DD 等分点,采取了顺时针方向;而这次,采用的是逆时针方向,当然能取到完全相同的点。也就是说,(ω0,ω1,,ωD1)(\omega^0, \omega^1, \ldots, \omega^{D-1})(ω0,ω1,ω2,,ω(D1))(\omega^0, \omega^{-1}, \omega^{-2}, \ldots, \omega^{-(D-1)}) 只有旋转的方向不同。

所以,我们可以直接调用第一步中的函数 FFT(s,ω1)(s, \omega^{-1})!从而,恢复步骤的时间复杂度也为 O(dlogd)\bm{O(d \log d)}

整体算法与时间复杂度分析

至此,快速傅立叶变换的三个步骤已经全部设计完成!

让我们把它们组装起来:

FFT完整算法设计

  • 插值(第1~4步):计算 DDω\omega,再计算出 ωi\omega^i 对应的函数值 p(ωi)p(\omega^i)q(ωi)q(\omega^i)
  • 乘法(第5步):对每个点,计算 r(ωi)=p(ωi)q(ωi)r(\omega^i) = p(\omega^i)q(\omega^i)
  • 恢复(第6~9步):利用 r(ωi)r(\omega_i) 结合 FFT 函数,解出 r(x)r(x) 的系数。

总的时间复杂度为:

O(dlogd)+O(d)+O(dlogd)=O(dlogd)O(d \log d) + O(d) + O(d \log d) = \bm{O(d \log d)}

注:本文中所有图片均来自张宇昊老师的课程PPT。


分治法(5):快速傅立叶变换(FFT)
https://cny123222.github.io/2025/03/11/分治法-5-:快速傅立叶变换-FFT/
Author
Nuoyan Chen
Posted on
March 11, 2025
Licensed under