Caiwen的博客

算法分析-分治

2026-03-16 11:59

1. 渐近符号

  • OO: "Big Oh",f=O(g)f = O(g) 表示, c>0\exists \space c > 0,使得  n\forall \space nf(n)cg(n)f(n) \le c\cdot g(n)。相当于 fgf\le g
  • oo:"Little Oh",f=o(g)f=o(g) 表示,limnf(n)g(n)=0\lim_{n\rightarrow\infty}\frac{f(n)}{g(n)} = 0,相当于 f<gf < g
  • Ω\Omega:"Big Omega",f=Ω(g)f=\Omega (g) 表示 g=O(f)g = O(f),相当于 fgf\ge g
  • ω\omega:"Little Omega",f=ω(g)f=\omega(g) 表示 f=o(f)f = o(f),相当于 f>gf>g
  • Θ\Theta:"Theta",f=Θ(g)f = \Theta(g),表示 f=O(g)f = O(g)f=Ω(g)f=\Omega(g),相当于 f=gf = g

OO 还可以使用 f(n)g(n)\frac{f(n)}{g(n)} 的 lim sup 定义。lim sup 直观的感受是,当趋近于正无穷的时候,函数值的上界会越来越小(如果存在 lim sup 的话),最后这个上界收敛到的值就是该函数的 lim sup。f(n)g(n)\frac{f(n)}{g(n)} 存在 lim sup 的话,就有 f=O(g)f = O(g) 的关系。

不过排除掉震荡函数和其他什么邪门的函数,如果我们直接算 limnf(n)g(n)\lim_{n\rightarrow\infty}\frac{f(n)}{g(n)} 发现是存在极限的,那么必然可以得到 f=O(g)f = O(g)(注意我们把极限等于 00 的情况也包含进去了,因为对于 ff,满足 oo 关系的函数 gg 必然也能满足 OO

2. 主定理

对于 T(n)=aT(nb)+f(n)T(n) = a\cdot T(\frac{n}{b}) + f(n),其中 aabb 均为常数,我们有如下定理:

  • 情况一,如果存在 ϵ>0\epsilon > 0,满足 f(n)=O(n(logba)ϵ)f(n) = O(n^{(\log_ba) - \epsilon}),则有 T(n)=Θ(nlogba)T(n) = \Theta(n^{\log_b a})
  • 情况二,如果存在 k0k \ge 0,满足 f(n)=Θ(nlogbalogkn)f(n) = \Theta(n^{\log_b a}\log^kn),则有 T(n)=Θ(nlogbalogk+1n)T(n)=\Theta(n^{\log_ba}\log^{k+1}n)
  • 情况三,如果存在 ϵ>0\epsilon > 0,满足 f(n)=Ω(n(logba)+ϵ)f(n) = \Omega(n^{(\log_ba)+\epsilon}),同时满足正则条件,即存在 c<1c < 1,满足当 nn 足够大时, af(nb)cf(n)a\cdot f(\frac{n}{b})\le c \cdot f(n),则有 T(n)=Θ(f(n))T(n)=\Theta(f(n))

分治算法的时间复杂度一般使用递归树来分析。如果某个式子不满足主定理,那么可以考虑从递归树上进行推导。

同样地,主定理也可以使用递归树来理解。递归树的时间复杂度贡献来源有两种,一种是叶子节点的贡献,一种是非叶子节点的贡献。

对于叶子节点的贡献,递归树每层往下都分 aa 个叉,一共是 logbn\log_bn 层。叶子节点的时间复杂度贡献只有 Θ(1)\Theta(1),所以全部叶子节点的贡献为 Θ(alogbn)=Θ(nlogba)\Theta(a^{\log_bn}) = \Theta(n^{\log_ba})

对于非叶子节点的贡献,稍微准确一点地来算应该是:

j=0logbn1ajf(nbj)\sum_{j=0}^{\log_bn - 1}a^jf(\frac{n}{b^j})

这大概意义上来说,有:(这里不保对,仅供用于后续理解)

aj+1f(nbj+1)ajf(nbj)=af(nbjb)f(nbj)af(nb)f(n)=r\frac{a^{j+1}\cdot f(\frac{n}{b^{j+1}})}{a^j\cdot f(\frac{n}{b^j})} = a\cdot \frac{f(\frac{n}{b^j\cdot b})}{f(\frac{n}{b^j})} \approx a\cdot \frac{f(\frac{n}{b})}{f(n)} = r

然后非叶子节点的贡献就是 pf(n)p\cdot f(n)p=i=0logbn1rip = \sum_{i=0}^{\log_bn-1} r^i

然后对于情况一,f(n)=O(nlogba)f(n)=O(n^{\log_ba}),此时 f(n)f(n) 都比较小了,pp 往往不能把 f(n)f(n) 放大太多,所以非叶节点的贡献小于叶子节点的贡献,最终的时间复杂度就由叶子节点决定,为 Θ(nlogba)\Theta(n^{\log_ba})

对于情况二,如果 f(n)=Θ(nlogbalogkn)f(n) = \Theta(n^{\log_b a}\log^kn),说明 f(n)f(n) 和叶子节点的贡献差不多。同时我们有如下观察:

af(nb)=Θ(a(nb)logbalogk(nb))=Θ(anlogbaalogk(nb))=Θ(nlogbalogkn)=f(n)\begin{align} a\cdot f(\frac{n}{b}) &= \Theta(a\cdot (\frac{n}{b})^{\log_ba}\log^k(\frac{n}{b})) \\ &= \Theta(a\cdot \frac{n^{\log_ba}}{a}\log^k(\frac{n}{b})) \\ &= \Theta(n^{\log_ba}\log^kn) \\ &= f(n) \end{align}

所以每一层的贡献都是一样的。总共有 Θ(logn)\Theta(\log n) 层,因此总时间复杂度为 T(n)=Θ(nlogbalogk+1n)T(n)=\Theta(n^{\log_ba}\log^{k+1}n)

对于情况三,如果 f(n)=Ω(nlogba)f(n) = \Omega(n^{\log_ba}),说明单个 f(n)f(n) 就比叶子节点的贡献大了。理论上总时间复杂度应该是 Θ(pf(n))\Theta(p\cdot f(n)) 的。主定理又加了一个限制,要求 af(nb)cf(n)a\cdot f(\frac{n}{b})\le c \cdot f(n),也就是 af(nb)f(n)=rc<1a\cdot \frac{f(\frac{n}{b})}{f(n)} = r \le c < 1,这就使得 pp 是对一个公比小于 11 的等比数列求和,求和结果是收敛的,所以总时间复杂度是 Θ(f(n))\Theta(f(n)) 。不过我认为,实际上这个限制只是用来卡掉一些特殊情况,比如不太连续的那种邪门函数。对于一般的函数这个条件是自然满足的,毕竟如果不收敛的话,一致发散到最后一层叶子节点,很容易使得叶子节点的贡献大于 f(n)f(n),从而使得情况三的前提就不成立了。

然后谈一下情况一和情况三中的 ϵ\epsilon。我的理解是,这是为了让 OOΩ\OmegaΘ\Theta 不重叠。但是主定理没选择用 ooω\omega 来限制,而是使用 ϵ\epsilon,这是因为 ooω\omega 只是说和 Θ\Theta 有差距即可,有什么差距不做限制,而主定理的 ϵ\epsilon 要求必须是差多项式因子的差距。

比如,如果 f(n)=nlogbalognf(n) = \frac{n^{\log_ba}}{\log n},那么 limnnlogbalognn(logba)ϵ=limnnϵlogn\lim_{n\rightarrow\infty}\frac{\frac{n^{\log_ba}}{\log n}}{n^{(\log_ba)-\epsilon}}=\lim_{n\rightarrow\infty}\frac{n^\epsilon}{\log n},我们发现根本找不到一个 ϵ>0\epsilon > 0 使得这个极限存在。

3. 算法

3.1 Karatsuba 算法

主要用于高精度乘法。

现在有两个长度为 nn 的大数 XXYY,将其分别拆分成两个部分,得到:

X=xhigh10n2+xlowY=yhigh10n2+ylowX=x_{high}\cdot 10^\frac{n}{2} + x_{low} \\ Y = y_{high}\cdot 10^\frac{n}{2} + y_{low}

于是:

XY=(xhigh10n2+xlow)(yhigh10n2+ylow)=xhighyhigh10n+(xhighylow+xlowyhigh)10n2+xlowylow\begin{align} XY &= (x_{high}\cdot 10^\frac{n}{2} + x_{low})(y_{high}\cdot 10^\frac{n}{2} + y_{low}) \\ &= x_{high}y_{high}\cdot 10^n+(x_{high}y_{low}+x_{low}y_{high})\cdot 10^\frac{n}{2}+x_{low}y_{low} \end{align}

然后注意到:

(xhigh+xlow)(yhigh+ylow)=xhighyhigh+(xhighylow+xlowyhigh)+xlowylow\begin{align} &(x_{high}+x_{low})(y_{high}+y_{low})\\ &= x_{high}y_{high}+(x_{high}y_{low}+x_{low}y_{high})+x_{low}y_{low} \end{align}

xhighylow+xlowyhigh=(xhigh+xlow)(yhigh+ylow)xhighyhighxlowylow\begin{align} & x_{high}y_{low}+x_{low}y_{high} \\ & = (x_{high}+x_{low})(y_{high}+y_{low}) - x_{high}y_{high} - x_{low}y_{low} \end{align}

所以我们只需要计算 (xhigh+xlow)(yhigh+ylow)(x_{high}+x_{low})(y_{high}+y_{low})xhighyhighx_{high}y_{high}xlowylowx_{low}y_{low} 这三部分即可。于是有 T(n)=3T(n2)+Θ(n)T(n) = 3T(\frac{n}{2}) + \Theta (n),得到时间复杂度为 Θ(nlog23)Θ(n1.585)\Theta (n^{\log_23}) \approx \Theta(n^{1.585})

在 Python 中,乘法使用 Θ(n2)\Theta(n^2) 的小学乘法算法。但是,如果运算符两边的操作数的位数均大于 KARATSUBA_CUTOFF,那么就使用 Karatsuba 算法。KARATSUBA_CUTOFF 被定义成 7070

3.2 Fibonacci 数列

定义 Fibonacci 数列的递归公式为:

{F0=0F1=1(nN)Fn=Fn1+Fn2\begin{cases} F_0 = 0 & \\ F_1 = 1 & (n\in N) \\ F_n = F_{n-1} + F_{n-2} & \end{cases}

直接递归

T(n)=T(n1)+T(n2)+Θ(1)T(n) = T(n-1) + T(n-2) + \Theta(1)

可以先通过放缩来得到上界和下界。令 c=Θ(1)c = \Theta(1)

T(n)=T(n1)+T(n2)+c2T(n1)+c4T(n2)+3c23T(n3)+7c...2nT(0)+(2n1)cT(n)=O(2n)\begin{align} T(n) &= T(n-1) + T(n-2) + c \\ &\le 2T(n-1) + c \\ &\le 4T(n-2) + 3c \\ &\le 2^3\cdot T(n-3) + 7c \\ &... \\ &\le 2^n\cdot T(0) + (2^n - 1)c \\ T(n) & = O(2^n) \end{align}

T(n)=T(n1)+T(n2)+c2T(n2)+c4T(n4)+3c23T(n6)+7c...2n2T(0)+(2n21)cT(n)=Ω(2n2)=Ω((2)n)\begin{align} T(n) &= T(n-1) + T(n-2) + c \\ &\ge 2T(n-2) + c \\ &\ge 4T(n-4) + 3c \\ &\ge 2^3\cdot T(n-6) + 7c \\ &... \\ &\ge 2^{\frac{n}{2}} \cdot T(0) + (2^{\frac{n}{2}} - 1)c \\ T(n) &= \Omega(2^{\frac{n}{2}}) = \Omega((\sqrt{2})^n) \end{align}

使用特征方程可以得到紧界。上式的齐次部分为:

T(n)T(n1)T(n2)=0T(n) - T(n-1) - T(n-2) = 0

,特称方程为:

r2r1=0r^2 - r - 1 = 0

得到两根:φ1=1+52\varphi_1 = \frac{1+\sqrt{5}}{2}φ2=152\varphi_2=\frac{1-\sqrt{5}}{2},齐次通解为:

Aφ1n+Bφ2nA\cdot \varphi_1^n + B\cdot \varphi_2^n

由于非齐次项为常数,所以非齐次特解也为常数。于是非齐次通解为:

T(n)=Aφ1n+Bφ2n+Θ(1)T(n) = A\cdot \varphi_1^n + B\cdot \varphi_2^n + \Theta(1)

注意到,φ2<1\left | \varphi_2 \right | < 1,因此,当 nn \rightarrow \infty 时,Aφ1nA\cdot \varphi_1^n 主导,Bφ2n0B\cdot \varphi_2^n \rightarrow 0,即 T(n)=Θ(φ1n)T(n) = \Theta(\varphi_1^n)

时间复杂度的式子和递归的式子高度一致。因此 Fn=Θ(φ1n)F_n = \Theta(\varphi_1^n)FnF_n 的位数为 log10Fn=Θ(n)\log_{10} F_n = \Theta(n)

这里我们假设了加法操作是常数的时间。但实际上加法操作是 Θ(n)\Theta(n) 的。不过即使考虑了加法操作的时间,由于指数的增长速度很快,最后的时间复杂度还是指数的。

递推

时间复杂度显然是 Θ(n)\Theta(n) 的。

考虑加法操作的耗时,该算法的总耗时是 O(n2)O(n^2) 的。

矩阵加速

时间复杂度为 Θ(logn)\Theta(\log n)

但是这中间需要进行矩阵乘法,进行元素相乘。两个 nn 位数相乘,只使用 Θ(n2)\Theta(n^2) 的小学乘法算法的话,最后得到的总耗时是 O(n2logn)O(n^2\log n) 的,看起来还不如递推。

不过我们还可以再精确地计算。矩阵加速时,会计算 A1A^1A2A^2A4A^4A8A^8......,那么矩阵中数字的位数大概是 11224488......这样,然后进行 O(n2)O(n^2) 的乘法运算,所需时间大概为 12+22+42+82++n21^2+2^2+4^2+8^2+\dots+n^2,中间有 Θ(logn)\Theta(\log n) 项。最后的结果大概是 n2n^2 这一项主导,得到总耗时为 O(n2)O(n^2)

实际上,如果两数相乘的时间复杂度如果为 M(n)M(n) 的话,那么总耗时就是 M(n)M(n)

3.3 BFPRT

BFPRT 用于在长度为 nn 的无序数组中找出前 kk 大的数,即 Top k 问题。

朴素的做法是直接排序,时间复杂度是 Ω(nlogn)\Omega(n\log n) 的。

使用堆可以把时间复杂度做到 O(n+klogn)O(n+k\log n),其中 O(n)O(n) 的建堆时间,之后每次从堆中取出一个数需要 O(logn)O(\log n) 的时间,总共进行 kk 次。但是,如果 kk 比较靠近中间,比如 k=n2k=\frac{n}{2},时间复杂度就又变成了 O(nlogn)O(n\log n)

快速选择算法基于快速排序的思想。首先选取一个主元,然后把主元两边的元素交换,将序列随机分成三个部分:小于主元的部分、等于主元的部分(即主元本身)和大于主元的部分,此时主元本身是排好序的了,那么我们可以根据主元的位置来判断接下来递归左右哪个部分。

当主元选取的比较好(恰好是中位数时),每次递归都会排除掉一半的序列,时间复杂度为 O(n)O(n)。但是如果选取的不好(比如选到最值或者接近最值),那么每次只会排除掉几个,时间复杂度为 O(n2)O(n^2)。随机选取可以做到期望的时间复杂度为 O(n)O(n)

BFPRT 算法,也被称为 Median of medians 算法,能够最坏情况下的时间复杂度仍为 O(n)O(n)

首先这个算法将序列从左到右,每 55 个元素分成一组,共得到 n5\frac{n}{5} 组。每组分别找到该组内 55 个元素之间的中位数,这样共产生 n5\frac{n}{5} 个中位数。然后,我们对这 n5\frac{n}{5} 个中位数跑一遍当前算法(当前算法就是求 top k,令 k=n2k=\frac{n}{2} 就是求中位数),即可得到中位数之间的中位数。然后选择这个中位数作为主元,后续的过程就跟快速选择算法一样了。

取中位数的中位数作为主元,可以确保这个主元的选择不会太差。如上图,如果 aa 指向 bb,说明一定存在 aba\le b 的关系。没连边的节点之间的大小关系不确定。每个竖行表示一组。白色节点是该组内的中位数。节点 xx 表示是中位数的中位数,也是我们最后要作为主元的节点。

然后我们看到,所有能到达 xx 的点都是必然小于等于 xx 的,这些点位于图中的阴影区域。这个区域内的点的数量大概为 12n53=3n10\frac{1}{2}\cdot \frac{n}{5}\cdot 3=\frac{3n}{10},也就是至少有 30% 的节点是小于等于 xx 的。

所有 xx 能到达的点也都是必然大于等于 xx 的,同理,大概至少有 30% 的节点是大于等于 xx 的。

那么我们选到的这个中位数的中位数,每次递归怎么也能排掉 30% 的序列。

于是我们有:

T(n)T(n5)+T(7n10)+Θ(n)T(n) \le T(\frac{n}{5}) + T(\frac{7n}{10}) + \Theta(n)

其中 T(n5)T(\frac{n}{5}) 是对中位数找到中位数的时间,T(7n10)T(\frac{7n}{10}) 是继续往下递归的时间。

这个形式无法使用主定理,我们只能去猜测这个时间复杂度并带入验证。

假设 T(n)cnT(n)\le cn,带入上式,有:

T(n)T(n5)+T(7n10)+Θ(n)cn5+7cn10+an=9cn10+an=cn+(cn10+an)\begin{align} T(n) & \le T(\frac{n}{5}) + T(\frac{7n}{10}) + \Theta(n) \\ & \le \frac{cn}{5}+\frac{7cn}{10}+an \\ &= \frac{9cn}{10}+an \\ &= cn + (-\frac{cn}{10}+an) \end{align}

只要 cn10+an0-\frac{cn}{10}+an\le 0,即可使得 T(n)cnT(n)\le cn 的假设成立。那么也就是说 c10ac \ge 10a。对于任意常数 aa,我们都能满足,于是假设成立,因此 BFPRT 的时间复杂度为 O(n)O(n),且是最坏情况下的时间复杂度。

为什么选择 5 个一组

选择其他的数字其实也没多大的问题。不过首先最好是要选择奇数,因为偶数的中位数计算比较麻烦,容易造成偏差。假如我们选择 33 个,同样的推导过程,有:

T(n)=T(n3)+T(2n3)+O(n)cn3+2cn3+an=cn+an\begin{align} T(n) &= T(\frac{n}{3}) + T(\frac{2n}{3}) + O(n) \\ &\le \frac{cn}{3} + \frac{2cn}{3} + an \\ &= cn + an \end{align}

我们发现 anan 是非负的,因此 T(n)cnT(n) \le cn 的假设不成立。三个一组不太可行。

实际上只要是大于等于五个一组的都可以,但是五个一组的常数会小一点,所以一般我们选择五个一组。

为什么不一直分组找中位数

既然我们通过了五个一组划分,然后找到了组内的中位数,那么看起来还可以再对这些中位数再进行五个一组这样的操作,这么一直进行下去,毕竟再递归 BFPRT 算法本身去找中位数,其内部还是会进行这么个过程。

实际上分组划分找中位数得到的只是近似中位数,递归调用 BFPRT 找到的是精确的中位数。如果不断地分组划分的话,最后找到的中位数会是反复近似多次的结果,很可能无法保证后续时间复杂度的正确性了。

3.4 平面最近点对

我们充分发扬人类智慧:

将所有点全部绕原点旋转同一个角度,然后按 xx 坐标排序

根据数学直觉,在随机旋转后,答案中的两个点在数组中肯定不会离得太远

所以我们只取每个点向后的 55 个点来计算答案

这样速度快得飞起,在 n=1000000n=1000000 时都可以在 1s 内卡过

当然,这是错的。

我们充分发扬人类智慧:

将所有点全部绕原点旋转同一个角度,然后按 x×yx\times y 排序

根据数学直觉,在随机旋转后,答案中的两个点在数组中肯定不会离得太远

所以我们只取每个点向前的 5050 个点来计算答案

这样速度快得飞起,在 n=400000n=400000 时都可以在 124ms 内卡过

先把所有点按照 xix_i 为第一关键字,yiy_i 为第二关键字排序,并以点 pmp_mm=n2m=\left \lfloor \frac{n}{2} \right \rfloor)把点集分成两个部分,然后对两个部分分别递归下去,求出点集各自内部的最近点对,设距离为 h1h_1h2h_2,取其中的最小值为 hh

然后再考虑跨过两个点集之间的点的最近距离。首先距离大于 hh 的肯定不用考虑了,于是我们把所有横坐标与 xmx_m 的差小于 hh 的点放入集合 BB 中,即图中绿色的点。

然后为了避免两个点之间相互考虑,对于点 pip_i,只考虑纵坐标大于等于 yiy_i 的点,也就是对于图中的点 PP ,只考虑红色区域内的 BB 的点,即下图中红色的点,这相当于一个剪枝。

(下图中,中间的黑色粗线是左右两部分的分割线。红色部分区域由两个 h×hh\times h 的正方形组成)

image.png

然后直接在 BB 集合里面暴力枚举就好了。只要在 yy 坐标上进行如上这种剪枝,把遍历范围限制住(而不是直接在整个 BB 里枚举),时间复杂度就是对的。

为什么对?上图中左半部分的 h×hh\times h 区域里面的点,已经在分治时处理了,所以点之间两两距离必然是 h\ge h 的。(是同一正方形部分内的点之间,不是横跨两部分正方形的点之间)

我们把 h×hh\times h 的正方形再分成 44h2×h2\frac{h}{2} \times \frac{h}{2} 的小正方形,一个小正方形内最多只有一个点,因为小正方形内的最大距离为对角线,即 2h2<h\frac{\sqrt{2}h}{2}<h

于是,对于点 PP 最多只会枚举到 2×42\times 4 个点。

在具体实现上,为了做上面的剪枝,需要再把集合 BB 中的点按 yy 坐标排序,于是 T(n)=2T(n2)+O(nlogn)T(n) = 2\cdot T(\frac{n}{2}) + O(n\log n),时间复杂度为 O(nlog2n)O(n\log^2n)

可以使用归并排序。在分治找最小距离的同时再把坐标按 yy 排序(两部分合并之后,最开始 xx 的排序关系就没用了,再变成 yy 的排序是没问题的),这样就是 T(n)=2T(n2)+O(n)T(n)=2\cdot T(\frac{n}{2})+O(n),时间复杂度为 O(n)O(n)

3.5 棋盘覆盖问题

有一个 2k×2k2^k\times 2^k 的棋盘,其中有一个方格已经被覆盖了,然后要求你用下图三种东西去把剩余的没被覆盖的方格覆盖掉。

做法是把棋盘横着和竖着分成四部分,然后在没有方格覆盖的三个部分上应用上图的图形,这样搞完之后分成的四个部分都是只有一个方格被覆盖了,化成了规模更小的子问题。

3.6 循环赛日程表

有如下的设计规则:

  • 每位选手必须与其他 n1n-1 位选手各比赛 11 次。
  • 每位选手每天只能比赛 11 次。
  • 循环赛一共进行 n1n-1 天。

这个问题可以用分治解决,对于 2n2n 个选手,可以先把所有他们分成两部分,每部分 nn 个人。在前 n1n-1 天,两个部分同时先自己内部解决,然后后 nn 天,两部分之间再相互比赛。

根据这个思想,可以构造出一个日程表:

然后我们发现这个表是有点规律的。实际写代码的时候根据这个规律去生成方案。

3.7 FFT

对于多项式 f(x)=i=0naixi,g(x)=i=0nbixif(x)=\sum_{i=0}^{n}a_ix^i,g(x)=\sum_{i=0}^{n}b_ix^i,定义其乘积 fgfg 为:

(fg)(x)=(i=0naixi)(i=0nbixi)(fg)(x)=\left(\sum_{i=0}^{n}a_ix^i\right)\left(\sum_{i=0}^{n}b_ix^i\right)

朴素做法是 O(n2)O(n^2) 的时间复杂度。使用 FFT 可以做到 O(nlogn)O(n\log n) 的时间复杂度。

对于 nn 次多项式,可以用 n+1n+1 个系数来唯一表示,即系数表示。还可以用 n+1n+1 个多项式上不同的点来表示(插值),即点值表示,正确性可以用范德蒙行列式证明。

点值表示下的多项式可以快速地计算多项式乘积,只需要把相同 xx 下的 yy 值相乘即可得到多项式相乘后,新的多项式在 xx 下的 yy 值。

FFT 求多项式乘积的过程如下:

  • ff 通过快速傅里叶变换,从系数表示转为点值表示。通过取特殊的点,该过程可以做到 O(nlogn)O(n\log n)

  • gg 也按上述的方法从系数表示转为点值表示。

  • 然后可以 O(n)O(n) 的时间计算出 fgfg 的点值表示。

  • 再通过对 fgfg 进行快速傅里叶逆变换,从点值表示转为系数表示,该过程耗时 O(nlogn)O(n\log n)

单位根

在复平面上画一单位圆,从 xx 轴正方向开始逆时针在单位圆上取点,将单位圆平均分成 nn 部分,这 nn 个点即为 nn 个复数。设幅角为正且最小的复数为 ωn\omega_n,称为 nn 次单位根,即:

ωn=cos2πn+isin2πn\omega_{n}=\cos \frac{2\pi}{n}+{\rm i}\sin \frac{2\pi}{n}

单位根有如下性质:

  • 由欧拉公式可得:

    ωnk=cos2kπn+isin2kπn\omega_{n}^{k}=\cos \frac{2k\pi}{n}+{\rm i}\sin \frac{2k\pi}{n}\\

  • ωn0=ωnn=1\omega_n^0=\omega_n^n=1

  • ωrnrk=ωnk\omega_{rn}^{rk} = \omega_n^k

  • ωnk+n2=ωnk\omega _{n}^{k+\frac{n}{2}} = -\omega _{n}^{k}

  • ωnk=ωnnk\overline{\omega_n^k} = \omega_n^{n-k}

快速傅里叶变换

任何多项式都可以化成 nn 次多项式 f(x)f(x),其中 nn22 的幂次(只需要在原来的多项式后面加系数为 00 的高次项即可)。

然后按下标的奇偶性,把多项式中的项分成两部分:

f(x)= (a0+a2x2+a4x4++an2xn2)+(a1x+a3x3+a5x5++an1xn1)\begin{align}f(x)= \space &(a_0+a_2{x^2}+a_4{x^4}+\dots+a_{n-2}x^{n-2})\\&+(a_1x+a_3{x^3}+a_5{x^5}+ \dots+a_{n-1}x^{n-1})\end{align}\\

f1(x)=a0+a2x1+a4x2++an2xn21f2(x)=a1+a3x+a5x2++an1xn21\begin{align} f_1(x)&=a_0+a_2{x^1}+a_4{x^2}+\dots+a_{n-2}x^{\frac{n}{2}-1}\\ f_2(x)&=a_1+a_3{x}+a_5{x^2}+ \dots+a_{n-1}x^{\frac{n}{2}-1} \end{align}\\

f(x)=f1(x2)+xf2(x2)f(x)=f_1(x^2)+xf_2(x^2)\\

现在我们想快速地求出 f(wni)f(w_n^i)i=0n1i=0\dots n-1)来得到多项式对应的点值表示。我们把要求的这个分成两部分:ωnk\omega_n^kωnk+n2\omega_n^{k+\frac{n}{2}}k<n2k<\frac{n}{2})。

带入 x=ωnkx=\omega_n^kk<n2k<\frac{n}{2})可得

f(ωnk)=f1(ωn2k)+ωnkf2(ωn2k)=f1(ωn2k)+ωnkf2(ωn2k)(1)\begin{align}f(\omega_n^k)&=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{align}\tag1

带入 x=ωnk+n2x=\omega_n^{k+\frac{n}{2}}k<n2k<\frac{n}{2})可得

f(ωnk+n2)=f1(ωn2k+n)+ωnk+n2f2(ωn2k+n)=f1(ωn2kωnn)ωnkf2(ωn2kωnn)=f1(ωn2k)ωnkf2(ωn2k)=f1(ωn2k)ωnkf2(ωn2k)(2)\begin{align}f(\omega_n^{k+\frac{n}{2}})&=f_1(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}f_2(\omega_n^{2k+n})\\ &=f_1(\omega_n^{2k}\cdot\omega_n^n)-\omega_n^kf_2(\omega_n^{2k}\cdot\omega_n^n)\\&=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{align}\tag2

于是我们只需要求出 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}}),相当于只需要求解两个规模砍半的子问题,T(n)=2T(n2)+O(n)T(n)=2\cdot T(\frac{n}{2})+O(n),得到时间复杂度为 O(nlogn)O(n\log n)

快速傅里叶逆变换

傅里叶变换的过程可以用矩阵表示:

[11111(ωn1)1(ωn1)2(ωn1)n11(ωn2)1(ωn2)2(ωn2)n11(ωnn1)1(ωnn1)2(ωnn1)n1][a0a1a2an1]=[y0y1y2yn1]\begin{bmatrix}1 & 1 & 1 & \cdots & 1 \\1 & (\omega_n^1)^1 & (\omega_n^1)^2 & \cdots & (\omega_n^1) ^ {n-1}\\1 & (\omega_n^2)^1 & (\omega_n^2)^2 & \cdots & (\omega_n^2) ^ {n-1} \\ \vdots & \vdots &\vdots & \ddots & \vdots \\ 1 & (\omega_n^{n-1})^1 & (\omega_n^{n-1})^2 & \cdots & (\omega_n^{n-1}) ^ {n-1}\end{bmatrix} \begin{bmatrix}a_0 \\ a _ 1 \\ a _ 2 \\ \vdots \\ a _ {n-1}\\ \end{bmatrix} = \begin{bmatrix}y_0 \\ y _ 1 \\ y _ 2 \\ \vdots \\ y _ {n-1}\\ \end{bmatrix}\quad\\

然后我们看这个公式:

xn1=(x1)(xn1+xn2++1)x^n-1=(x-1)(x^{n-1}+x^{n-2}+\cdots+1)\\

  • x=1x=1 时,xn1+xn2++1=nx^{n-1}+x^{n-2}+\cdots+1 = n
  • x1x\neq1 时, xn1+xn2++1=xn1x1x^{n-1}+x^{n-2}+\cdots+1 = \frac{x^n-1}{x-1}

然后我们有:

[1(ωnni)1(ωnni)2(ωnni)n1][1 (ωnj)1(ωnj)2(ωnj)n1]=k=0n1(ωnni)k(ωnj)k=k=0n1(ωnni+j)k\begin{align}\begin{bmatrix}1&(\omega_n^{n-i})^1&({\omega_n^{n-i}})^2&\cdots&(\omega_{n}^{n-i})^{n-1}\end{bmatrix}\begin{bmatrix}1\\\ (\omega_n^{j})^1\\(\omega_n^{j})^2\\\vdots\\(\omega_n^{j})^{n-1}\end{bmatrix}&=\sum_{k=0}^{n-1}(\omega_n^{n-i})^k(\omega_n^j)^k\\ &=\sum_{k=0}^{n-1}(\omega_n^{n-i+j})^k \end{align}

套用上面的公式:

  • ωnni+j=1\omega_n^{n-i+j} = 1,即 i=ji=j 时,上式为 nn
  • ωnni+j1\omega_n^{n-i+j} \neq 1,即 iji\neq j 时,(ωnni+j)n=ωnn(nij)=ω1nij=1(\omega_n^{n-i+j})^n = \omega_n^{n(n-i-j)}=\omega_1^{n-i-j}=1,于是上式为 00

这就表明

C=1n[11111(ωnn1)1(ωnn1)2(ωnn1)n11(ωnn2)1(ωnn2)2(ωnn2)n11(ωn1)1(ωn1)2(ωn1)n1]\mathbf C=\frac1n\begin{bmatrix}1&1&1&\cdots&1\\ 1&(\omega_n^{n-1})^1&({\omega_n^{n-1}})^2&\cdots&(\omega_{n}^{n-1})^{n-1}\\ 1&(\omega_n^{n-2})^1&({\omega_n^{n-2}})^2&\cdots&(\omega_{n}^{n-2})^{n-1}\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ 1&(\omega_n^1)^1&{(\omega_n^1)}^2&\cdots&(\omega_{n}^1)^{n-1}\end{bmatrix}\\

[11111(ωn1)1(ωn1)2(ωn1)n11(ωn2)1(ωn2)2(ωn2)n11(ωnn1)1(ωnn1)2(ωnn1)n1]\begin{bmatrix}1 & 1 & 1 & \cdots & 1 \\1 & (\omega_n^1)^1 & (\omega_n^1)^2 & \cdots & (\omega_n^1) ^ {n-1}\\1 & (\omega_n^2)^1 & (\omega_n^2)^2 & \cdots & (\omega_n^2) ^ {n-1} \\ \vdots & \vdots &\vdots & \ddots & \vdots \\ 1 & (\omega_n^{n-1})^1 & (\omega_n^{n-1})^2 & \cdots & (\omega_n^{n-1}) ^ {n-1}\end{bmatrix}\\

的逆矩阵。

根据单位根的性质:

C=1n[11111(ωnn1)1(ωnn1)2(ωnn1)n11(ωnn2)1(ωnn2)2(ωnn2)n11(ωn1)1(ωn1)2(ωn1)n1]=1n[11111(ωn1)1(ωn1)2(ωn1)n11(ωn2)1(ωn2)2(ωn2)n11(ωnn1)1(ωnn1)2(ωnn1)n1]\begin{align} \mathbf C&=\frac1n\begin{bmatrix}1&1&1&\cdots&1\\ 1&(\omega_n^{n-1})^1&({\omega_n^{n-1}})^2&\cdots&(\omega_{n}^{n-1})^{n-1}\\ 1&(\omega_n^{n-2})^1&({\omega_n^{n-2}})^2&\cdots&(\omega_{n}^{n-2})^{n-1}\\ \vdots&\vdots&\vdots&\ddots&\vdots\\ 1&(\omega_n^1)^1&{(\omega_n^1)}^2&\cdots&(\omega_{n}^1)^{n-1}\end{bmatrix}\\ &=\frac1n\begin{bmatrix}1 & 1 & 1 & \cdots & 1 \\1 & (\overline{\omega_n^1})^1 & (\overline{\omega_n^1})^2 & \cdots & (\overline{\omega_n^1}) ^ {n-1}\\1 & (\overline{\omega_n^2})^1 & (\overline{\omega_n^2})^2 & \cdots & (\overline{\omega_n^2}) ^ {n-1} \\ \vdots & \vdots &\vdots & \ddots & \vdots \\ 1 & \overline{(\omega_n^{n-1}})^1 & (\overline{\omega_n^{n-1}})^2 & \cdots & (\overline{\omega_n^{n-1}}) ^ {n-1}\end{bmatrix}\\\end{align}\\

也就是说,在做快速傅里叶逆变换的时候,只需要在做快速傅里叶变换的基础上,把本来要带入的单位根取共轭复数,然后再把变换的结果乘 1n\frac{1}{n} 就可以了。

最后更新于:2026-03-17 04:50

Caiwen
本文作者
一只蒟蒻,爱好编程和算法