Skip to content
NotesAlgorithm

算法设计与分析课程笔记,覆盖分治、图算法、最短路与复杂度基础。

Divide and Conquer: Closest Pair of Points

1. 问题描述 (Problem Description)

给定二维平面上的 n 个点,找出一对点,使得它们之间的欧几里得距离最小。

2. 暴力解法 (Brute Force)

遍历所有可能的点对 (pi,pj),计算它们之间的距离并取最小值。

  • 点对数量(n2)=n(n1)2
  • 时间复杂度O(n2)

3. 分治法优化思路 (Divide and Conquer Idea)

核心思路是将点集分为左右两部分,分别求解。

  1. 划分 (Divide):按照 x 坐标排序,用一条垂直线 L 将点集分为左右各一半 PLPR
  2. 求解 (Conquer):递归地在 PLPR 中找出最近点对距离,分别记为 δLδR
  3. 合并 (Combine)
    • δ=min(δL,δR)
    • 此时最近点对可能出现在:PL 内部、PR 内部,或者跨越 L(一个点在 PL,另一个在 PR)。
    • 我们只需要寻找是否存在跨越 L 且距离小于 δ 的点对。

4. δ 区域选择与跨界搜索策略

为了高效处理“合并”步骤,我们只关注距离垂直线 L 水平距离在 δ 以内的点(形成一个宽度为 2δ 的垂直带状区域 Strip)。

  • 策略
    • 将 Strip 区域内的点按 y 坐标排序。
    • 对于 Strip 中的每一个点 p,我们只需要检查在 y 坐标排序序列中紧随其后的常数个点(通常证明只需检查 7 个点)。
  • 几何原理
    • L 左侧和右侧的 δ×δ 正方形内,由于已知内部最近点距至少为 δ,因此每个正方形内最多只能放下 4 个点。在跨越 Lδ×2δ 矩形区域内,最多只有 8 个点满足两两距离 δ

5. 时间复杂度分析 (Complexity Analysis)

我们希望递归过程的每一层合并操作是线性的。

  • 预处理:按 x 坐标和 y 坐标预排序所需时间为 O(nlogn)

  • 递归式

    T(n)=2T(n/2)+O(n)

    关键细节:如何在合并步骤实现 O(n) 如果每一层都调用排序函数(如 sort),则合并步代价为 O(nlogn),总复杂度将变成 O(nlog2n)。为了降到 O(nlogn),我们采用类似 归并排序 (Merge Sort) 的策略:

    1. 返回结果:递归函数不仅返回最小距离 δ,还返回该区域内所有点按 y 轴排序后的列表。
    2. 线性合并序:在 Combine 阶段,我们不需要重新排序。由于左子集和右子集已经分别是 y-sorted 列表,我们只需通过一次 O(n)Merge 操作(双指针法)即可得到当前全集的 y-sorted 列表。
    3. 筛选 Strip:从这个已经 y 排序的全集中,线性遍历筛选出那些水平距离中轴线小于 δ 的点放入 Strip。此时 Strip 里的点自然也是按 y 排序的。
    4. 检查邻居:在 y 排序的 Strip 中,每个点只需检查其后的 7 个点。
  • 求解: 根据主定理 (Master Theorem),a=2,b=2,d=1。因为 a=bd (2=21):

    T(n)=O(nlogn)

结论:通过在递归过程中利用归并排序的思想维护 y 轴序,我们将合并步的代价严格控制在 O(n),从而实现了整体 O(nlogn) 的最优复杂度。

结论:通过分治法,最近点对问题从 O(n2) 优化到了 O(nlogn)

Sorting Lower Bound

spaghetti sort 意大利面排序

意大利面排序(Spaghetti Sort)是一种启发式或“物理”排序算法,由 A. K. Dewdney 提出。它展示了如何利用物理并行性来打破传统计算模型中的排序下界。

1. 算法过程 (Physical Process)

准备:对于待排序的 n 个正整数,准备 n 根意大利面。 映射:根据每个数值的大小,将对应的意大利面剪成相应的长度。 对齐:将所有剪好的面竖直抓在手中,底部对齐,轻轻放在平整的桌面上。 取出:用另一只手从上方平稳下降。第一个触碰到手的面就是最长的(最大值)。 记录:记录该值并移走这根面,重复直到取完。

2. 理论复杂度

  • 在这个“物理模型”中,对齐和下降的过程被认为是 O(1) 或与 n 无关的,而取出所有面需要 n 次操作。因此,在物理世界中,它的理论时间复杂度接近 O(n)

3. 为什么在现代计算机模型(RAM 模型)上不可行?

尽管它看起来像是一个 O(n) 算法,但在现有的计算机体系结构(冯·诺依曼架构 / 随机存取机器 RAM 模型)中,它无法实现,原因如下:

(1) 计算模型的差异 (Model Mismatch)

计算机是串行/离散的。在数字计算机中,没有“物理下降并同时触碰”这个动作。为了模拟这个过程,计算机必须:

  • 逐个比较所有面的高度以找到最大值(这需要 O(n))。
  • 总共 n 个面,复杂度退化回 O(n2)。即使使用优先队列(堆),也只能达到 O(nlogn)

(2) 资源与空间复杂度 (Space Complexity)

  • 剪短意大利面的操作隐藏了空间开销。如果你要排序一个很大的数(如 264),你不可能找到那么长的意大利面。
  • 在计算机中,这对应于桶排序计数排序。虽然它们是线性的,但其空间复杂度取决于数值的范围 (Range),而不是元素的个数 n

基于比较排序的时间下界证明思路

比较排序模型 (Comparison Sort Model) 中,算法只能通过两两比较来确定元素的相对顺序。证明 Ω(nlogn) 下界的核心工具是 决策树 (Decision Tree)

1. 决策树模型

  • 节点:每个内部节点表示一次比较操作,例如 aiaj
  • 分支:每次比较有两个可能结果(是/否),对应树的两个分支。
  • 叶子节点:树的每一个叶子节点代表一种可能的排序结果(排列)

2. 关键推导步骤

  1. 可能的排列总数:对于 n 个不同的元素,共有 n! 种可能的排列方式。
  2. 叶子数量的要求:为了能够区分所有可能的输入,决策树必须至少有 n! 个叶子节点。设叶子节点数为 L,则有:Ln!
  3. 树高与比较次数:算法的最坏情况运行时间(最少比较次数)对应于决策树的最小高度 h。由于这是一棵二叉树,高度为 h 的树最多有 2h 个叶子节点:2hLn!
  4. 解不等式:对两边取对数:hlog2(n!)
  5. 斯特林公式应用:利用 log(n!)nlognnloge(或简单的 n!(n/2)n/2):log2(n!)=Ω(nlogn)

3. 结论

在比较模型下,任何排序算法为了区分 n! 种可能性,其决策树的高度至少为 Ω(nlogn)。因此,比较排序的最坏情况时间复杂度下界是 Ω(nlogn)

随机排序算法及下界分析

以上讨论的是确定性排序算法 (Deterministic Sorting Algorithms)。对于随机化算法 (Randomized Algorithms,例如随机取 pivot 的 QuickSort),我们需要从概率的角度来分析其本质与下界。

1. 随机排序算法的本质

  • 随机性来源:算法在执行过程中不完全依赖输入,而是引入了随机数发生器(如抛硬币)来决定下一步动作(例如随机选取划分的基准元素)。
  • 算法视角:一个随机化算法实质上是一组确定性算法的概率分布。对于同一组固定的输入,由于随机数不同,算法计算的路径、生成的决策树也会不同。
  • 关注指标:对于随机化算法,我们通常不再只关注某一次执行的最坏情况,而是分析其对于最坏输入的 期望运行时间 (Expected Running Time)。随机性使得算法能以极高的概率避免像 QuickSort 中 O(n2) 这样的最糟糕情况分布。

2. 随机决策树 (Randomized Decision Trees)

为了分析随机化排序,我们可以扩展决策树模型:

  • 比较节点 (Comparison Nodes):普通的测定 aiaj 的节点。
  • 随机节点 (Random Nodes):表示一次随机选择,它不代表元素之间的实际比较操作。因此在树高(比较次数)计算中不计入成本。

对于任意指定的输入排列,算法在随机树中走过的叶子深度是一个概率变量,我们计算的是到达正确叶子所需的期望深度

3. 随机算法的下界依然是 Ω(nlogn)

令人略感遗憾的是,随机抛硬币并不能打破基于比较的排序下界。即使是随机化的比较排序,其在最坏输入下的期望比较次数依然是 \Omega(n \log n)。这可以通过理论计算机科学中的重要定理来解释:

Yao's Minimax Principle (姚期智最小最大原理)

姚期智原理建立在冯·诺依曼的博弈论基础之上。它指出,对于任何问题:

(随机算法对最坏输入的期望代价) (最优确定性算法在最坏输入分布上的平均代价)

简而言之:假设有一个“恶魔”对手,它能够挑选一个使算法表现最差的输入数据分布。对于排序问题,无论对手如何构造输入的数据分布,任何确定的比较排序平均下来都需要 Ω(nlogn) 次比较。根据姚氏原理,不管你怎么设计随机化策略,在面临“恶魔”的最坏输入选择时,你的期望操作次数不可能低于这个下界。

总结

  1. 信息论实质:排序过程本质上是通过比较来消除信息的不确定性。你需要区分 n! 种排列,即获取 log2(n!)nlogn bits 的信息量。随机抛硬币本身并不能为你提供关于原数据的偏序信息。
  2. 随机化的作用:随机化无法降低信息学上的下界,但它能让你在绝大多数真实场景中(甚至对抗恶意构造的数据下)都能有极大概率维持极佳的性能,从而使平均情况的分析更加稳健(如 QuickSort 的期望 O(nlogn))。

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

1. 多项式乘法建模 (Modeling Polynomial Multiplication)

给定两个多项式:

  • p(x)=a0+a1x+a2x2++ad1xd1
  • q(x)=b0+b1x+b2x2++bd1xd1

它们的乘积 r(x)=p(x)q(x) 是一个最高次项为 2d2 的多项式(项数为 2d1)。 传统的系数直接相乘(卷积)的方法时间复杂度为 O(d2)。FFT 的核心思想是通过点值表示法 (Point-Value Representation) 将该操作优化到 O(dlogd)

2. 核心定理:d 个点唯一确定一个 d1 阶多项式

一个最高次为 d1 的多项式有 d 个系数 (a0,a1,,ad1)。如果在多项式上取 d互不相同的横坐标点 x0,x1,,xd1,并得到对应的函数纵坐标值 y0,y1,,yd1,则这 d 个点 (xi,yi) 可以唯一确定这个多项式的各个系数。

证明(利用范德蒙德矩阵 Vandermonde Matrix): 将选取的 d 个点代入多项式表达式,可以得到如下的线性方程组:

[1x0x02x0d11x1x12x1d11xd1xd12xd1d1][a0a1ad1]=[y0y1yd1]

左侧系数矩阵被称为范德蒙德矩阵 V。 计算该矩阵的行列式:

det(V)=0i<j<d(xjxi)

因为我们前提假设取出的 xi互不相同的,所以对所有 ij(xjxi)0。因此:

det(V)0

行列式非零意味着矩阵满秩可逆,所以存在唯一的解解向量 a

3. FFT 多项式乘法框架 (Framework of FFT)

基于上述“点值可以唯一还原对应多项式”的原理,为了高效求解 r(x)=p(x)q(x),FFT 构建了以下三个核心步骤(Evaluation Multiply Interpolation):

  1. 求值 (Evaluation): 由于目标多项式 r(x) 的项数为 2d1,要唯一确定它,我们需要获取至少 2d1 个点。(为方便二分治,一般取 n2d1n2 的整次幂)。 选取 n 个恰好的点 x0,x1,,xn1。分别将它们带入原多项式,求出对应的离散值序列:

    P=(p(x0),p(x1),,p(xn1))Q=(q(x0),q(x1),,q(xn1))
  2. 点乘 (Point-wise Multiplication): 我们要求的 r(x) 在这些点上的值非常容易获得。因为 r(xi)=p(xi)q(xi),所以只需将上一步得到的点同项直接相乘即可:

    R=(r(x0),r(x1),,r(xn1))=(p(x0)q(x0),,p(xn1)q(xn1))

    这一步只需要 O(n) 次简单的标量相乘。

  3. 插值 (Interpolation): 现在我们已经拥有了 r(x)n 个不同的点值 (xi,r(xi))。 接下来,我们依据这些点,反向插值出最终的多项式系数。

整个 FFT 算法的精髓就在于如何极其巧妙地选择那 n 个点(即利用复平面上的单位单位根 (Roots of Unity) 的对称性),使得第 1 步求值和第 3 步插值能够运用分治法,实现每次将问题规模巧妙二分,从而将最核心的两步优化至 O(nlogn)

4. 为什么选择复数单位根 (Complex Roots of Unity)?

在“求值”阶段,如果随机选择 n 个不同的实数,直接计算的时间复杂度依然是 O(n2)。我们需要选择一组“特殊”的点,使得计算过程中存在大量的重复子结构,从而可以利用分治法进行优化。这组特殊的点就是复平面上的 n 次单位根

(1)单位根的定义

n 次单位根是指满足 xn=1 的所有复数 x。 根据欧拉公式 eiθ=cosθ+isinθ,这 n 个根均匀分布在复平面的单位圆上。 我们将n 次单位根记为:

ωn=e2πin

那么所有的 n 个单位根可以表示为:

ωn0,ωn1,ωn2,,ωnn1

(2)分治法的核心:多项式的奇偶拆分

考虑一个最高次项为 n1 阶的多项式(假设 n2 的整次幂): P(x)=a0+a1x+a2x2++an1xn1

我们可以按项的奇偶性将它拆分为两个规模一半(项数为 n/2)的子多项式:

  • 偶数次项系数组成:Peven(x)=a0+a2x+a4x2++an2xn/21
  • 奇数次项系数组成:Podd(x)=a1+a3x+a5x2++an1xn/21

此时原多项式可以非常优雅地表示为:

P(x)=Peven(x2)+xPodd(x2)

(3)单位根的绝妙性质 (Properties of Roots of Unity)

为什么单位根完美适配上述的奇偶拆分?这归功于单位根的关键数学性质:

  1. 折半引理 (Halving Lemma / 平方缩减性)

    (ωnk)2=ωn/2k

    意义:当我们将 n 个不同的 n 次单位根平方后,它们两两重合,会“折叠”成仅仅 n/2不同的 n/2 次单位根。 这正是问题规模成功减半的根本保障!在计算 Peven(x2)Podd(x2) 时,原本需要代入 n 个不同的值,现在只需要递归代入 n/2 个不同的值即可。

  2. 对称性质 (Symmetry Property)

    ωnk+n/2=ωnk

    意义:这意味着我们要代入的 n 个点在复平面原点两侧是成对互为相反数的(即 xx)。 当我们把这成对的两个点代入拆分后的公式中:

    P(ωnk)=Peven(ωn/2k)+ωnkPodd(ωn/2k)P(ωnk+n/2)=P(ωnk)=Peven(ωn/2k)ωnkPodd(ωn/2k)

    由此可见,对于成对的两个点 ωnkωnk+n/2,我们只需要计算一次 Peven(ωn/2k)Podd(ωn/2k)。得到结果后,只需做一次标量加法和一次标量减法,就可以同时求出这两个点的值!(这就是著名的蝶形运算 Butterfly Operation 的数学基础)。

(4)求值阶段的复杂度递推式

由于上述绝妙性质,求出一个项数为 n 的多项式在 n 个单位根上的所有值的代价,变为了求两个项数为 n/2 的多项式在 n/2 个单位根上的所有值的代价:

T(n)=2T(n/2)+O(n)

(这里的 O(n) 是最后将 PevenPodd 在本层进行线性的相加/相减组合的合并开销)。

根据主定理 (Master Theorem),求值步骤的时间复杂度被成功从 O(n2) 优化至: $$O(n \log n)$$