Skip to content
NotesAlgorithm

算法设计与分析课程笔记,覆盖分治、图算法、网络流、线性规划与复杂度基础。

Divide and Conquer: Closest Pair of Points ​

1. 问题描述 (Problem Description) ​

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

2. 暴力解法 (Brute Force) ​

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

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

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

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

  1. 划分 (Divide):按照 x 坐标排序,用一条垂直线 L 将点集分为左右各一半 PL 和 PR。
  2. 求解 (Conquer):递归地在 PL 和 PR 中找出最近点对距离,分别记为 δ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(nlog⁡n)。

  • 递归式:

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

    关键细节:如何在合并步骤实现 O(n)? 如果每一层都调用排序函数(如 sort),则合并步代价为 O(nlog⁡n),总复杂度将变成 O(nlog2⁡n)。为了降到 O(nlog⁡n),我们采用类似 归并排序 (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(nlog⁡n)

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

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

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(nlog⁡n)。

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

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

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

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

1. 决策树模型 ​

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

2. 关键推导步骤 ​

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

3. 结论 ​

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

随机排序算法及下界分析 ​

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

1. 随机排序算法的本质 ​

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

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

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

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

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

3. 随机算法的下界依然是 Ω(nlog⁡n) ​

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

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

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

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

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

总结 ​

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

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

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

给定两个多项式:

  • p(x)=a0+a1x+a2x2+⋯+ad−1xd−1
  • q(x)=b0+b1x+b2x2+⋯+bd−1xd−1

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

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

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

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

[1x0x02⋯x0d−11x1x12⋯x1d−1⋮⋮⋮⋱⋮1xd−1xd−12⋯xd−1d−1][a0a1⋮ad−1]=[y0y1⋮yd−1]

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

det(V)=∏0≤i<j<d(xj−xi)

因为我们前提假设取出的 xi 是互不相同的,所以对所有 i≠j,(xj−xi)≠0。因此:

det(V)≠0

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

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

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

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

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

    R=(r(x0),r(x1),…,r(xn−1))=(p(x0)q(x0),…,p(xn−1)q(xn−1))

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

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

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

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,…,ωnn−1

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

考虑一个最高次项为 n−1 阶的多项式(假设 n是 2 的整次幂): P(x)=a0+a1x+a2x2+⋯+an−1xn−1

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

  • 偶数次项系数组成:Peven(x)=a0+a2x+a4x2+⋯+an−2xn/2−1
  • 奇数次项系数组成:Podd(x)=a1+a3x+a5x2+⋯+an−1xn/2−1

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

P(x)=Peven(x2)+x⋅Podd(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 个点在复平面原点两侧是成对互为相反数的(即 x 和 −x)。 当我们把这成对的两个点代入拆分后的公式中:

    P(ωnk)=Peven(ωn/2k)+ωnk⋅Podd(ωn/2k)P(ωnk+n/2)=P(−ωnk)=Peven(ωn/2k)−ωnk⋅Podd(ω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) 是最后将 Peven 和 Podd 在本层进行线性的相加/相减组合的合并开销)。

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