哈佛-CS224-高级算法笔记-全-

哈佛 CS224 高级算法笔记(全)

001:课程介绍与静态前驱问题

在本节课中,我们将学习高级算法课程的基本信息,并深入探讨一个核心的数据结构问题——静态前驱问题。我们将了解两种解决该问题的数据结构:van Emde Boas树和融合树,并分析它们在Word RAM模型下的性能。

课程概述与目标

本课程是CS224高级算法。主讲人是Glai Nelson,助教是Jeffrey。课程的主要目标是提升分析和创建算法的能力。我们将学习多种在CS124课程中未涉及的分析技术和算法模型。课程将更侧重于理论,作业将以书面形式完成,不包含编程任务。

课程结构与评分

课程评分由三个部分组成:

  • 课堂笔记:占总成绩的10%。学生需轮流记录课堂内容,并在课后24小时内整理成LaTeX格式的笔记。
  • 作业:占总成绩的60%。所有作业需使用LaTeX编写,并通过邮件提交,且有页数限制。
  • 期末项目:占总成绩的30%。学生需在阅读期最后一天提交项目报告。项目提案的截止日期是10月30日。

此外,根据班级规模,学生可能需要轮流担任一次作业评分员。

算法模型与排序的误解

在传统的算法课程中,我们通常学习基于比较的排序模型,其时间复杂度下界为 Ω(n log n)。然而,在真实的计算机模型中,我们可以进行更多操作。

Word RAM模型更贴近实际计算机。在该模型中:

  • 数据项是范围在 [0, 2^w - 1] 的整数,其中 w 是字长,宇宙大小 U = 2^w
  • 我们假设指针可以存放在一个字中,因此 w ≥ log n
  • 除了比较,我们可以在常数时间内进行整数运算(加、减、乘、除)、位运算(与、或、非、异或)和位移操作。

在Word RAM模型中,排序可以突破 n log n 的下界。本节课将探讨如何通过解决一个相关的数据结构问题来实现更快的排序。

静态前驱问题

静态前驱问题是一个数据结构问题。其目标是设计一个数据结构,用于表示一个包含 n 个元素的集合 S,并支持一种查询操作:对于给定的查询值 z,返回集合 S 中小于 z 的最大元素。

我们追求低空间占用和快速查询。“静态”意味着集合 S 在构建后不会改变(不支持插入和删除)。如果是“动态”版本,则还需要支持元素的插入和删除操作。

一个简单的解决方案是:

  • 静态:存储排序后的数组,使用二分查找。查询时间为 O(log n),空间为 O(n)
  • 动态:使用平衡二叉搜索树(如红黑树)。查询和更新时间为 O(log n),空间为 O(n)

有趣的是,如果我们有一个高效的动态前驱数据结构,就可以用它来构造一个快速的排序算法:依次插入所有元素,然后通过反复查询前驱来输出有序序列。因此,一个优于 O(log n) 的动态前驱数据结构意味着存在优于 O(n log n) 的排序算法。

两种高效的前驱数据结构

本节我们将介绍两种在Word RAM模型下表现优异的前驱数据结构,它们在不同的参数条件下各有优势。

1. van Emde Boas树

van Emde Boas树(vEB树)是一种递归数据结构。对于一个宇宙大小为 U 的vEB树,其内部包含:

  • 一个大小为 √U 的数组,每个元素是一个宇宙大小为 √U 的vEB子树(称为簇)。
  • 一个宇宙大小为 √U 的vEB树(称为摘要),用于记录哪些簇非空。
  • 额外存储的最小值和最大值元素。

将一个整数 x 视为高位 c 和低位 i 的组合,即 x = c * √U + i。元素 x 被存储在簇 c 中,值为 i。如果簇 c 在插入前为空,则同时将 c 插入摘要树中。

查询(前驱)算法

  1. x 大于当前树的最大值,则直接返回最大值。
  2. 否则,检查 x 所在簇 c
    • 若该簇非空且 x 大于该簇的最小值,则在该簇内递归查询 i 的前驱。
    • 否则,在摘要树中查询 c 的前驱簇 c',然后返回簇 c' 中的最大值。

插入算法需要特殊处理最小值,以确保递归成本可控。通过巧妙的设计,vEB树的查询和更新时间均可达到 O(log w) = O(log log U)

初始的vEB树需要 Θ(U) 的空间,这在大宇宙下不可接受。优化方法是使用哈希表(字典)来动态存储非空的簇,而非静态数组。每个非空簇的存储开销可以“记账”到该簇的最小元素上,从而将总空间降低到 Θ(n)

2. 融合树

融合树是另一种高效的数据结构,由Fredman和Willard提出。对于静态前驱问题,融合树支持 O(log_w n) 的查询时间,并使用线性空间。

由于 log_w n = log n / log w,当 w 较大时(例如 w ≈ log n),该值远小于 log n。融合树的核心思想是利用字长的优势,在单个字内并行地进行多个比较操作。

结合vEB树(O(log w))和融合树(O(log_w n)),我们可以实现动态前驱查询时间为:
O( min{ log w, log_w n } )
当两者相等时(即 log w = √(log n)),得到最优时间 O(√(log n))。这意味着存在 O(n √(log n)) 的排序算法。实际上,还有更快的排序算法,例如 O(n log log n)(确定性)和 O(n √(log log n))(随机化),线性时间排序是否可能仍是一个开放问题。

课程总结

本节课我们一起学习了高级算法课程的框架,并深入探讨了Word RAM模型下的静态前驱问题。我们分析了两种关键的数据结构:van Emde Boas树和融合树。vEB树通过递归和哈希表优化,实现了 O(log log U) 的操作时间和线性空间。融合树则利用字长并行性,实现了 O(log_w n) 的查询时间。理解这些数据结构的设计思想和性能权衡,是构建高效算法的重要基础。下节课我们将继续深入探讨融合树的细节。

002:融合树 (Fusion Trees)

在本节课中,我们将学习融合树,这是一种用于解决静态前驱查询问题的数据结构,其查询时间复杂度为 O(log_w n),其中 w 是机器字长。我们将看到如何通过使用乘法、草图压缩和字级并行等技巧,在常数时间内处理一个包含多个键的节点。


概述

融合树的核心思想是使用多路搜索树,其中每个内部节点存储大约 k 个键(k = Θ(w^(1/5))),而不是二叉搜索树中的单个键。这样,树的高度可以降低到 O(log_k n) = O(log_w n)。然而,挑战在于如何在常数时间内处理一个包含 k 个键的节点,因为这些键本身甚至无法全部放入一个机器字中。我们将通过一系列巧妙的位操作来解决这个问题。


融合树的基本结构

上一节我们介绍了融合树的目标是降低树高。本节中我们来看看其基本节点结构。

一个融合树节点包含 k 个已排序的键:x₀ < x₁ < ... < x_{k-1}。与每个键关联的是一个子树,包含所有落在该键对应区间内的元素。例如,所有小于 x₀ 的键位于最左侧的子树,所有介于 x_i 和 x_{i+1} 之间的键位于相应的子树中。

理想情况下,树的高度为 O(log_k n)。当 k = w^(1/5) 时,高度为 O(log_w n)。但问题在于,在每个节点,我们需要在 k 个键中定位查询键 q 的位置,如果朴素地使用线性扫描或二分查找,会引入额外的 O(k) 或 O(log k) 因子,从而抵消了降低树高的优势。因此,我们的核心挑战是:

如何在常数时间内,在一个融合树节点中完成搜索?


草图压缩

为了将节点信息压缩到单个机器字中,我们引入了“草图”的概念。其核心思想是,并非键的所有位对于区分节点内的键都是必要的。

考虑所有键的二进制表示构成的二叉 Trie 树。区分这些键只需要关注那些在 Trie 树中产生分支的位。我们称这些位为“分支位”。

设共有 r 个分支位,其索引为 b₀ < b₁ < ... < b_{r-1}。对于任意键 x,我们定义其草图 sketch(x) 为仅保留这些分支位后得到的 r 位数字。

关键观察:由于 r ≤ k-1 = O(w^(1/5)),因此所有 k 个键的草图总共只需要 O(k * r) = O(w^(2/5)) 位,这可以放入一个机器字中。

然而,直接计算草图(即提取并紧密压缩这些特定位)并非一个基本的 CPU 指令。我们需要一种方法,在常数时间内,为任意查询键 q 计算其草图。


通过乘法进行压缩

我们无法进行完美压缩,但可以接受一种“近似压缩”,将重要的 r 个位压缩到一个宽度为 O(r^4) 的连续区域内。这通过精心选择的乘法来实现。

引理:给定分支位索引 b_i,我们可以选择一个乘数 M,使得对于所有 i,和值 b_i + m_i 互不相同,并且所有这些和值分布在一个宽度为 O(r^4) 的连续区间内。这里 m_i 是 M 的二进制表示中特定的位索引。

压缩过程

  1. 掩码:用掩码提取键 x 的所有分支位,得到 x'。
  2. 乘法:计算 x' * M。
  3. 二次掩码:用另一个掩码提取出位置在 b_i + m_i 的位。
  4. 移位:将结果右移,使得最小的 b_i + m_i 对应到结果的第 0 位。

最终,我们得到了 x 的一个压缩表示(草图),它包含了原始分支位的信息,并且存储在一个宽度为 O(r^4) = O(w^(4/5)) 的位段内。由于 M 和掩码可以在预处理阶段为每个节点计算好,因此对于任何输入(节点存储的键或查询键),我们都可以通过常数次乘法、按位与和移位操作来计算其草图。


处理查询与近似草图

仅比较查询键 q 和节点键 x_i 的草图是不够的。因为 q 可能在某些非分支位上与 x_i 不同,导致其草图无法正确反映其在排序序列中的真实位置。

解决方案:当 sketch(q) 落在 sketch(x_i) 和 sketch(x_{i+1}) 之间时,我们构造一个辅助键 y。

  1. 找到 q 与 x_i 或 x_{i+1} 的二进制表示中,第一个出现分歧的位(通过计算 XOR 并寻找最高有效位)。
  2. 设 y 为到这个分歧点为止,q 与它们共享的前缀。
  3. 构造键 e:
    • 如果 q 在该分歧位转向了右侧(即 q 在该位为 1,而 x_i 或 x_{i+1} 为 0),则令 e = y 后接一个 0,再后接一串 1。
    • 如果 q 在该分歧位转向了左侧,则令 e = y 后接一个 1,再后接一串 0。

关键性质:键 e 在节点键 {x_i} 中的排序位置,与查询键 q 的真实排序位置相同。因此,我们可以通过比较 e(或其草图)与节点键来确定递归方向。


并行比较与定位

现在,假设我们有了查询键 q 的草图,以及节点中所有键的草图。我们需要快速找出 q 的草图位于哪两个相邻的节点键草图之间。

我们将所有节点键的草图存储在一个字中,格式如下:1 sketch(x₀) 1 sketch(x₁) ... 1 sketch(x_{k-1})。每个草图块之间用“1”分隔,每个草图宽度为 O(r^4)。

对于查询键 q,我们构造其扩展草图:0 sketch(q) 0 sketch(q) ... 0 sketch(q),重复 k 次。

操作过程

  1. 从扩展草图字中减去节点草图字。
  2. 由于每个块的开头是“1”与“0”的比较,减法会在每个块产生一个借位,当且仅当 sketch(q) > sketch(x_i) 时,该块结果的最高位为 0(表示发生借位),否则为 1。
  3. 结果字中,每个块的开头位(0或1)构成了一个单调序列:先是若干个1(对应 sketch(q) <= sketch(x_i)),然后是0(对应 sketch(q) > sketch(x_i))。
  4. 我们需要找到第一个 0 出现的位置。这等价于统计结果字中开头位为 1 的数量。

并行计数技巧
通过乘以一个特定的、在固定位置有 1 的掩码字,可以将所有块开头的位(1或0)求和到结果字的一个小区间内。然后通过掩码和移位,即可读出 1 的个数,从而确定第一个大于 sketch(q) 的键的索引 i。

这样,我们仅用常数次乘法、加法和位操作,就完成了与节点内所有 k 个键的并行比较。


计算最高有效位

上述流程中,我们需要计算两个数 XOR 后的最高有效位(MSB)索引。在标准模型中,这需要 O(w) 或 O(log w) 时间。但我们承诺在常数时间内完成。以下是基于字级并行和草图压缩的 O(1) 时间 MSB 算法概要。

算法思路

  1. 将 w 位的字 x 划分为 √w 个块,每块 √w 位。
  2. 步骤1:找到第一个非零块
    • 构造一个字,其每个块的首位为 1,其余位为 0。
    • 通过减法、掩码和按位操作,生成一个字,其中每个块的首位为 1 当且仅当原字中对应块非零。
    • 使用与“并行比较”类似的技术(通过乘法压缩块首位于连续区域),找出第一个非零块的索引。
  3. 步骤2:在找到的非零块内找到最高有效位
    • 将该块(√w 位)提取出来。
    • 由于 √w 很小,我们可以预先计算或使用类似的方法,在这个小字内快速找到 MSB。
  4. 结合块索引和块内位索引,即可得到全局 MSB 索引。

该算法巧妙地运用了我们已经熟悉的乘法压缩和并行比较技术,在常数时间内解决了 MSB 问题。


总结

本节课中我们一起学习了融合树数据结构的核心构造:

  1. 结构:使用多路搜索树,节点容量 k = Θ(w^(1/5)),以将树高降至 O(log_w n)。
  2. 草图压缩:通过仅保留分支位,并使用精心设计的乘法将键信息压缩到可管理的宽度内,使得节点信息能放入单个机器字。
  3. 查询处理:通过构造辅助键 e 解决草图可能失真的问题,并利用并行比较技术在常数时间内确定查询键在节点中的位置。
  4. 基础原语:展示了如何在常数时间内计算最高有效位,这是实现上述技巧的关键组件。

融合树展示了如何充分利用字 RAM 模型的强大能力(特别是乘法和字级并行)来突破基于比较的算法下限,是理论计算机科学中的一个经典成果。

003:哈希技术

在本节课中,我们将要学习哈希技术及其在计算机科学中的核心应用。我们将从负载均衡问题入手,探讨哈希的基本概念,然后深入研究k-独立性、字典问题,并简要介绍线性探测等高级哈希方案。

负载均衡与哈希

哈希的一个经典应用是将任务均匀地分配给多台机器。假设我们有n个任务和m台机器,每个任务有一个唯一的ID。我们希望将每个任务分配给一台机器,使得每台机器的负载大致均衡。

一种简单的方法是使用一个哈希函数h,将任务ID映射到机器编号。当新任务J到达时,它被分配给机器h(J)。我们希望了解这种分配的性能,即是否有机器会过载。

这通常被称为“球与箱”随机过程:我们将n个球随机扔进m个箱子,希望了解其行为。具体来说,我们希望证明:存在一台机器接收超过k个任务的概率很小。

切尔诺夫界

为了分析上述概率,我们首先回顾一个重要的尾概率不等式——切尔诺夫界。

定理(切尔诺夫界):假设X₁, X₂, ..., Xₙ是独立的随机变量,其中Xᵢ服从伯努利分布,期望为p(即P(Xᵢ=1)=p)。令X = Σᵢ Xᵢ,则对于任意δ > 0,有:
P(X ≥ (1+δ)E[X]) ≤ (e^δ / (1+δ){(1+δ)})

我们可以利用这个界来分析负载均衡问题。考虑m = n的情况,即机器数与任务数相等。此时,每台机器的期望负载是1。应用切尔诺夫界可以证明,存在一台机器负载超过C·log n / log log n的概率至多是1/poly(n)。通过取并集,我们可以得到所有机器负载都不超过该阈值的概率很高。

然而,切尔诺夫界的证明需要随机变量完全独立。在负载均衡的分析中,我们实际上只需要一个更弱的条件。

k-独立性

在哈希的语境下,我们并不总是需要完全独立的哈希函数。k-独立性是一个更弱但通常足够的概念。

定义(k-独立性):一个哈希函数族H被称为k-独立的,如果对于任意k个不同的键x₁, x₂, ..., xₖ ∈ U,以及任意k个值y₁, y₂, ..., yₖ ∈ {0, 1, ..., m-1},当从H中均匀随机选择一个哈希函数h时,有:
P(h(x₁)=y₁ ∧ h(x₂)=y₂ ∧ ... ∧ h(xₖ)=yₖ) = 1/mᵏ

这意味着任意k个键的哈希值在统计上相互独立。一个重要的例子是多项式哈希:设U = m = p(一个素数),定义哈希函数族为所有次数不超过k-1的多项式,即h(x) = Σ_{i=0}^{k-1} aᵢ xⁱ mod p,其中系数aᵢ从{0, 1, ..., p-1}中均匀随机选取。这个族是k-独立的。与存储一个完全随机函数(需要O(u log m)比特)相比,存储这样一个多项式哈希函数只需要O(k log p)比特,当k为常数时,空间效率显著提高。

在负载均衡的分析中,我们证明“存在k个任务映射到同一台机器”的概率时,实际上只涉及最多k个任务哈希值的联合分布。因此,使用一个k-独立的哈希函数族(其中k约为log n / log log n)就足以保证与完全随机哈希相同的性能界限。

字典问题

哈希的一个核心应用是解决字典问题。字典是一种数据结构,支持两种操作:

  1. 更新(update):将键k与值v关联起来。
  2. 查询(query):返回与键k关联的值v(如果存在)。

一个经典的动态字典解决方案是链式哈希

以下是链式哈希的工作原理:

  • 初始化一个大小为m的数组A,每个位置指向一个空链表。
  • 从某个哈希函数族(例如2-独立族)中随机选择一个哈希函数h。
  • 更新操作:对于键k,计算其哈希值i = h(k)。在A[i]指向的链表中查找k。如果找到,则更新其关联的值;如果未找到,则将(k, v)对添加到链表末尾。
  • 查询操作:类似地,哈希后在其对应的链表中查找键k。

一次操作(更新或查询)的运行时间取决于链表A[h(k)]的长度。可以证明,如果哈希函数是2-独立的,且数组大小m至少与当前存储的键数n成比例(例如m ≥ n),则链表的期望长度是常数,从而实现期望常数时间的操作。

线性探测

另一种流行的动态字典方案是线性探测。它因其对缓存友好而备受青睐,在现代计算机架构中,顺序内存访问速度远快于随机访问。

以下是线性探测的工作原理:

  • 我们有一个数组A(大小为m)和一个哈希函数h。
  • 插入操作:对于键k,我们首先尝试将其存储在A[h(k)]。如果该位置已被占用,则我们顺序地(线性地)检查下一个位置A[h(k)+1], A[h(k)+2], ...,直到找到一个空位,然后将(k, v)存储在那里。
  • 查询操作:从A[h(k)]开始顺序查找,直到找到键k或遇到一个空位(表示键不存在)。

线性探测的分析更为复杂。历史研究表明:

  • 在完全随机哈希函数的假设下,当m ≥ (1+ε)n时,每次操作的期望时间为O(1/ε²)。
  • 一个重要的理论突破表明,5-独立性足以保证线性探测的期望操作时间为常数(当m ≥ cn,c为常数)。
  • 另一方面,存在特定的4-独立哈希函数族,使得线性探测的期望时间可能达到Ω(log n)。这意味着4-独立性不足以保证常数时间性能。

为了直观理解为什么需要较高的独立性(如5-独立或7-独立),可以考虑分析过程。分析的关键是考察包含查询键哈希值的“满区间”。一个区间I被称为“满的”,如果哈希到该区间的键数至少等于区间长度。可以证明,如果一次查询需要探查k个位置,那么查询键的哈希值必然位于某个长度为k的满区间内。

因此,期望查询时间可以上界为包含h(x)的所有满区间的期望数量。通过计算一个给定长度k的区间是“满的”概率,并对其求和,可以证明期望时间是常数。这个概率的计算涉及到区间内哈希键数量的尾部分布,此时就需要用到k-独立性来应用矩方法或切尔诺夫界进行估计。例如,可以证明在7-独立哈希函数下,长度为k的区间是满的概率为O(1/k³),从而保证期望查询时间为常数。

总结

本节课我们一起学习了哈希技术的多个核心方面。我们从负载均衡的“球与箱”模型出发,引入了切尔诺夫界作为分析工具。接着,我们探讨了k-独立性这一重要概念,它允许我们在节省空间的同时获得强大的概率保证。然后,我们研究了哈希在解决字典问题中的应用,详细介绍了链式哈希和线性探测两种方案。特别是线性探测,我们了解到其性能保证对哈希函数的独立性有特定要求(至少需要5-独立性),并初步探讨了其分析思路,即通过分析“满区间”的概率来界定操作时间。这些内容为理解现代算法中高效数据结构的概率基础提供了重要的视角。

004:哈希专题

在本节课中,我们将要学习哈希算法的几个高级主题。我们将完成对线性探测的分析,探讨近似成员查询的数据结构(如布隆过滤器),并介绍布谷鸟哈希和蓝莓过滤器。课程内容将侧重于核心概念的分析与证明,旨在让初学者能够理解。


线性探测与五向独立性

上一节我们介绍了使用七向独立哈希函数分析线性探测的方法。本节中我们来看看如何使用五向独立性达到同样的效果。

核心引理:在插入元素 x 时,如果进行了 k 次探测,那么哈希值 h(x) 至少被 k 个不同的“满”区间所包含,每个区间的长度至少为 k

证明思路:考虑哈希数组。h(x) 的位置被占用后,会向前或向后寻找空位。每次探测都对应一个以 h(x) 为起点的、长度递增的区间。如果进行了 k 次探测,那么这 k 个区间都是“满”的(即区间内元素数量至少为区间长度的一半)。

为了将分析从七向独立性降至五向,关键技巧是避免对每个长度 k 的区间进行朴素的全联合界。我们可以构建一个覆盖哈希数组的完美二叉树。

以下是分析步骤:

  1. 在二叉树上,定义一个高度为 h 的节点是“危险”的,如果落入其对应区间的元素数量至少为 (3/4) * 2^h
  2. 对于任何包含 h(x) 的长度为 k 的“满”区间,我们可以用常数个(例如4到9个)长度约为 k/8 的树节点区间来覆盖它。
  3. 如果该“满”区间存在,那么这常数个节点区间中至少有一个是“危险”的。
  4. 因此,存在“满”区间的概率,被控制在存在“危险”节点区间的概率之内。
  5. 利用五向独立性,可以计算“危险”事件的概率为 O(1/k^2)
  6. 对所有 k 求和(Σ 1/k^2)是收敛的,从而证明期望探测次数为常数。

这个论证的关键在于,通过树结构将联合界中的 k 个区间减少为常数个,从而允许我们使用更低阶的矩(四阶矩)进行分析,而五向独立性恰好能满足四阶矩分析的要求。


近似成员查询:布隆过滤器

上一节我们讨论了精确的字典问题。本节中我们来看看一个允许误差的变体:近似成员查询,其经典解决方案是布隆过滤器。

问题定义:维护一个集合 S,支持插入操作,并回答查询“元素 x 是否在 S 中?”。允许假阳性(即 x ∉ S 但查询返回“是”),但不允许假阴性。目标是使用远小于存储所有键所需的空间(约 n 比特)。

基础方案(单哈希函数)

  • 初始化一个长度为 m 的比特数组 A,所有位为0。
  • 选择一个哈希函数 h: U -> [m]
  • 插入(x):设置 A[h(x)] = 1
  • 查询(x):返回 A[h(x)]

分析

  • x ∈ S,则 A[h(x)] 肯定为1,查询正确。
  • x ∉ S,查询出错(假阳性)当且仅当存在某个 y ∈ S 使得 h(x) = h(y)
  • 通过联合界,假阳性概率 ≤ n / m。若设 m = 2n,则概率 ≤ 1/2。

降低误差(标准布隆过滤器)
为了将假阳性概率降至 ε,可以采用以下两种等价方法:

  1. 增大数组:使用单个哈希函数,但将数组长度 m 设为 n / ε。空间为 O(n/ε) 比特。
  2. 多个数组:使用 k = log₂(1/ε) 个独立的哈希函数 h₁, h₂, ..., hₖ 和对应的比特数组。
    • 插入(x):对所有 i ∈ [1, k],设置 A_i[h_i(x)] = 1
    • 查询(x):当且仅当所有 A_i[h_i(x)] 均为1时,返回“是”。
    • 此时,假阳性概率约为 (n/m)^k。若设 m = 2n,则概率约为 2^{-k} = ε
    • 总空间为 O(n log(1/ε)) 比特,查询时间为 O(log(1/ε))

哈希函数要求:分析仅依赖于任意两个元素哈希值的独立性,因此使用两向独立哈希函数族即可。


蓝莓过滤器与布谷鸟哈希

布隆过滤器解决了成员查询问题。本节中我们来看看一个相关的“检索问题”,其解决方案是蓝莓过滤器,而它的构建依赖于布谷鸟哈希。

检索问题:对于一个静态集合 S 中的每个元素 x,都关联一个 r 比特的字符串 value(x)。需要构建一个数据结构,当查询 x ∈ S 时,返回正确的 value(x);当查询 x ∉ S 时,返回值无保证。目标空间约为 n * r 比特(不存储键本身)。

核心工具:布谷鸟哈希
布谷鸟哈希是一种解决动态字典问题的方法,它使用两个哈希函数。

  • 维护一个大小为 m = O(n) 的数组 A
  • 使用两个哈希函数 g, h: U -> [m]
  • 插入(x):尝试将 x 放入 A[g(x)]。如果该位置被元素 y 占用,则“踢走” y,并将 y 重新插入到它的另一个哈希函数指向的位置(即 A[h(y)]A[g(y)])。这个过程可能引发一连串的“踢出”操作。如果链的长度超过 O(log n),则重建整个哈希表(选择新的随机哈希函数)。
  • 可以证明,期望的插入时间是常数

布谷鸟图:分析布谷鸟哈希的关键是构建一个“布谷鸟图”。

  • 图有 m 个顶点,对应数组位置。
  • 对于每个元素 x ∈ S,在顶点 g(x)h(x) 之间添加一条边。
  • 插入 x 时的“踢出”链,对应于在这个无向图中从 g(x)h(x) 出发寻找空位(或寻找一个低冲突的分配方案)的过程。

构建蓝莓过滤器

  1. 为集合 S 运行布谷鸟哈希构建过程。可以证明,在随机哈希函数下,布谷鸟图中不含环的概率至少为 1/2。如果检测到环,则重新选择哈希函数并重建,期望重建次数为2。
  2. 假设我们得到了一个无环的布谷鸟图,即一个森林。将森林中的每棵树任意定根。
  3. 每个元素 x 对应一条边 (g(x), h(x))。我们规定 x “拥有”这条边中离根更远的那个子节点。
  4. 现在,每个数组位置(图顶点)将存储一个 r 比特的字符串。我们从根向叶子填充这些值:
    • 将所有根节点的值初始化为0。
    • 对于每个非根节点 v,设其父节点为 p,拥有 v 的元素为 x。我们希望查询 x 时,A[g(x)] XOR A[h(x)] = value(x)。由于 p 的值已知(已填充),我们可以解出 A[v] 应设置的值:A[v] = A[p] XOR value(x)
  5. 查询(x):返回 A[g(x)] XOR A[h(x)]
    • x ∈ S,根据构造,返回值等于 value(x)
    • x ∉ S,返回值是某个无意义的字符串。

此结构仅存储了 m * r 比特(m = O(n)),即 O(n * r) 比特,完美解决了静态检索问题。动态版本和更优的实现则需要更复杂的技术。


总结

本节课中我们一起学习了哈希算法的多个进阶主题。

  1. 我们深入分析了线性探测,并了解了如何利用五向独立性和巧妙的树状覆盖论证,来证明其常数期望性能。
  2. 我们探讨了近似成员查询问题,介绍了经典的布隆过滤器,理解了其通过允许假阳性来换取空间效率的核心思想,以及其简单的构造与分析方法。
  3. 我们介绍了布谷鸟哈希,它利用两个哈希函数和元素的“踢出”机制来实现高效的动态字典,其分析依赖于布谷鸟图的性质。
  4. 最后,我们看到了如何将无环的布谷鸟图应用于解决静态检索问题,从而构造出空间高效的蓝莓过滤器。

这些数据结构展示了哈希函数在算法设计中的强大能力,以及通过概率分析和随机化来权衡时间、空间与准确性的精妙之处。

005:布谷鸟哈希与双选择哈希分析 🐦

在本节课中,我们将完成布谷鸟哈希的分析,并介绍一种称为“双选择哈希”的负载均衡技术。我们将看到如何通过使用两个哈希函数来显著改善哈希表的最坏情况性能。


布谷鸟哈希分析回顾

上一节我们介绍了布谷鸟哈希的基本工作原理。本节中,我们来分析其插入操作的期望时间复杂度。

布谷鸟哈希使用一个大小为 m 的数组 A 和两个随机哈希函数 gh。插入元素 x 时,我们尝试将其放入 A[g(x)]A[h(x)]。如果两个位置都被占用,算法会“踢出”其中一个现有元素,并将其重新插入到它的另一个哈希位置,这个过程可能引发一连串的“踢出”操作。

如果插入过程中的“踢出”链长度超过 C * log n 步(C 是一个常数),我们就放弃,选择新的哈希函数 gh,并重建整个哈希表。

我们的目标是证明,插入操作的期望时间是常数 O(1)

插入过程中的可能情况

以下是插入元素 x 时,在布谷鸟图中可能遇到的几种情况:

  1. 路径:插入过程沿着一条路径进行,最终找到一个空位。
  2. 单循环:插入过程进入一个循环,但最终能完成插入。
  3. 双循环:插入过程陷入两个循环,可能导致无限循环。算法通过设置步数上限 C * log n 来避免这种情况。

期望时间分析

我们定义以下随机变量来分析插入时间 T

  • P_k:指示是否存在长度至少为 k 的路径。
  • C_k:指示是否存在长度至少为 k 的单循环配置。
  • D:指示是否存在导致无限循环的双循环配置。

插入的期望时间可以分解为:

E[T] ≤ Σ_k E[P_k] + Σ_k E[C_k] + Pr[D=1] * n * E[T] + Pr[步骤数 > C log n] * n * E[T]

我们希望证明后两项的概率非常小(例如 O(1/n^2)),这样它们对期望时间的贡献可以忽略不计。

1. 分析长路径的概率 E[P_k]

考虑一条特定的长度为 k 的路径。它涉及 k+1 个顶点(哈希值)和 k 条边(元素)。对于每个元素,其两个哈希值匹配路径上指定位置的概率至多为 2 / m^2。通过联合界对所有可能的路径配置进行求和,我们可以得到:

E[P_k] ≤ (2n / m)^k

当选择 m = 4n 时,上式变为 (1/2)^k。这是一个几何级数,其和为常数。

2. 分析长单循环的概率 E[C_k]

一个单循环配置可以看作由三条路径组成。其中最长的一条路径至少包含 k/3 条边。因此,其概率上界可以通过类似路径分析的方法得到,结果也是一个几何级数。

3. 分析双循环的概率 Pr[D=1]

双循环配置涉及 k-1 个顶点和 k 条边。通过类似的计数和联合界分析,我们可以证明 Pr[D=1]O(1/n^2) 量级。因此,Pr[D=1] * n * E[T] 项对总期望时间的贡献是常数。

4. 分析超长步骤的概率

如果插入步骤超过 C log n,要么发生了双循环 D(概率已证明很小),要么存在长度至少为 C log n 的路径或单循环 P_{C log n}C_{C log n}。通过选择足够大的常数 C,可以使 P_{C log n} 的概率小至 O(1/n^2)

综上所述,布谷鸟哈希插入操作的期望时间是常数 O(1)


负载均衡:双选择哈希的力量 ⚖️

现在,我们离开布谷鸟哈希,看看使用两个哈希函数的另一个巧妙应用——负载均衡。

背景:链式哈希

回顾链式哈希,我们有 n 个元素和 n 个桶。每个元素通过一个随机哈希函数 h 映射到一个桶,冲突的元素在桶内形成链表。

  • 使用完全随机哈希函数时,最长链的长度约为 Θ(log n / log log n)
  • 使用 2-独立哈希函数时,期望查询时间是常数,但最坏情况链长仍可能为 Θ(log n / log log n)

我们希望同时获得良好的期望性能和最坏情况性能。

双选择哈希算法

双选择哈希(Azar, Broder, Karlin, Upfal, STOC '99)的算法如下:

  1. 选择两个独立的随机哈希函数 gh
  2. 插入元素 x:检查桶 g(x) 和桶 h(x) 的当前负载(链表长度),将 x 插入负载较轻的那个桶中。
  3. 查询元素 x:需要同时检查桶 g(x) 和桶 h(x) 内的链表。

直观上,通过选择两个桶中更空的那个,负载分布应该更加均匀。

性能分析

研究表明,使用双选择哈希后,最重桶中的元素数量高概率地不超过 log log n / log 2 + Θ(1)。这相比普通哈希的 Θ(log n / log log n) 是指数级的改进!

扩展到 D 个选择

一个自然的问题是:如果使用 d 个哈希函数,并插入到 d 个桶中最空的那个,性能会如何?
结果是,最重桶的负载变为 log log n / log d + Θ(1)。这仅仅是将对数的底数从 2 改为 d,是常数因子的改进,而非指数级。

一个更聪明的变体:Vöcking 的方法

Vöcking 提出了一个改进方案:

  1. n 个桶分成 d 组。
  2. 插入元素时,从每组中随机选择一个桶,得到 d 个候选桶。
  3. 插入到这 d 个桶中负载最轻的那个;如果多个桶负载相同且最轻,则选择最左边的组中的桶。
    这种方法能得到更优的界:最重桶的负载约为 log log n / (d * log φ_d),其中 φ_d 是一个常数。当 d=2 时,这等价于大约 log log n / (2 * log φ),比简单的双选择更好。

双选择哈希的直观分析

为什么双选择哈希能产生 log log n 的负载?以下是一个非严格的直观解释。

定义 B_i 为负载至少为 i 的桶的数量。考虑一个元素 x 被插入后,其所在桶的负载(即“高度”)至少为 i+1 的概率。这要求 x 的两个候选桶 g(x)h(x) 的负载都至少为 i。假设哈希函数独立,且负载分布均匀,则:

Pr[height(x) ≥ i+1] ≈ (B_i / n)^2

那么,负载至少为 i+1 的桶的数量的期望 E[B_{i+1}] 满足:

E[B_{i+1}] ≤ n * (B_i / n)^2 = B_i^2 / n

这蕴含了递推关系 B_{i+1}/n ≤ (B_i/n)^2。假设在某个级别 i,比例 B_i/n 小于 1/2,那么它将按照平方级的速度迅速衰减:

B_{i+j} / n ≤ (1/2)^{2^j}

(1/2)^{2^j} < 1/n,即 j ≈ log log n 时,B_{i+j} 将小于 1,这意味着不存在负载那么高的桶。因此,最大负载约为 i + log log n。通过仔细分析,可以确定 i 是一个小常数,从而得到 O(log log n) 的界。

要使这个论证变得严格,需要使用切尔诺夫界等工具,并处理随机变量之间的依赖性,但核心思想就是上述的平方衰减过程。


总结

本节课中我们一起学习了:

  1. 布谷鸟哈希的完整分析:我们证明了通过使用两个哈希函数和重建机制,布谷鸟哈希能在常数期望时间内完成插入操作。
  2. 双选择哈希:我们看到了在负载均衡问题中,简单地使用两个哈希函数并选择负载较轻的桶,就能将最坏情况负载从 Θ(log n / log log n) 指数级地降低到 Θ(log log n)。这展示了多重哈希函数的强大能力。

从下一讲开始,我们将离开哈希的领域,进入新的主题——数据结构与摊还分析,首先从斐波那契堆开始。

006:摊还分析与斐波那契堆 🧮

在本节课中,我们将学习摊还分析的概念,并深入研究两种重要的数据结构:二项堆和斐波那契堆。我们将了解如何通过“懒惰”策略和势能函数方法来证明操作的摊还时间复杂度。

摊还分析概述

摊还分析用于分析一系列操作的平均性能,即使其中某些单个操作可能代价高昂。其核心思想是,任何包含 NA 次 A 操作、NB 次 B 操作等的操作序列,其总时间最多为 NA * TA + NB * TB + ...,其中 TATB 等是各操作的摊还成本。

一种常见的证明摊还界限的方法是势能函数法

势能函数法

我们定义一个势能函数 Φ,它将数据结构的状态映射到非负实数。对于一个空的数据结构,其势能 Φ 为 0。

假设我们执行了 K 个操作,每个操作的实际运行时间为 T1, T2, ..., TK。操作前后数据结构的势能分别为 S0, S1, ..., SK

我们定义第 i 个操作的摊还成本为:实际时间 Ti + (Φ(Si) - Φ(Si-1)),即实际成本加上势能的变化。

将所有操作的摊还成本相加,总摊还成本为:
Σ Ti + (Φ(SK) - Φ(S0))
由于 Φ(S0) = 0Φ(SK) ≥ 0,因此总摊还成本至少等于总实际成本。如果我们能证明每个操作的摊还成本有上界,那么整个序列的实际成本也就有了上界。


堆数据结构回顾

堆是一种抽象数据类型,支持以下操作:

  • 插入(Insert X):将元素 X 插入堆中。
  • 删除最小元(DeleteMin):返回并删除堆中的最小元素。
  • 降低关键字(DecreaseKey(P, K)):将指针 P 指向的元素的键值减小为 K(新键值必须不大于原键值)。

堆是许多算法(如 Dijkstra 最短路径算法)的核心组件。

不同堆实现的比较

以下是几种堆实现的性能对比:

  • 二叉堆 (Williams, 1964)
    • 所有操作的最坏情况时间复杂度均为 O(log n)
  • 二项堆 (Vuillemin, 1978)
    • 插入的摊还时间复杂度为 O(1)
    • 删除最小元降低关键字的最坏情况时间复杂度为 O(log n)
  • 斐波那契堆 (Fredman & Tarjan, 1987)
    • 插入降低关键字的摊还时间复杂度为 O(1)
    • 删除最小元的摊还时间复杂度为 O(log n)

斐波那契堆的改进使得 Dijkstra 等算法的运行时间得以优化。


二项堆详解

二项堆将元素组织成一个森林(多个树的集合)。每棵树都满足堆序性质:父节点的键值不大于子节点的键值。

二项树的结构

二项堆中的树称为二项树。一棵秩为 k 的二项树 (Bk) 具有以下递归定义:

  • B0 是只有一个节点的树。
  • Bk 由两棵 Bk-1 树连接而成,其中一棵树的根成为另一棵树根的最左孩子。

因此,Bk 树具有以下性质:

  • 恰好有 2^k 个节点。
  • 树的高度为 k
  • 根的度数为 k
  • 根的第 i 个子树是一棵 Bk-i 树。

在二项堆中,我们维护一个性质:对于任意秩 k,最多只有一棵秩为 k 的二项树

二项堆的操作

以下是二项堆核心操作的实现:

  • 降低关键字(DecreaseKey)

    • 更新节点的键值。
    • 如果违反了堆序性质(新键值小于父节点键值),则将该节点与其父节点交换(上浮)。
    • 重复此过程,直到堆序性质恢复。
  • 插入(Insert)

    • 将新节点作为一个单独的 B0 树加入森林。
    • 这可能会违反“每秩一树”的性质。我们需要通过不断合并相同秩的树来修复,这个过程类似于二进制加法
  • 删除最小元(DeleteMin)

    • 遍历所有树的根节点,找到最小元素。
    • 删除该最小根节点,其所有子节点成为新的树加入森林。
    • 再次合并森林中所有相同秩的树。

二项堆的摊还分析

我们使用势能函数法来分析二项堆插入操作的摊还成本。

我们定义势能函数 Φ(H) = T,其中 T 是二项堆 H 中树的数量。

  • 插入操作分析
    • 实际成本:假设合并过程中,我们合并了 m 棵相同秩的树。实际成本为 O(m+1),其中 +1 代表创建新树。
    • 势能变化:插入前有 T 棵树。插入并合并后,树的数量变为 T' = T - m + 1(因为合并 m 对树会减少 m 棵树,但新增了一棵新树)。因此,势能变化 ΔΦ = T' - T = -m + 1
    • 摊还成本实际成本 + ΔΦ = O(m+1) + (-m+1) = O(1)

因此,二项堆的插入操作具有 O(1) 的摊还时间复杂度。其他操作(删除最小元、降低关键字)的最坏情况时间复杂度为 O(log n),由于树的数量不超过 O(log n),其摊还成本也为 O(log n)


从二项堆到斐波那契堆

斐波那契堆的目标是进一步优化,实现 O(1) 摊还时间的插入降低关键字操作。其核心思想是懒惰操作

懒惰策略

  • 懒惰插入:插入新元素时,仅仅将其作为一棵单节点树加入根链表,不立即进行合并。所有的合并工作推迟到删除最小元操作时进行。
  • 懒惰降低关键字(初步想法):要降低某个节点的键值时,如果它违反了堆序性质,我们直接将其从父节点上“切断”,并将其作为一棵新树提升到根链表。

然而,这种极端的懒惰策略会破坏二项堆中“高秩树拥有大量节点”的性质。经过多次降低关键字操作后,我们可能得到一棵秩很高但节点很少的树,这将导致删除最小元操作变慢。

斐波那契堆的关键改进:级联切断

为了解决上述问题,斐波那契堆引入了一个规则:

  • 每个节点有一个标记(mark) 位,记录它是否已经失去过一个孩子
  • 当一个节点 x 因降低关键字被从其父节点 p 切断时:
    • 如果 p 之前未被标记,则标记 p。这是它第一次失去孩子。
    • 如果 p 之前已被标记,则说明这是它第二次失去孩子。此时,我们将 p 也从它的父节点切断(并清除其标记),此过程可能级联向上进行。

这个规则确保了树的形状不会因降低关键字操作而过度退化。可以证明,在斐波那契堆中,一棵秩为 k 的树,其包含的节点数至少与第 (k+2) 个斐波那契数 F_{k+2} 成正比。由于斐波那契数呈指数增长,这意味着 k = O(log n)

斐波那契堆的摊还分析

我们定义势能函数为:Φ(H) = T(H) + 2 * M(H),其中:

  • T(H) 是堆 H 中树的数量。
  • M(H) 是堆 H 中被标记节点的数量。

以下是各操作的摊还成本分析:

  • 插入(Insert)

    • 实际成本 O(1)
    • 增加了一棵树,标记节点数不变。势能变化 ΔΦ = +1
    • 摊还成本:O(1) + 1 = O(1)
  • 删除最小元(DeleteMin)

    • 实际成本 O(T),其中 T 是删除前根链表中树的数量(需要遍历找最小元,并合并)。
    • 删除后,经过合并,树的数量最多为 O(log n)(因为最高秩为 O(log n))。标记节点数变化很小。
    • 摊还成本:O(T) + O(log n) - T ≈ O(log n)
  • 降低关键字(DecreaseKey)

    • 假设我们执行了 c 次级联切断。
    • 实际成本 O(c)
    • 势能变化分析:
      • 树的数量增加了 c 棵(每切断一个节点产生一棵新树)。
      • 标记位变化:每次切断会清除一个节点的标记(被切断的节点),但可能标记其父节点。在最坏情况下,标记节点数净减少 c
    • 因此,ΔΦ ≤ c - 2c = -c
    • 摊还成本:O(c) + (-c) = O(1)

通过势能函数法,我们证明了斐波那契堆的插入和降低关键字操作具有 O(1) 的摊还时间复杂度,删除最小元操作具有 O(log n) 的摊还时间复杂度。


总结

本节课我们一起学习了摊还分析的核心方法——势能函数法,并深入剖析了二项堆和斐波那契堆。

  • 摊还分析让我们能够关注操作序列的整体性能,通过“银行账户”或“势能”的概念,将昂贵操作的成本分摊到之前的廉价操作上。
  • 二项堆通过维护一个由二项树组成的森林,并利用类似二进制加法的合并策略,实现了 O(1) 摊还时间的插入。
  • 斐波那契堆在二项堆的基础上引入了懒惰操作级联切断规则。通过精心设计的势能函数,我们证明了它能以 O(1) 的摊还成本支持插入和降低关键字操作,这是许多图算法得以加速的关键。

下一讲,我们将探讨另一种著名的摊还数据结构:伸展树(Splay Tree),它将展示如何通过自适应调整来达到近乎最优的搜索性能。

007:数据结构与性质

在本节课中,我们将学习一种名为“伸展树”的自调整二叉搜索树数据结构。我们将了解其基本操作、工作原理,并通过势能分析法证明其高效的平摊性能。此外,我们还将探讨伸展树所具备的一系列强大性质。


伸展树简介

伸展树是一种二叉搜索树。在每次操作后,它会通过一系列旋转操作进行自我调整和重新平衡。这种数据结构由 Sleator 和 Tarjan 于 1985 年提出。

伸展树支持以下操作:

  • 插入:插入一个带有关键字 K 和值 V 的项。
  • 查找:根据关键字 K 返回其关联的值。
  • 删除:删除关键字为 K 的项。

所有操作都在二叉搜索树模型中进行。在该模型中,我们有一个指向树根的指针,每一步可以执行以下操作之一:

  • 跟随指向子节点或父节点的指针。
  • 对当前指针指向的节点进行旋转。

旋转操作是重新平衡数据结构的关键。下图展示了以节点 X 为中心的旋转操作:

旋转前:
        Y
       / \
      X   C
     / \
    A   B

旋转后(X 成为新的局部根):
        X
       / \
      A   Y
         / \
        B   C

伸展操作

伸展树的核心是 伸展 操作。在执行查找或插入操作后,都会对目标节点 X 执行伸展操作,其目的是通过一系列旋转将 X 移动到树的根部。

伸展操作根据节点 X、其父节点 Y 和祖父节点 Z 的位置关系,分为三种情况处理。以下是每种情况的操作步骤:

情况 1:Y 是根节点

  • 只需对 X 执行一次单旋转。

情况 2:XY 同为左孩子或同为右孩子(之字形)

  • 先对 Y 旋转,再对 X 旋转。

情况 3:XY 分别为左孩子和右孩子,或分别为右孩子和左孩子(一字形)

  • X 连续执行两次旋转。

伸展操作会重复应用上述规则,直到 X 成为树的根节点。


伸展树的强大性质

伸展树不仅在最坏情况下能保证 O(log n) 的平摊时间复杂度,还具备一系列令人惊叹的性质,使其性能远超普通的平衡二叉搜索树。

以下是伸展树的一些关键性质:

静态最优性
对于任何固定的二叉搜索树 T 和任何操作序列,伸展树执行该序列的总时间最多是 T 执行时间的常数倍。这意味着,即使伸展树不知道未来的操作序列,其性能也能与针对该序列静态优化的最佳树相媲美。

工作集性质
T_j 为自上次访问第 j 个项以来,所访问的不同项的数量。那么,伸展树执行 M 次操作的总时间为 O(M + N log N + Σ log T_j)。这意味着,频繁访问的项(工作集)会位于树的顶部附近,访问成本接近常数。

静态手指性质
F 为任意固定项。对于任何查找操作 i,其平摊成本为 O(log |F - i| + 1)。这意味着,如果大多数查找操作都集中在某个固定项 F 附近,那么成本会很低。

动态手指性质
设操作序列为 x1, x2, ..., xM。那么第 i 次操作的平摊成本为 O(log |xi - xi-1| + 1)。这可以理解为“手指”总是移动到上次访问的项,并测量与当前项的距离。该性质的证明非常复杂,长达上百页。

动态最优性猜想
这是关于伸展树最著名且尚未被证明的猜想。它断言,对于任何操作序列 σ,伸展树的执行成本最多是最优二叉搜索树算法执行成本的常数倍。即,存在一个常数 C,使得 Splay_Cost(σ) ≤ C * OPT(σ)。目前已知的最佳结果(如 Tango 树)是 O(log log n) * OPT(σ)


平摊分析:势能法

我们将使用势能法来分析伸展树的平摊性能。首先定义一些概念:

  • 为每个节点 x 分配一个权重 w(x)
  • 定义 S(x) 为以 x 为根的子树中所有节点权重之和(包括 x 自身)。
  • 定义节点 x 的秩 r(x) = log S(x)
  • 定义整个数据结构的势能 Φ 为所有节点秩的总和:Φ = Σ r(x)

我们的目标是证明,对节点 x 执行一次伸展操作的平摊成本为 O(log (W / S(x)) + 1),其中 W 是所有节点的总权重。

分析的关键在于分别考虑伸展操作中每种情况(之字形、一字形、单旋转)对势能变化的影响。通过数学推导(特别是利用算术-几何平均不等式),可以证明每种基本操作的平摊成本都能被有效地限制。

最终,当我们将伸展过程中所有步骤的平摊成本相加时,中间项会相互抵消(望远镜求和),只剩下根节点的秩与初始节点 x 的秩之差,从而得到上述结论。


应用分析:证明静态最优性

通过巧妙设置权重函数,我们可以利用上述平摊分析框架来证明伸展树的各种性质。以下以静态最优性的一个简化版本为例:

假设我们有一个固定的最优树 T,其中项 i 位于深度 L_i。设项 i 被访问了 m_i 次。在树 T 中,服务该访问序列的总成本为 O(M + Σ m_i L_i),其中 M 是总操作数。

为了证明伸展树也能达到类似的性能,我们为每个节点 i 设置权重:w(i) = 1 / 3^{L_i}

分析步骤:

  1. 总实际时间实际时间 = 平摊时间 + Φ_初始 - Φ_最终
  2. 势能范围:由于权重按 3^{-L} 设置,最坏情况下节点的秩约为 -L。在固定树 T 中,深度 L 的节点最多有 2^L 个。因此,初始势能 Φ_初始 和最终势能 Φ_最终 的差值最多为 O(n^2)(通过更精细的权重设置可优化为 O(n log n))。
  3. 单次查找的平摊成本:根据之前的定理,查找项 i 的平摊成本为 O(1 + log (W / S(i)))
    • 总权重 W 是一个常数(因为 Σ_{L} (2^L / 3^L) 收敛)。
    • 节点 i 的子树权重 S(i) 至少为其自身权重 w(i) = 1 / 3^{L_i}
    • 因此,平摊成本为 O(1 + log (常数 / (1/3^{L_i}))) = O(1 + L_i)
  4. 总平摊时间:所有操作的总平摊时间为 O(Σ (1 + L_i) * m_i) = O(M + Σ m_i L_i)
  5. 结合势能差:总实际时间为 O(M + Σ m_i L_i + n^2)。当操作序列足够长时(M 主导 n^2),伸展树的性能与最优树 T 同阶。

这就展示了如何通过选择合适的权重函数,来证明伸展树具备静态最优性。类似的方法可用于证明其工作集性质等其他特性。


总结

本节课我们一起学习了伸展树这一优雅而强大的自调整二叉搜索树数据结构。我们了解了其基本操作——伸展,并通过势能法分析了其 O(log n) 的平摊性能。更重要的是,我们探讨了伸展树所具备的一系列卓越性质,如静态最优性、工作集性质和动态手指性质,这些性质使其在理论上近乎最优。最后,我们通过设置权重函数,演示了如何利用平摊分析框架来证明这些性质。伸展树因其简单高效的实现和强大的理论保证,在实践中被广泛应用。

008:在线算法 🧠

在本节课中,我们将学习在线算法的基本概念。在线算法需要在面对未知未来请求序列的情况下,实时做出决策。我们将通过竞争分析来评估这些算法的性能,即比较在线算法的成本与一个知晓未来的最优离线算法的成本。我们将首先通过几个简单的例子来理解这个概念,然后深入研究列表更新和分页这两个经典问题。


列表更新问题 📝

上一节我们介绍了在线算法的基本框架,本节中我们来看看一个具体的应用:列表更新问题。

列表更新问题涉及一个包含 n 个项目的链表。有三种操作:

  • 访问(X):访问项目 X
  • 插入(X):将项目 X 插入到链表末尾。
  • 删除(X):从链表中删除项目 X

访问链表中第 i 个项目的成本是 i。在访问一个项目后,算法可以免费将其移动到链表中更靠前的位置。我们的目标是设计一个在线算法,使其总访问成本尽可能低。

在 Sleator 和 Tarjan 的论文之前,人们分析了许多启发式方法。以下是其中几种:

以下是几种常见的启发式方法:

  • 移至前端 (MF):总是将被访问的项目移动到链表的最前端。
  • 交换 (Transpose):将被访问的项目与其前一个项目交换位置。
  • 频率计数 (Frequency Count):根据项目的历史访问频率,将项目按频率从高到低排序。

Sleator 和 Tarjan 证明了 移至前端 算法具有动态最优性。具体来说,对于任何访问序列 S,任何算法 A(包括知晓未来的最优算法),如果 AMF 的初始链表顺序相同,那么 MF 的总成本最多是 A 成本的两倍,再减去一些项。

定理公式
cost_MF(S) ≤ 2 * cost_A(S) + paid_exchanges_A - free_exchanges_A - M
其中 M 是操作序列的长度。

这个定理的证明使用了势能分析法。定义势函数 ΦMF 的链表顺序相对于 A 的链表顺序的逆序对数量。通过分析每次操作的实际成本和势能变化,可以证明每次访问的摊还成本最多是 2i - 1(其中 i 是项目在 A 链表中的位置),从而证明了定理。


分页问题 💾

上一节我们讨论了列表更新问题及其最优算法,本节中我们来看看一个密切相关但具有不同成本函数的问题:分页问题。

分页问题模拟了计算机中的缓存管理。我们有一个快速的缓存(内存),其容量为 k 个页面,以及一个慢速的后备存储(如磁盘)。系统接收到一系列对页面的请求。如果请求的页面在缓存中(命中),则成本为 0;如果不在缓存中(缺页),则需要从磁盘将其载入缓存,成本为 1。如果缓存已满,则必须选择一个页面将其驱逐回磁盘。

这可以看作是列表更新问题的一个特例,其成本函数 f(i) 为:
f(i) = 0,如果 i ≤ k(页面在缓存中)
f(i) = 1,如果 i > k(页面在磁盘中)

此外,在发生缺页时,我们必须将新页面移动到缓存中(即列表的前 k 个位置之一)。

常见的缓存替换策略(启发式算法)包括:

  • 最近最少使用 (LRU):驱逐最久未被访问的页面。这对应于列表更新中的“移至前端”策略。
  • 先进先出 (FIFO):驱逐最早进入缓存的页面。
  • 最不经常使用 (LFU):驱逐历史访问频率最低的页面。

Sleator 和 Tarjan 证明了 LRUFIFO 都是 k-competitive 的,即对于任何请求序列,它们的成本最多是最优离线算法成本的 k 倍。同时,他们也证明了一个下界:任何确定性在线算法的竞争比至少为 k

下界证明思路:假设共有 k+1 个不同的页面。对手可以构造一个请求序列,总是请求当前不在在线算法缓存中的那个页面,迫使在线算法每次请求都发生缺页。而最优离线算法通过使用“最远将来”策略(总是驱逐在最远未来才会被再次请求的页面),可以保证每 k 次请求才发生一次缺页。因此,在线算法的成本至少是最优算法的 k 倍。

为了证明 LRU 的 k-竞争性,我们分析一个称为 1-bit LRU标记算法 的简化版本:

  1. 初始时,所有页面都未标记
  2. 当访问一个页面时,将其标记
  3. 当需要驱逐一个页面时,任意选择一个未标记的页面驱逐。
  4. 如果没有未标记的页面,则清除所有页面的标记,然后重新开始。

我们可以将请求序列划分为多个阶段,每个阶段从清除所有标记开始,到下一次需要清除所有标记时结束。分析表明,在每个阶段内,LRU 恰好发生 k 次缺页,而最优算法至少发生 1 次缺页。因此,LRU 是 k-competitive 的。


超越下界:改进策略 🚀

上一节我们看到确定性分页算法的竞争比下界是 k,这对于大缓存来说并不理想。本节中我们来看看几种绕过这个下界的方法。

以下是几种改进策略:

  1. 资源增强 (Resource Augmentation):比较在线算法(拥有 k 个缓存槽)和最优离线算法(只拥有 k' 个缓存槽,其中 k' < k)的性能。Sleator 和 Tarjan 证明,在这种情况下,LRU 和 FIFO 的竞争比可以降低到 k'/(k - k' + 1)

  2. 随机化 (Randomization):通过让算法做出随机决策,可以对抗那些不知道未来随机结果的对手。我们需要根据对手的能力来定义不同的对手模型:

    • 全知对手 (Omniscient Adversary):知道所有信息,包括算法未来的随机数。随机化无帮助。
    • 自适应对手 (Adaptive Adversary):知道过去的所有信息(包括已抛出的随机数),并据此选择接下来的请求。确定性下界仍然适用。
    • ** oblivious 对手 (Oblivious Adversary)**:在算法开始运行前就固定了整个请求序列,不知道算法运行时的随机结果。随机化算法可以对此类对手取得更好的竞争比。

对于 oblivious 对手,Fiat 等人提出了一种 标记算法 (Marking Algorithm),它是 1-bit LRU 的随机化版本:当需要驱逐时,随机选择一个未标记的页面进行驱逐。他们证明了这个算法的期望竞争比是 2H_k,其中 H_k 是第 k 个调和数(H_k ≈ ln k)。同时,他们也证明了任何随机化算法对 oblivious 对手的竞争比至少是 Ω(log k)

证明思路:将序列划分为阶段。在每个阶段内,分析“干净”页面(在本阶段和上一阶段都未被访问过)的数量。可以证明,如果在一个阶段内有 l 次对干净页面的访问,那么最优算法至少需要付出 l/2 的成本,而随机标记算法的期望成本最多为 2l H_k。通过跨阶段求和,即可得到竞争比。


总结 📚

本节课中我们一起学习了在线算法和竞争分析的基本概念。我们通过“滑雪租赁”和“寻找免费披萨”两个趣味例子理解了在线决策的本质。然后,我们深入研究了列表更新问题,并证明了移至前端算法具有动态最优性。接着,我们探讨了分页问题,分析了 LRU、FIFO 等确定性算法的 k-竞争性及其下界。最后,我们讨论了如何通过资源增强随机化(特别是针对 oblivious 对手的标记算法)来突破确定性下界,获得更好的理论保证。这些概念为理解现实中的缓存管理、资源分配等在线问题奠定了坚实的基础。

009:随机分页与在线原始对偶框架 🎯

在本节课中,我们将学习随机分页算法(Mark算法)的详细分析,并介绍一个用于分析和设计在线算法的通用框架——在线原始对偶框架。我们将通过滑雪租赁问题作为示例,展示如何应用该框架,并探讨如何通过随机化算法获得更好的竞争比。


随机分页的Mark算法分析 📊

上一节我们介绍了分页问题的背景,本节中我们来看看Mark算法的具体分析。Mark算法是一种随机化分页算法,其竞争比为 ( O(\log k) ),其中 ( k ) 是缓存的大小。

算法描述

Mark算法是“一位LRU”算法的一种实现。其工作流程如下:

  1. 初始时,所有页面均被标记。
  2. 当需要将页面 ( p ) 载入缓存时,从所有未标记的页面中均匀随机选择一个进行驱逐。
  3. 然后标记页面 ( p )。
  4. 如果所有页面都已标记,则取消所有页面的标记。

分析框架

我们将时间划分为多个阶段进行分析。一个阶段开始于我们取消所有页面的标记,结束于所有页面再次被标记且即将请求一个不在缓存中的页面。

以下是分析中使用的关键定义:

  • 干净页面:在上一阶段未被请求,且在当前阶段尚未被请求的页面。
  • 陈旧页面:在上一阶段被请求,但在当前阶段尚未被请求的页面。

我们引入以下变量:

  • ( l_i ):第 ( i ) 阶段请求的干净页面数量。
  • ( s_i ):第 ( i ) 阶段请求的陈旧页面数量,满足 ( s_i = k - l_i )。
  • ( d_i ):第 ( i ) 阶段开始时,OPT缓存中不在Mark缓存中的页面数量。
  • ( d_i' ):第 ( i ) 阶段结束时,OPT缓存中不在Mark缓存中的页面数量,即 ( d_{i+1} )。

竞争比证明

我们将证明以下两个关键不等式:

  1. OPT在第 ( i ) 阶段的代价至少为 ( \max(l_i - d_i, d_i') ),这至少是 ( \frac{l_i - d_i + d_i'}{2} )。
  2. Mark算法在第 ( i ) 阶段的期望代价至多为 ( H_k \cdot l_i ),其中 ( H_k ) 是第 ( k ) 个调和数。

综合这两个不等式,我们可以得到Mark算法的竞争比为 ( 2H_k ),即 ( O(\log k) )。

下界证明

任何随机化分页算法的竞争比下界为 ( \Omega(\log k) )。证明思路如下:

  • 考虑一个包含 ( k+1 ) 个页面的宇宙,请求序列是均匀随机生成的。
  • 任何在线算法的期望代价约为 ( \frac{m}{k+1} ),其中 ( m ) 是序列长度。
  • 而OPT(使用未来最远页面驱逐策略)的期望代价约为 ( \frac{m}{k \log k} )。
  • 因此,竞争比至少为 ( \Omega(\log k) )。

在线原始对偶框架概述 🔄

上一节我们完成了Mark算法的分析,本节中我们来看看在线原始对偶框架。该框架由Buchbinder和Naor在2005年提出,用于设计和分析在线算法。

线性规划回顾

在线原始对偶框架基于线性规划(LP)。一个标准形式的线性规划如下:
[
\text{最小化 } c^T x \quad \text{满足} \quad Ax \geq b, \quad x \geq 0
]
其中 ( A, b, c ) 的所有元素均非负时,称为覆盖线性规划。其对偶问题为:
[
\text{最大化 } b^T y \quad \text{满足} \quad A^T y \leq c, \quad y \geq 0
]
这称为包装线性规划。弱对偶定理指出,原始问题的最优值至少等于对偶问题的最优值。

在线设置

在在线设置中,约束 ( Ax \geq b ) 的每一行(即每个约束)按顺序到达。算法必须在线维护一个可行的原始解 ( x ),且 ( x ) 的分量只能单调非减(即决策不可撤销)。同时,算法也会维护一个对偶解 ( y )。

近似互补松弛条件

为了评估在线算法的性能,我们使用近似互补松弛条件。假设 ( x ) 和 ( y ) 分别是原始和对偶问题的可行解,且存在常数 ( \alpha, \beta \geq 1 ) 使得:

  • 若 ( x_j > 0 ),则 ( A_j^T y \geq c_j / \alpha )。
  • 若 ( y_i > 0 ),则 ( A_i x \leq \beta \cdot b_i )。
    那么有 ( c^T x \leq \alpha \beta \cdot b^T y )。由于 ( b^T y \leq \text{OPT}{\text{dual}} \leq \text{OPT}{\text{primal}} \leq c^T x ),因此 ( c^T x \leq \alpha \beta \cdot \text{OPT}_{\text{primal}} )。这意味着在线算法的代价最多是最优解的 ( \alpha \beta ) 倍。

应用:滑雪租赁问题 🎿

上一节我们介绍了在线原始对偶框架,本节中我们以滑雪租赁问题为例,展示如何应用该框架获得2-竞争算法。

问题建模

假设滑雪租赁持续 ( k ) 天,每日租金为1美元,购买价格为 ( B ) 美元。我们定义变量:

  • ( x \in {0,1} ):是否购买。
  • ( z_i \in {0,1} ):第 ( i ) 天是否租赁。

原始线性规划如下:
[
\text{最小化 } Bx + \sum_{i=1}^k z_i \quad \text{满足} \quad x + z_i \geq 1 \quad (\forall i), \quad x, z_i \geq 0
]
其对偶问题为:
[
\text{最大化 } \sum_{i=1}^k y_i \quad \text{满足} \quad \sum_{i=1}^k y_i \leq B, \quad 0 \leq y_i \leq 1 \quad (\forall i)
]

原始对偶算法

以下是获得2-竞争比的原始对偶算法步骤:

  1. 初始化 ( x = 0 ), ( z_i = 0 ), ( y_i = 0 )。
  2. 当第 ( i ) 天的约束 ( x + z_i \geq 1 ) 到达时:
    • 如果约束已满足,则不做任何操作。
    • 否则,增加 ( y_i ) 直到某个对偶约束变紧(即 ( y_i = 1 ) 或 ( \sum y_i = B ))。
    • 然后,将对应的原始变量设置为1(如果 ( y_i = 1 ) 则设 ( z_i = 1 );如果 ( \sum y_i = B ) 则设 ( x = 1 ))。

竞争比分析

算法结束时,我们得到一对可行的原始解和对偶解。检查近似互补松弛条件:

  • 若 ( x > 0 ) 或 ( z_i > 0 ),则对应的对偶约束是紧的,即 ( \alpha = 1 )。
  • 若 ( y_i > 0 ),则原始约束 ( x + z_i \geq 1 ) 成立,且由于算法设置,有 ( x + z_i \leq 2 ),即 ( \beta = 2 )。
    因此,竞争比 ( \alpha \beta = 2 )。

随机化改进与在线集合覆盖 🌐

上一节我们通过滑雪租赁问题展示了原始对偶框架的应用,本节中我们简要探讨如何通过随机化获得更好的竞争比,并介绍在线集合覆盖问题。

随机化滑雪租赁

通过随机化算法,滑雪租赁问题的竞争比可以改进至 ( \frac{e}{e-1} \approx 1.582 )。该结果在原始对偶框架下自然得出,具体算法将在下一节课详细展开。

在线集合覆盖问题

在线集合覆盖问题描述如下:

  • 有 ( m ) 个集合,初始时仅知道集合名称,不知道包含哪些元素。
  • 在线过程中,元素逐个到达。当元素到达时,必须确保它被某个已选择的集合覆盖。
  • 如果当前没有集合覆盖该元素,则必须选择一个集合加入解决方案。
  • 目标是最小化选择的集合总数,并与离线最优解比较。

在线集合覆盖同样可以用原始对偶框架进行分析,并能获得良好的竞争比。


总结 📝

本节课中我们一起学习了随机分页算法Mark的详细分析,证明了其 ( O(\log k) ) 竞争比。随后,我们介绍了在线原始对偶框架,这是一个强大的工具,用于设计和分析在线算法。通过滑雪租赁问题,我们展示了如何应用该框架获得2-竞争算法,并提及了通过随机化可将竞争比改进至 ( \frac{e}{e-1} )。最后,我们简要介绍了在线集合覆盖问题,为后续学习奠定了基础。

010:原始对偶方法在线算法与近似算法入门

在本节课中,我们将学习如何运用原始对偶方法设计优秀的在线算法,并初步了解其在近似算法领域的应用。我们将通过两个经典问题——滑雪租赁和集合覆盖——来具体阐述这一方法。

滑雪租赁问题的随机化算法

上一节我们介绍了原始对偶方法的基本框架。本节中,我们来看看如何将其应用于滑雪租赁问题,并得到一个随机化的竞争比接近 e/(e-1) 的算法。

问题模型与线性规划

在滑雪租赁问题中,租赁成本为1美元,购买成本为B美元。我们不知道总滑雪天数K。其对应的覆盖型线性规划如下:

原始线性规划 (Primal LP):

最小化 B*x + Σ(i=1 to K) z_i
约束条件: 对于所有天数 i, x + z_i ≥ 1
          x ≥ 0, z_i ≥ 0

其中,x表示是否购买滑雪板,z_i表示第i天是否租赁。

对偶线性规划 (Dual LP):

最大化 Σ(i=1 to K) y_i
约束条件: Σ(i=1 to K) y_i ≤ B
          对于所有 i, 0 ≤ y_i ≤ 1

分数算法

我们在线构建原始解和对偶解,并保持其可行性。

算法步骤:

  1. 初始化 x = 0, 所有 z_i = 0, 所有 y_i = 0。
  2. 当第 i 天的约束在线到达时:
    • 如果 x ≥ 1,则不做任何操作。
    • 否则,设置:
      • z_i = 1 - x
      • x = (1 + 1/B) * x + 1/(C*B) (C为待定常数)
      • y_i = 1

分析:

  • 在每一步,对偶目标函数的增加量 Δ(dual) = 1。
  • 原始目标函数的增加量 Δ(primal) = B*Δx + z_i = 1 + 1/C。
  • 因此,原始目标值/对偶目标值 ≤ 1 + 1/C。
  • 我们需要确保对偶可行性,即 Σ y_i ≤ B。这要求“else”子句最多执行B次。通过选择常数 C = (1 + 1/B)^B - 1,可以保证 x 在 B 次增量后达到 1,从而满足对偶约束。
  • 当 B 很大时,(1 + 1/B)^B ≈ e,因此竞争比约为 1 + 1/(e-1) = e/(e-1)

然而,这个解是分数的(即部分购买/租赁),并非实际可行的策略。

随机舍入以获得整数解

为了获得一个实际可行的随机化算法,我们引入随机性将分数解舍入为整数解。

随机化算法:

  1. 初始化所有变量为0,并随机选取一个均匀分布的参数 α ∈ [0, 1]。
  2. 在线处理每天约束时,仍按上述分数算法更新 x 和 z_i。
  3. 在实际决策中,我们在第一天满足 α ≤ x 时购买滑雪板。在此之前的日子则选择租赁。

期望成本分析:

  • 购买成本的期望 = B * Pr[购买] = B * x。
  • 租赁成本的期望 = Σ Pr[在第 i 天租赁] = Σ (1 - x_i) = Σ z_i。
  • 因此,总期望成本 = B*x + Σ z_i,恰好等于分数原始目标函数的值。

由此,我们得到了一个期望竞争比为 e/(e-1) 的随机化在线算法。

在线集合覆盖问题

接下来,我们探讨原始对偶方法在在线集合覆盖问题中的应用。我们将展示如何获得 O(log n * log m) 的竞争比。

问题定义与线性规划

我们有一个元素全集 U = {1, ..., n} 和 m 个集合 S_1, ..., S_m。在线阶段,元素逐个到达。当一个元素 i 到达时,我们会得知哪些集合包含它,并且必须立即选择一个集合来覆盖它(如果尚未被已选集合覆盖)。目标是最小化最终选择的集合总数。

其线性规划松弛如下:

原始线性规划 (Primal LP):

最小化 Σ(S ∈ C) x_S
约束条件: 对于所有 i ∈ U, Σ(S: i ∈ S) x_S ≥ 1
         对于所有 S, x_S ≥ 0

对偶线性规划 (Dual LP):

最大化 Σ(i ∈ U) y_i
约束条件: 对于所有 S ∈ C, Σ(i ∈ S) y_i ≤ 1
         对于所有 i, y_i ≥ 0

原始视角算法与分析

我们首先从一个纯原始的视角来设计算法。

算法步骤:

  1. 初始化所有 x_S = 1/m。
  2. 当一个新元素 i 到达,且其约束 Σ(S: i ∈ S) x_S < 1 未被满足时:
    • 令包含 i 的集合为 S_j1, ..., S_jr。
    • 同时连续地增加这些集合的 x_S 值,遵循微分方程:dx_S(t)/dt = x_S(t)
    • 持续这个过程,直到元素 i 的约束被满足(即 Σ x_S ≥ 1)。

分析:

  • 解微分方程可得,x_S 随时间 t 呈指数增长:x_S(t) = (1/m) * e^t。
  • 关键观察:每当为元素 i 运行此过程时,至少有一个包含 i 的集合属于最优解 OPT。我们称这个集合为 S*。
  • 对于任何集合 S,其 x_S 从 1/m 开始增长,一旦达到 1 便不再增长(因为其覆盖能力已饱和)。因此,任何集合 S 经历此增长过程的总时间最多为 ln(m)
  • 因此,所有属于 OPT 的集合所经历的总增长时间最多为 |OPT| * ln(m)。
  • 在增长过程中,原始目标函数值的增加率 d(primal)/dt = Σ (dx_S/dt) = Σ x_S ≤ 1(因为约束未被满足)。
  • 原始目标初始值为 1。经过最多 |OPT| * ln(m) 时间单位,且每单位时间增量最多为 1,故最终原始目标值 ≤ 1 + |OPT| * ln(m) = O(log m * OPT)。

这给出了一个分数解,其成本是 O(log m) 近似的。

原始对偶视角

我们可以通过同时维护对偶变量来重新审视这个算法。

算法步骤(原始对偶版本):

  1. 初始化所有 x_S = 1/m,所有 y_i = 0。
  2. 当元素 i 到达且约束不满足时:
    • 对每个包含 i 的集合 S,运行:dx_S(t)/dt = x_S(t)。
    • 同时运行:dy_i(t)/dt = 1。
    • 持续直到约束满足。

分析:

  • 在增长阶段,原始目标增加率 ≤ 1,对偶目标增加率 = 1。因此,最终原始成本 ≤ 对偶成本。
  • 我们需要检查对偶可行性。对于任意集合 S,考虑其对偶约束 Σ(i ∈ S) y_i ≤ 1。每当该和式增加时(即某个 i ∈ S 的 y_i 增加),我们知道同时 x_S 也在增加。而 x_S 最多只能增长 ln(m) 时间(从 1/m 到 1)。因此,Σ(i ∈ S) y_i ≤ ln(m)。
  • 如果将整个对偶解 y 除以 ln(m),则得到一个可行的对偶解,其对偶目标值为 (原对偶成本)/ln(m)。
  • 根据弱对偶定理,任何可行对偶解的目标值 ≤ 原始最优解 OPT。因此有:(原对偶成本)/ln(m) ≤ OPT。
  • 又因为原始成本 ≤ 原对偶成本,所以原始成本 ≤ ln(m) * OPT。

这个视角不直接与 OPT 比较,而是通过构建一个“近似可行”的对偶解来间接证明。

获得整数解

上述算法产生分数解 x_S。为了获得整数解(即实际选择哪些集合),我们需要随机舍入。

直接舍入的问题: 如果简单地将每个集合 S 以概率 x_S 独立地选入解中,一个元素被覆盖的期望次数 ≥ 1,但仍有可能不被任何集合覆盖。

解决方案(增强舍入):

  1. 为每个集合 S 独立、均匀地随机选取 R = O(log n) 个阈值 α_{S,1}, ..., α_{S,R} ∈ [0,1]。
  2. 在算法运行过程中,维护分数变量 x_S。
  3. 如果对于某个 j,有 x_S ≥ α_{S,j},则将集合 S 加入最终的解中。

分析:

  • 对于任意元素 i,其被一个特定阈值覆盖的概率至少为常数。因此,不被所有 R 个阈值覆盖的概率至多为 2^{-O(R)} = 1/poly(n)。
  • 通过并集界限,所有 n 个元素都被覆盖的概率很高。
  • 由于每个集合 S 最多可能因为 R 个阈值中的任何一个而被选中,期望成本最多增加 R 倍。
  • 因此,最终我们得到一个整数解,其期望成本为 O(log n * log m * OPT)。

转向近似算法

本节我们介绍了原始对偶方法在线算法中的应用。接下来,我们将进入一个相关的新主题:近似算法

近似算法概述

在近似算法中,我们面对的是离线优化问题(例如集合覆盖)。问题可能是NP难的,最优算法非常慢。我们的目标是设计一个快速算法,其产生的解的成本保证在最优解成本的某个小因子 α 之内(对于最小化问题)。

由于我们刚刚学习了在线算法中的原始对偶方法,我们将从一个使用原始对偶方法设计近似算法的例子开始。实际上,近似算法中的原始对偶方法历史更久远(可追溯至上世纪70年代)。

带权集合覆盖的贪心算法

考虑集合覆盖的推广:每个集合 S 有一个成本 c_S。目标是选择总成本最小的集合族来覆盖所有元素。

其线性规划及对偶如下:

原始线性规划 (Primal LP):

最小化 Σ(S ∈ C) c_S * x_S
约束条件: 对于所有 i ∈ U, Σ(S: i ∈ S) x_S ≥ 1
         对于所有 S, x_S ≥ 0

对偶线性规划 (Dual LP):

最大化 Σ(i ∈ U) y_i
约束条件: 对于所有 S ∈ C, Σ(i ∈ S) y_i ≤ c_S
         对于所有 i, y_i ≥ 0

贪心算法:
只要存在未被覆盖的元素,就选择性价比最高的集合,即最小化 c_S / (S 新覆盖的元素数量) 的集合 S。

原始对偶分析预览

我们可以用原始对偶框架分析这个贪心算法:

  1. 初始化解:所有 x_S = 0,所有 y_i = 0。
  2. 当算法选择一个集合 S 时:
    • 设置 x_S = 1(原始成本增加 c_S)。
    • 对于 S 新覆盖的每个元素 i,设置 y_i = c_S / (新覆盖元素数)。
    • 这样,原始成本的增加量恰好等于对偶成本的增加量。
  3. 最终,原始解是可行的整数解,但对偶解可能不可行(可能违反 Σ(i ∈ S) y_i ≤ c_S)。
  4. 可以证明,对偶约束最多被违反 O(log n) 倍。因此,将对偶解缩放 O(log n) 倍后即可行。
  5. 根据对偶理论,缩放后的对偶目标值 ≤ OPT,从而证明贪心算法是 O(log n) 近似的。

这种通过构造一个违反约束的对偶解,然后缩放使其可行的技术,被称为对偶拟合

总结

本节课中我们一起学习了:

  1. 如何利用原始对偶方法为滑雪租赁问题设计一个竞争比接近 e/(e-1) 的随机化在线算法,关键步骤是分数算法后的随机舍入。
  2. 如何将原始对偶方法应用于在线集合覆盖问题,通过连续时间增长过程和对偶拟合技术,获得 O(log n * log m) 的竞争比,并讨论了通过增强随机舍入获得整数解的方法。
  3. 初步了解了原始对偶方法在离线近似算法领域的应用,特别是如何分析带权集合覆盖的贪心算法,并引出了“对偶拟合”的概念。

原始对偶方法为在线和离线算法设计提供了强大而统一的分析框架。

011:对偶拟合、整数性间隙与多项式时间近似方案

在本节课中,我们将要学习三个核心主题:首先,我们将完成对加权集合覆盖问题贪心算法的对偶拟合分析。接着,我们将探讨线性规划松弛在近似算法中的一个根本性限制——整数性间隙。最后,我们将介绍一种更强大的近似算法框架:多项式时间近似方案。

集合覆盖的对偶拟合分析

上一节我们介绍了对偶拟合的基本思想,本节中我们来看看如何将其应用于加权集合覆盖问题。

加权集合覆盖问题的输入是:一个由元素 1n 组成的全集 U,以及 m 个集合 S,每个集合 SU 的子集,并有一个成本 C_S。目标是找到一个子集族,覆盖所有元素,且总成本最小。

贪心算法如下:当存在未被覆盖的元素时,选择 成本/新覆盖元素数 比值最小的集合。

定理:贪心算法是 H_n 近似算法(H_n 是第 n 个调和数),这意味着其近似比约为 log n

我们将通过分析其对偶线性规划来证明这一点。

原始线性规划(松弛)

最小化: Σ_S C_S * x_S
约束条件:对于所有元素 i ∈ [1, n], Σ_{S: i ∈ S} x_S ≥ 1
          对于所有集合 S, x_S ≥ 0

其中 x_S = 1 表示选择集合 S

对偶线性规划

最大化: Σ_{e=1}^{n} y_e
约束条件:对于所有集合 S, Σ_{e ∈ S} y_e ≤ C_S
          对于所有元素 e, y_e ≥ 0

现在,我们使用对偶拟合技术。在算法运行过程中,当我们选择一个集合 S 时:

  • 在原始解中,设置 x_S = 1
  • 对于 S 新覆盖的每个元素 e,在对偶解中设置 y_e = C_S / (S 新覆盖的元素数)

观察:原始解的成本等于对偶解的值(因为我们按此方式构造)。

然而,这样构造的对偶解 y 可能不满足对偶约束(即对于某些集合 SΣ_{e ∈ S} y_e 可能超过 C_S)。但我们可以证明,将 y 按比例缩小 H_n 倍后,将得到一个可行的对偶解。

证明关键步骤
对于任意集合 S,设其大小为 k。将其元素按首次被覆盖的顺序排列为 e_1, e_2, ..., e_k。考虑元素 e_i 被覆盖的时刻。在那一刻,算法本可以选择集合 S,其“单价”为 C_S / (k - i + 1)。由于贪心算法选择了当时单价最低的集合,因此分配给 e_iy_{e_i} 至多为 C_S / (k - i + 1)。于是,对于集合 S 有:

Σ_{e ∈ S} y_e ≤ C_S * Σ_{i=1}^{k} (1 / i) = C_S * H_k ≤ C_S * H_n

因此,y / H_n 是一个可行的对偶解。

根据弱对偶定理,任何原始可行解的成本至少等于任何对偶可行解的值。因此,贪心算法的成本(即原始解成本)至多是对偶最优解的 H_n 倍,从而至多是原始整数最优解的 H_n 倍。这就证明了 log n 近似比。

顶点覆盖与对偶拟合

上一节我们分析了集合覆盖,本节中我们来看看一个更简单的例子:无权顶点覆盖。

在顶点覆盖问题中,给定一个无向图 G=(V, E),目标是选择一个顶点子集 S,使得每条边至少有一个端点在 S 中,并最小化 |S|

注意:顶点覆盖是集合覆盖的一个特例(将每个顶点视为一个“集合”,包含其关联的边作为“元素”)。

贪心算法如下:当存在未被覆盖的边 (u, v) 时,将 uv 都加入覆盖集 S

我们将通过构造原始解和对偶解来分析它。

原始线性规划(松弛)

最小化: Σ_{v ∈ V} x_v
约束条件:对于所有边 (u, v) ∈ E, x_u + x_v ≥ 1
          对于所有顶点 v, x_v ≥ 0

对偶线性规划

最大化: Σ_{e ∈ E} y_e
约束条件:对于所有顶点 v, Σ_{e: v ∈ e} y_e ≤ 1
          对于所有边 e, y_e ≥ 0

对偶拟合过程
当贪心算法覆盖一条边 e = (u, v) 时:

  • 在原始解中,设置 x_u = 1x_v = 1
  • 在对偶解中,设置 y_e = 1

分析

  • 原始解和对偶解都是可行的。
  • 每次将对偶解的值增加 1(通过设置 y_e=1),原始解的成本最多增加 2(可能只增加 1,如果某个顶点已被覆盖)。
  • 因此,原始解的成本至多是对偶解值的 2 倍。
  • 根据弱对偶定理,对偶解的值不超过原始整数最优解的值。
  • 所以,贪心算法是 2 近似算法。

整数性间隙

我们一直在使用线性规划松弛来设计和分析近似算法。但这种方法存在一个根本性的限制:松弛后的线性规划最优解与原始整数规划最优解之间可能存在固有的差距,这被称为整数性间隙

对于一个最小化问题,设 OPT_IP 为整数规划的最优值,OPT_LP 为其线性规划松弛的最优值。整数性间隙指的是 OPT_LPOPT_IP 的最大可能比值(对于最小化问题,OPT_LP ≤ OPT_IP,我们关心 OPT_IP / OPT_LP 的上界)。如果存在一个实例使得这个间隙是 β,那么任何基于与该 LP 最优值比较的分析方法,其近似比不可能优于 β

顶点覆盖的整数性间隙

考虑一个 n 个顶点的完全图 K_n

  • 分数最优解:给每个顶点分配 x_v = 1/2。这是一个可行的 LP 解,总成本为 n/2。可以证明这是最优的,所以 OPT_LP = n/2
  • 整数最优解:任何顶点覆盖必须包含至少 n-1 个顶点(否则会有一条边未被覆盖)。所以 OPT_IP ≥ n-1

因此,整数性间隙至少为 (n-1) / (n/2) ≈ 2。这意味着,使用这个特定的 LP 松弛,我们无法设计出优于 2 近似的算法(通过比较 OPT_LP 的分析框架)。

集合覆盖的整数性间隙

我们可以构造一个实例,使得集合覆盖的整数性间隙为 Ω(log n)
构造如下:设 n = 2^q - 1。将全集元素对应于 q 维二元向量空间 F_2^q 中的非零向量。对于每个向量 α ∈ F_2^q,定义一个集合 S_α,包含所有满足点积 α·e = 1 (mod 2) 的元素 e

  • 分数最优解:每个元素恰好被一半的集合包含。因此,设置每个 x_S = 2/(2^q) = 1/2^{q-1} 是一个可行的 LP 解,总成本为 2^q * (1/2^{q-1}) = 2。所以 OPT_LP ≤ 2
  • 整数最优解:假设存在一个大小仅为 q-1 的集合族覆盖了所有元素。这意味着存在 q-1 个线性方程(对应 q-1 个集合),其解空间(满足所有方程 α_i·e = 0e)至少是 1 维的,必然包含非零向量,这与“所有元素被覆盖”(即没有非零向量同时满足所有 q-1 个方程为 0)矛盾。因此,任何整数解至少需要 q 个集合,OPT_IP ≥ q ≈ log2(n+1)

因此,整数性间隙至少为 Ω(log n)。这从另一个角度解释了为什么贪心算法的 log n 近似比是紧的,并且提示我们,使用这个标准 LP 松弛无法获得更好的近似比。

多项式时间近似方案

前面我们看到的近似算法具有固定的近似比(如 2, log n)。但有时我们希望对于任意给定的精度要求 ε > 0,都能得到一个近似比 1+ε(对于最小化问题)或 1-ε(对于最大化问题)的算法,同时运行时间关于输入规模是多项式级的(尽管关于 1/ε 可能是指数级的)。这类算法被称为多项式时间近似方案

以下是相关定义:

  • PTAS:对于每个固定的 ε > 0 和问题规模 n,算法能在时间 n^{f(1/ε)} 内实现 (1+ε) 近似(最小化)或 (1-ε) 近似(最大化)。这里 f 是某个函数,运行时间关于 n 是多项式,但关于 1/ε 可以任意增长。
  • FPTAS完全多项式时间近似方案。算法运行时间关于 n1/ε 都是多项式级。
  • FPRAS完全多项式时间随机近似方案。这是一个随机算法,运行时间关于 n1/ε 是多项式级,并以至少 2/3 的概率返回一个 (1+ε) 近似的解。

背包问题的 PTAS

我们以经典的 0-1 背包问题 为例展示一个 PTAS。问题定义如下:给定容量 Wn 个物品,每个物品 i 有重量 w_i 和价值 v_i。目标是选择物品子集,使得总重量不超过 W,且总价值最大。

首先,我们回顾两个基础知识:

  1. 分数背包最优解:将物品按单位价值 v_i / w_i 降序排列,并依次尽可能多地放入背包,直到装满。最后一个物品可能只取一部分。
  2. 修改的贪心算法:运行上述贪心算法(但只取整件物品),同时单独考虑价值最高的单个物品。取这两个解中较好的一个。这个算法是 2 近似的。

关键引理:设贪心算法依次考虑了物品 1, 2, ..., k(按单位价值降序),并且第 k 个物品因空间不足而无法完全放入。那么,前 k 个物品的总价值 Σ_{i=1}^{k} v_i 至少等于分数背包的最优值,从而也至少等于整数背包的最优值 OPT

基于此,我们可以设计一个 PTAS。核心思想是:在最优解中,价值很大的物品数量是有限的。

算法思路

  1. 猜测:枚举所有大小不超过 t = ⌊1/ε⌋ 的物品子集 SS 将作为我们猜测的“大物品”集合(即最优解中价值超过 ε * OPT 的物品)。由于 t 是常数,这样的子集数量为 O(n^t),关于 n 是多项式。
  2. 验证与填充:对于每个猜测的 S,检查其总重量是否不超过 W。如果超重,则跳过。否则,从剩余物品中移除所有价值大于 S 中任何物品的物品(因为它们也应该是“大物品”,但不在我们的猜测中,说明猜测有误,但算法会继续尝试其他猜测)。
  3. 对剩余物品(都是“小物品”,价值 ≤ ε * OPT)运行上述贪心算法。
  4. 输出所有尝试中得到的最高价值解。

正确性分析:当算法恰好猜中了最优解中的“大物品”集合 S* 时,设 OPT = V(S*) + OPT',其中 OPT' 是最优解中小物品部分的价值。我们的算法得到了 V(S*) + GREEDY。根据引理,GREEDY + v_k ≥ OPT',其中 v_k 是贪心算法第一个放不下的物品的价值。由于所有小物品价值 ≤ ε * OPT,所以 v_k ≤ ε * OPT。因此,GREEDY ≥ OPT' - ε * OPT。最终,算法获得的价值至少为 V(S*) + OPT' - ε * OPT = OPT - ε * OPT

运行时间:主要开销是枚举大小为 O(1/ε) 的子集,共 O(n^{1/ε}) 种可能。对于每个猜测,运行贪心算法需要 O(n log n) 时间。因此,总运行时间为 O(n^{1/ε + 1} log n),对于固定的 ε,这是关于 n 的多项式时间。

本节课中我们一起学习了如何用对偶拟合分析加权集合覆盖和顶点覆盖的贪心算法,理解了整数性间隙对基于线性规划的近似算法设计带来的根本限制,并初步了解了更强大的多项式时间近似方案的概念及其在背包问题上的一个经典构造。

012:近似算法进阶

在本节课中,我们将要学习近似算法的几个进阶主题。我们将首先完成对完全多项式时间近似方案(FPTAS)的讨论,以背包问题为例。接着,我们将探讨完全多项式时间随机近似方案(FPRAS),并以DNF计数问题为例。最后,我们将介绍半正定规划(SDP)及其在近似算法中的应用,特别是最大割问题。

背包问题的FPTAS

上一节我们介绍了近似方案(PTAS),它允许我们在多项式时间内得到任意精度的近似解。本节中我们来看看完全多项式时间近似方案(FPTAS),它要求算法的运行时间不仅对输入规模是多项式的,对误差参数 1/ε 也必须是多项式的。

对于背包问题,我们将展示一个简单的FPTAS算法,其运行时间为 O(n³/ε),并能得到一个 (1 - ε) 近似解。

算法思路:缩放与舍入

该算法的核心思想是通过缩放物品的价值,使其变小,从而可以在多项式时间内运行精确的动态规划算法。

以下是算法的具体步骤:

  1. 找到所有物品中的最大价值 Vmax
  2. 定义缩放因子 α = (n / ε) / Vmax
  3. 对每个物品 i,定义新的价值 v_i' = floor(α * v_i)
  4. 使用动态规划算法,在缩放后的价值 v_i' 和原始重量 w_i 上,求解精确的最优解集合 A
  5. 输出集合 A 作为原问题的近似解。

算法正确性分析

我们声称,由上述算法得到的解 A,其价值至少是原问题最优解 OPT(1 - ε) 倍。

证明概要:
α 为缩放因子。对于任意物品集合 S,令 v(S)v'(S) 分别表示其在原价值和缩放后价值下的总价值。我们有:

  • v'(S) ≥ α * v(S) - |S| (因为向下取整最多使每个物品损失1点价值)
  • v'(S) ≤ α * v(S)

A* 为原问题的最优解集合。由于 A 是缩放后问题的精确最优解,因此 v'(A) ≥ v'(A*)

结合以上不等式,我们可以推导出:
v(A) ≥ (1/α) * v'(A) ≥ (1/α) * v'(A*) ≥ (1/α) * (α * v(A*) - |A*|) = v(A*) - |A*|/α ≥ OPT - ε * Vmax ≥ (1 - ε) * OPT

最后一个不等式成立是因为 OPT ≥ Vmax,且 α = (n / ε) / Vmax,所以 |A*|/α ≤ n / α = ε * Vmax

运行时间分析

缩放后的最大价值 Vmax' 不超过 n/α = n²/ε。动态规划算法的运行时间为 O(n * Vmax'),因此总运行时间为 O(n³/ε),满足FPTAS的要求。


不存在FPTAS的问题

并非所有问题都存在FPTAS。有时,存在简单的证据表明FPTAS不可能存在,除非P=NP。

装箱问题示例

考虑装箱问题:给定 n 个大小为 s1, s2, ..., sn(均在 (0, 1] 区间内)的物品,以及容量为1的箱子,目标是用最少的箱子装下所有物品。

为什么装箱问题没有FPTAS?
假设存在一个FPTAS,对于任意 ε > 0,它能在多项式时间内找到一个使用不超过 (1+ε)*OPT + 1 个箱子的解。如果我们选择 ε < 1/(n+1),那么误差将小于1。由于使用的箱子数必须是整数,该FPTAS实际上能给出精确的最优箱子数量。然而,判断物品是否能装入两个箱子(即 OPT ≤ 2 是否成立)本身是NP完全问题。因此,除非P=NP,否则装箱问题不存在FPTAS。这类问题被称为强NP难问题。


DNF计数问题的FPRAS

本节我们转向完全多项式时间随机近似方案(FPRAS)。它与FPTAS类似,但算法是随机的,并以至少 (2/3) 的概率输出一个满足精度要求的近似解。

我们将以DNF计数问题为例。给定一个析取范式(DNF)公式,目标是计算有多少个变量赋值能使其为真。精确计算该数量是#P-完全问题,非常困难。但我们可以随机地近似它。

直接抽样法的缺陷

一个朴素的想法是:随机抽取大量变量赋值,计算其中满足公式的比例,然后用这个比例乘以总赋值数 2^n 来估计解的数量。
问题:如果真实的满足赋值比例 p 非常小(例如 p ≈ 1/2^n),那么需要抽取指数级别的样本才能观测到哪怕一个满足赋值,否则估计值总是0。

Karp-Luby-Madras 算法

该算法的核心技巧是构造一个更大的集合 B',使得我们想要计数的集合 B(满足赋值的集合)在 B' 中占据的比例 p' 有一个已知的下界(p' ≥ 1/mm 为子句数),从而可以用较少的随机样本进行估计。

算法步骤:

  1. 定义集合:设共有 m 个子句。令 S_i 为满足第 i 个子句的赋值集合。我们想估计 B = ∪_{i=1}^m S_i 的大小。定义 B' 为所有“(子句,赋值)”对的集合,其中赋值满足该子句。注意,一个赋值可能出现在多个对中。
  2. 计算 |B'||B'| = Σ_{i=1}^m |S_i|。而 |S_i| = 2^{n - l_i},其中 l_i 是第 i 个子句中文字的数量。这可以精确、快速地计算。
  3. 估计比例 p' = |B| / |B'|
    • 抽样:从 B' 中均匀随机抽取 T 个样本。如何抽样?
      • 以概率 |S_i| / |B'| 随机选择一个子句 i
      • 然后,对于子句 i,随机生成一个满足它的赋值(固定子句中文字的值,其他变量随机赋值)。
    • 检查:对于每个抽样的 (i, x),检查子句 i 是否是赋值 x 所满足的第一个子句(按固定顺序,如索引顺序)。如果是,则 x ∈ B
    • 计算:令 \tilde{p'}T 个样本中属于 B 的比例。
  4. 输出估计值:输出 \tilde{N} = \tilde{p'} * |B'| 作为 |B| 的估计值。

算法分析

  • 为什么可行? 关键性质是 p' = |B| / |B'| ≥ 1/m。因为每个满足赋值至少出现在 B' 中一次,至多 m 次。
  • 样本复杂度:使用切尔诺夫界可以证明,若设 δ 为失败概率,则只需抽取 T = O( (m * log(1/δ)) / ε² ) 个样本,就能以至少 1-δ 的概率保证 \tilde{p'}p'(1±ε) 近似,从而 \tilde{N}|B|(1±ε) 近似。
  • 运行时间:抽样和检查每个样本均可在多项式时间内完成。因此,这是一个FPRAS。

半正定规划与最大割问题

最后,我们介绍一种更强大的数学工具——半正定规划,并展示其如何用于获得更好的近似算法。

什么是半正定规划?

半正定规划是线性规划的推广。在线性规划中,变量是向量;在SDP中,变量是一个半正定矩阵 X

  • 半正定矩阵:一个对称矩阵 X 是半正定的,如果对于所有实向量 y,都有 y^T X y ≥ 0。等价地,其所有特征值非负,或可写成 X = M^T MM 为某个实矩阵)。
  • SDP标准形式
    最小化: C • X (矩阵内积,即 Trace(C^T X))
    约束: A_i • X = b_i, for i = 1..m
          X ≽ 0 (X是半正定矩阵)
    
  • 可解性:与LP类似,存在多项式时间算法可以求解SDP到任意精度。

向量规划

利用 X = M^T M 的性质,SDP可以等价地转化为向量规划。将矩阵 X 的列视为向量 v_1, ..., v_n,则 X_{ij} = v_i · v_j。向量规划中的变量就是这些向量点积。

最大割问题的SDP舍入算法

最大割问题:给定无向图 G=(V, E),找到一个划分 (S, V\S),使得跨越割的边数最多。

  • 简单近似:随机划分或局部搜索可获得 1/2 近似比。

Goemans-Williamson 算法 使用SDP获得了约 0.878 的近似比。

  1. 整数二次规划形式:为每个顶点 i 分配变量 x_i ∈ {-1, +1}(+1表示一侧,-1表示另一侧)。目标函数为:Maximize Σ_{(i,j)∈E} (1 - x_i x_j)/2。当 x_ix_j 异号时,此项为1,表示边被割开。
  2. SDP松弛:将离散变量 x_i ∈ {-1,+1} 松弛为单位向量 v_i ∈ R^n,且 ||v_i|| = 1。将乘积 x_i x_j 松弛为点积 v_i · v_j。得到向量规划:
    最大化: Σ_{(i,j)∈E} (1 - v_i · v_j)/2
    约束: v_i · v_i = 1, for all i
    
    这个向量规划等价于一个SDP,可以在多项式时间内求解。
  3. 随机超平面舍入:求解SDP得到一组单位向量 {v_i}。然后,随机选取一个单位球面上的随机向量 r。对于每个顶点 i,如果 v_i · r ≥ 0,则将其放入 S;否则放入 V\S
  4. 期望性能分析:对于一条边 (i, j),其被割的概率等于向量 v_iv_j 被随机超平面分开的概率。可以证明,这个概率是 arccos(v_i · v_j) / π。因此,算法的期望割权值为:
    E[算法输出] = Σ_{(i,j)∈E} arccos(v_i · v_j) / π
    而SDP的目标函数值是:
    SDP值 = Σ_{(i,j)∈E} (1 - v_i · v_j)/2
    通过最小化比值 (arccos(θ)/π) / ((1-θ)/2)θ ∈ [-1,1] 上的值,Goemans和Williamson证明了算法的期望值至少是SDP值的 0.878... 倍。又因为SDP值是原整数规划最优值 OPT 的一个上界(松弛),所以算法是一个 0.878 近似算法。

总结

本节课中我们一起学习了近似算法的三个进阶方向:

  1. FPTAS:我们以背包问题为例,展示了如何通过缩放和舍入技术,设计出运行时间对 1/ε 也是多项式的精确近似方案。
  2. FPRAS:我们以DNF计数问题为例,说明了如何通过巧妙的样本空间构造和抽样,为计数问题设计高效的随机近似方案。
  3. 半正定规划:我们介绍了SDP这一强大的优化工具,并以最大割问题为例,展示了如何将组合优化问题松弛为SDP,并通过巧妙的随机舍入技术获得比简单方法更优的近似比。这开启了使用更复杂数学工具进行算法设计的新途径。

013:主题模型学习

在本节课中,我们将学习如何将理论计算机科学中的算法设计技术应用于机器学习问题,特别是学习主题模型。我们将从无监督学习的基本概念开始,逐步深入到主题模型的定义、其背后的概率模型,以及如何通过引入自然假设来设计高效的算法。


概述

无监督学习的目标是在没有标签的数据中发现隐藏的结构。我们通常使用概率模型来定义这种结构,其中模型参数描述了数据的生成过程。学习问题则是在给定从该分布中抽取的样本后,尝试恢复这些参数。然而,直接求解最大似然估计通常是NP难的。我们将探讨如何通过引入“可分离性”等自然假设,将问题转化为一个更易处理的几何问题,并设计出高效的算法。


无监督学习与概率模型

上一节我们介绍了无监督学习的目标。本节中,我们来看看如何用概率模型来形式化地定义“结构”。

在机器学习中,一种流行的定义结构的方式是使用概率模型。一个概率模型是一个函数,它将一组参数映射到一个数据分布。参数就代表了我们要寻找的结构。

例如,一个简单的概率模型是高斯随机变量。其参数是均值μ和方差σ²。一旦知道了这些参数,我们就可以从这个高斯分布中采样数据点。对应的学习问题是:给定从这个分布中采样的多个数据点,估计其均值和方差。

一个更复杂的例子是高斯混合模型。该模型假设数据点来自K个高斯分布中的一个。生成一个数据点时,首先以概率w_i选择第i个高斯分量,然后从该高斯分布中采样一个点。

以下是高斯混合模型的参数:

  • K: 高斯分量的数量。
  • μ_i: 第i个高斯分量的均值向量,其中 i ∈ {1, 2, ..., K}。
  • w_i: 选择第i个高斯分量的权重,其中 i ∈ {1, 2, ..., K},且 Σ w_i = 1。

对应的学习问题是:给定从某个未知参数θ的高斯混合分布D(θ)中抽取的多个样本,目标是找到这个未知参数θ(或其近似值)。


最大似然估计及其挑战

上一节我们定义了学习概率模型的问题。本节中,我们来看看解决该问题最流行的方法及其面临的挑战。

解决这类学习问题最常用的方法是最大似然估计。其思想是:在所有可能的参数中,找到那个能使观测到的数据出现概率最大的参数。

具体而言,假设我们有数据点v_1, v_2, ..., v_n,我们希望找到参数θ,以最大化似然函数:
max_θ Σ_{i=1 to n} log( P_θ(v_i) )
其中,P_θ(v_i) 是在参数θ下生成数据点v_i的概率密度。

对于简单模型(如单个高斯分布),最大似然估计很容易求解。然而,对于更复杂的模型(如高斯混合模型),求解最大似然估计是NP难的,其难度至少与K均值聚类相当。

处理NP难问题有几种思路:

  1. 设计近似算法: 但对于最大似然这种在概率对数空间求最大值的问题,常数倍的近似并不直接对应概率空间的常数倍近似,意义有限。
  2. 利用平均情况分析: 在实际中,数据通常确实来自某个概率分布,这比最坏情况可能更容易。
  3. 引入自然假设: 针对具体问题(如主题模型),对参数施加合理的限制,可能使问题变得可解。
  4. 使用启发式算法: 在实践中有效,并希望能在特定假设下对其进行分析。

我们将主要关注第三种思路,为主题模型引入“可分离性”假设。


主题模型简介

上一节我们讨论了学习概率模型的一般框架。本节中,我们具体看看今天要讨论的问题:主题模型。

主题模型的目标是从一个大型文本文档语料库中自动学习语义结构。例如,从多年的《纽约时报》文章中,自动发现“政治”、“体育”、“科学”等主题,并了解每个主题下常出现的词语,以及每篇文章涉及哪些主题。

这是一个无监督学习任务。我们通过一个概率生成模型来定义目标,这类模型统称为主题模型。为了简化这个复杂的文本生成问题,研究界提出了几个关键假设:

  1. 词袋假设: 忽略词语在文档中的顺序,只关心词语的出现与否。实验表明,这对捕捉语义主题影响不大。
  2. 主题定义: 一个主题被定义为一个在词语上的概率分布。例如,“食物”主题下,“香蕉”一词可能以1%的概率出现。
  3. 文档-主题分布: 每篇文章对应一个在主题上的概率分布。这允许一篇文章涉及多个主题(例如,80%关于政治,20%关于金融)。

在这些假设下,我们可以描述文档的生成过程:

  1. 确定文档长度L(通常视为已知或固定)。
  2. 为该文档生成一个主题比例向量w,这是一个K维向量,满足w_i ≥ 0且Σ w_i = 1。
  3. 对于文档中的每一个词(i从1到L):
    a. 根据分布w选择一个主题t_i ∈ {1, 2, ..., K}。
    b. 根据主题t_i对应的词语分布(即主题矩阵A的第t_i列),生成一个词z_i。

主题矩阵A是一个N×K的矩阵,其中N是词汇表大小,K是主题数量。矩阵的每一列代表一个主题,是该主题下词语的概率分布,因此列中元素非负且和为1。


从主题模型到非负矩阵分解

上一节我们介绍了主题模型的生成过程。本节中,我们来看看如何将其转化为一个矩阵分解问题。

考虑文档-词语矩阵M,其大小为N×M(M是文档数量)。矩阵的第j列代表了第j个文档中词语的分布(即每个词出现的频率)。当文档足够长时,这个频率可以很好地近似词语在该文档中出现的真实概率。

根据生成过程,词语v在文档d中出现的概率可以写为:
P(词 = v | 文档 = d) = Σ_{j=1 to K} P(主题 = j | 文档 = d) * P(词 = v | 主题 = j)
这恰好是矩阵乘法的形式。令W为一个K×M的矩阵,其第d列就是文档d的主题比例向量w_d。那么上述关系可以简洁地写为:
M ≈ A * W
其中A是N×K的主题矩阵,W是K×M的主题比例矩阵。

因此,我们的学习问题转化为:给定观测矩阵M,寻找非负矩阵AW,使得M = A * W。这就是著名的非负矩阵分解问题。如果不要求非负性,可以通过奇异值分解轻松解决。但非负性对于将结果解释为概率至关重要,而一般的非负矩阵分解问题是NP难的。


可分离性假设

上一节我们遇到了一个NP难的非负矩阵分解问题。本节中,我们引入一个关键的自然假设来克服这个困难。

这个假设称为可分离性,由Donoho和Stodden提出,在主题模型语境下非常直观。

定义: 一个非负矩阵A(N×K)是可分离的,如果对于它的每一列j(即每个主题),都存在至少一行i(即一个词),满足:

  • 该行在第j列的元素 A_{i,j} > 0
  • 该行在所有其他列的元素 A_{i, k} = 0,对于所有 k ≠ j。

这意味着,对于每个主题,都存在一个“锚词”,它在这个主题下以非零概率出现,而在其他主题下完全不出现。例如,“雪崩”可能只出现在“天气”主题中,“市盈率”可能只出现在“金融”主题中。

在可分离性假设下,主题矩阵A经过行重排后,其顶部是一个K×K的对角矩阵(由这些锚词行构成),底部可以是任意非负矩阵。


转化为几何问题并设计算法

上一节我们引入了可分离性假设。本节中,我们来看看这个假设如何将问题转化为一个几何问题,并启发我们设计算法。

在可分离性假设下,并且我们对矩阵进行适当的缩放使得每行和为1,那么文档-词语矩阵M的锚词行将恰好等于主题比例矩阵W的对应行。而M的所有其他行,都是W的行的凸组合。

因此,问题转化为:给定一组点(M的行向量),它们位于一个未知的凸包中,并且已知凸包的所有顶点(即锚词对应的行,也就是W的行)都包含在这组点中。我们的目标是找出这些顶点。

这是一个经典的几何问题:给定一个点集,找出构成其凸包顶点的子集。一个简单的算法是:对于点集中的每一个点v_i,检查它是否在其他点构成的凸包内。这可以通过求解一个线性规划来完成:

  • 寻找系数α_j (j ≠ i),使得 v_i = Σ_{j≠i} α_j * v_j,且 α_j ≥ 0Σ α_j = 1
  • 如果该线性规划可行,则v_i在凸包内部,不是顶点。
  • 如果不可行,则v_i是顶点。

然而,在实际的主题模型学习中,我们无法精确获得矩阵M,只能从有限文档中估计词语共现概率。这会给点(矩阵的行)的位置带来噪声。因此,我们需要一个鲁棒的算法,检查一个点是否“接近”其他点的凸包,而不仅仅是“位于”其中。基于这个思想,可以设计出高效的算法,并且在实际数据集(如《纽约时报》语料库)上表现良好,其性能与启发式算法相当甚至更快。


总结

本节课中,我们一起学习了如何将算法技术应用于机器学习中的主题模型学习。我们从无监督学习和概率模型的基础开始,逐步定义了主题模型及其对应的非负矩阵分解问题。面对该问题的NP难性,我们引入了“可分离性”这一自然假设,将问题转化为寻找点集凸包顶点的几何问题,并概述了相应的算法设计思路。这种方法不仅具有理论保障,在实践中也能高效运行。

014:线性规划与单纯形法

在本节课中,我们将学习如何实际求解线性规划问题。我们将从介绍线性规划的标准形式开始,然后深入探讨经典的单纯形算法。我们将了解单纯形法如何工作,以及它如何自然地引出强对偶性。最后,我们会简要提及为什么需要多项式时间算法,并介绍椭球法。


将线性规划转化为标准形式

单纯形算法要求线性规划以标准形式呈现。标准形式如下:

最小化 cᵀx
满足 Ax = bx ≥ 0

其中,x 是一个 n 维向量,A 是一个 m × n 矩阵,且 n ≥ m

任何线性规划都可以转化为这种形式。以下是转换步骤:

  1. 等式与不等式转换

    • 等式约束 aᵢᵀx = bᵢ 等价于两个不等式:aᵢᵀx ≥ bᵢ-aᵢᵀx ≥ -bᵢ
    • 不等式约束 aᵢᵀx ≤ bᵢ 等价于 -aᵢᵀx ≥ -bᵢ
    • 这样,所有约束都变成了“大于等于”形式。
  2. 引入松弛变量

    • 对于每个“大于等于”约束 aᵢᵀx ≥ bᵢ,引入一个非负的松弛变量 sᵢ
    • 将约束重写为 aᵢᵀx - sᵢ = bᵢ,并添加约束 sᵢ ≥ 0
  3. 处理无符号变量

    • 如果原始变量 xᵢ 没有非负限制,可以将其拆分为两个非负变量的差:xᵢ = xᵢ⁺ - xᵢ⁻,其中 xᵢ⁺ ≥ 0xᵢ⁻ ≥ 0

经过这些转换,所有变量(包括原始变量、松弛变量和拆分变量)都是非负的,所有约束都是等式。由于每个原始约束都引入了一个松弛变量,所以变量总数 n 至少等于约束数 m


单纯形法概述

单纯形法是一种贪心算法,它在可行域(一个多面体)的顶点之间移动。其基本思想如下:

  1. 从某个顶点开始。
  2. 贪心地移动到相邻的、能使目标函数值严格改善的顶点。
  3. 当到达最优顶点时停止。

在几何上,可行域是多面体 P = {x | Ax = b, x ≥ 0}。最优解总是在某个顶点处取得(如果问题有界)。单纯形法就是沿着多面体的边,从一个顶点“走”到另一个更优的顶点。


顶点与基

为了精确描述算法,我们需要定义顶点及其与矩阵列的关系。

定义:点 x ∈ P 是一个顶点,如果不存在非零向量 y,使得 x + y ∈ Px - y ∈ P

定理x 是顶点 当且仅当 对应于 x 中正分量的 A 的列是线性无关的。

Bx 中正分量下标的集合,称为。根据上述定理,B 的大小最多为 m。我们可以通过添加其他列(对应 x 中为零的分量)来将基 B 扩展为大小恰好为 m 的集合,同时保持这些列的线性无关性。

对于一个顶点 x 及其基 B,我们可以将变量划分为基变量 x_B 和非基变量 x_N。由于 A_BA 中对应于 B 的列构成的子矩阵)是满秩的方阵(经过扩展后),我们可以解出 x_B
x_B = A_B⁻¹b - A_B⁻¹A_N x_N

目标函数可以重写为:
cᵀx = c_Bᵀ A_B⁻¹b + (c_Nᵀ - c_Bᵀ A_B⁻¹A_N) x_N
我们定义缩减成本向量为:\tilde{c}_N = c_N - A_Nᵀ (A_B⁻¹)ᵀ c_B


单纯形算法步骤

现在我们可以正式描述单纯形算法:

  1. 初始化:找到一个初始顶点 x 及其对应的基 B。这可以通过求解一个辅助的线性规划来完成(例如,引入人工变量并最小化其和)。
  2. 迭代
    a. 将线性规划用当前基 B 重写,计算缩减成本 \tilde{c}_N
    b. 最优性检验:如果对于所有非基变量 j,都有 \tilde{c}_j ≥ 0,则当前顶点 x 是最优解。算法停止。
    c. 选择入基变量:否则,选择一个满足 \tilde{c}_j < 0 的非基变量 j。增加 x_j 可以降低目标函数值。
    d. 确定步长与出基变量:增加 x_j 时,基变量 x_B 会根据公式 x_B = A_B⁻¹b - A_B⁻¹A_N x_N 发生变化。我们不断增加 x_j,直到某个基变量 x_k 首先减少到 0。这个变量 k 将离开基 B
    e. 更新:将变量 j 加入基 B,将变量 k 移出基 B。更新顶点 x 和基 B,然后回到步骤 2a。

关于旋转规则的说明:在步骤 2c 和 2d 中,如果有多个 j 满足 \tilde{c}_j < 0,或者有多个基变量同时变为 0,就需要一个规则来选择入基和出基变量,这称为旋转规则。一个简单的规则(如 Bland 规则)可以防止算法陷入无限循环。然而,尽管单纯形法在实践中通常非常高效,但所有已知的旋转规则在最坏情况下都需要指数时间。


强对偶性与互补松弛性

当单纯形算法停止时,我们不仅得到了原始问题的最优解 x*,还可以从最终基 B 的信息中直接读出对偶问题的最优解 y*

具体来说,在最优基 B 下,我们有 \tilde{c}_N ≥ 0。对偶变量可以设置为 y* = (A_B⁻¹)ᵀ c_B。可以验证:

  1. y* 是对偶可行的。
  2. 原始目标值 cᵀx* 等于对偶目标值 bᵀy*
  3. 互补松弛条件成立:对于原始最优解 x* 和对偶最优解 y*,有 x*_j (c_j - A_jᵀ y*) = 0 对所有 j 成立。

这实际上就证明了线性规划的强对偶定理:如果原始问题和对偶问题都是可行的,那么它们的最优值相等。


多项式时间算法简介

单纯形法虽然在实际应用中非常有效,但其最坏情况时间复杂度是指数级的。这促使人们寻找多项式时间的线性规划算法。

第一个被证明是多项式时间的算法是椭球法。它的核心思想是将线性规划可行性问题转化为在凸集中寻找一个点的问题。算法从一个包含可行域的大椭球开始,不断切割椭球,并构造一个体积更小的新椭球来包含可行域。由于椭球体积每次迭代都以固定比例缩小,经过多项式次迭代后,体积将变得极小,从而可以得出结论。

然而,椭球法理论意义重大,但在实际中并不如单纯形法或后来的内点法高效。内点法是另一类多项式时间算法,它通过穿越可行域内部(而不是沿着边界顶点)来逼近最优解,在实际的大规模线性规划求解中应用广泛。


总结

本节课我们一起学习了线性规划的求解方法。

  • 我们首先将线性规划转化为单纯形法所需的标准形式。
  • 然后详细介绍了单纯形算法,包括其几何直观、顶点的定义、基的概念以及算法的迭代步骤。
  • 我们看到,单纯形法终止时,不仅能给出原始最优解,还能自然地导出对偶最优解,从而证明了强对偶定理。
  • 最后,我们讨论了单纯形法在最坏情况下的指数复杂度,并简要介绍了多项式时间的替代算法,如椭球法和内点法。

单纯形法因其在实际问题中的卓越表现而历久弥新,而多项式时间算法则保证了线性规划问题在理论上的可解性。

015:单纯形法完结、强对偶性与内点法简介

在本节课中,我们将完成对单纯形法的讨论,介绍强对偶性与互补松弛性,并初步了解椭球算法和内点法的基本思想。

单纯形法回顾

上一节我们介绍了单纯形法的基本框架。本节中,我们来看看如何完善它,并处理一些特殊情况。

单纯形法的基本思想是:在由线性约束定义的多面体顶点之间移动,以最小化目标函数。顶点由一组称为的变量索引定义。

算法步骤

以下是单纯形法的主要步骤:

  1. 初始化:从一个已知的可行顶点 x0 开始。
  2. 迭代:在每一步,我们有一个当前顶点 v 及其对应的基 B
    • 计算缩减成本 c̃_N = c_N - A_N^T (A_B^{-1})^T c_B
    • 如果所有 c̃_N ≥ 0,则当前顶点是最优解,算法终止。
    • 否则,选择一个 j ∈ N 使得 c̃_j < 0
    • 增加变量 x_j,同时调整基变量 x_B 以保持可行性,直到某个基变量 x_k 变为零。
    • j 加入基 B,将 k 移出基 B,得到新的顶点。

处理退化情况

当基变量 v_B 中某些分量为零时,可能出现退化。此时,即使存在负的缩减成本,也可能无法增加 x_j,因为某个约束会立即被违反。

为了避免算法陷入无限循环,需要使用特定的转轴规则来选择进基变量 j 和出基变量 k布兰德规则是一种能保证算法终止的规则:

  • 选择 j:在所有 c̃_j < 0j 中,选择索引最小的一个。
  • 选择 k:定义向量 r 和矩阵 A',计算比值 Q = min_i (b'_i / a'_{ij}),其中 a'_{ij} > 0。选择使 Q 最小的索引 k,若有多个则选索引最小的。

强对偶性与互补松弛性

单纯形法不仅给出了原始问题的最优解,也自然地引出了对偶理论。

强对偶性

对于线性规划问题:

  • 原始问题min c^T x, s.t. Ax = b, x ≥ 0
  • 对偶问题max b^T y, s.t. A^T y ≤ cy 无符号限制)

在单纯形法的每一步,我们可以定义一个影子价格向量 y = (A_B^{-1})^T c_B。可以证明,当前原始目标值等于对偶目标值:c^T v = b^T y

当单纯形法终止时(即 c̃_N ≥ 0),对应的对偶松弛向量 r = c - A^T y 满足 r_N = c̃_N ≥ 0r_B = 0,因此 y 是对偶可行的。此时,原始目标值等于对偶目标值,这恰好证明了强对偶定理:如果原始问题和对偶问题都有可行解,则它们的最优值相等。

互补松弛性

互补松弛性是连接原始最优解 x* 和对偶最优解 y* 的重要性质。

定义对偶松弛向量 s = c - A^T y*。互补松弛性指出:
对于所有 i,要么 x*_i = 0,要么 s_i = 0

其证明源于强对偶性:在最优解处,原始与对偶目标值相等,即 c^T x* = b^T y*。而 c^T x* - b^T y* = (x*)^T s。由于 x* ≥ 0s ≥ 0,要使该式为零,每一项 x*_i * s_i 都必须为零。

椭球算法简介

单纯形法在最坏情况下需要指数时间。第一个被证明的多项式时间线性规划算法是椭球算法

椭球算法的核心思想是解决可行性问题:给定一个多面体 P,判断是否存在点 x ∈ P

算法概要

  1. 找到一个初始椭球 E0,确保它包含整个多面体 P(如果 P 有界)。
  2. 检查当前椭球中心 a 是否在 P 内。
    • 如果在,则找到可行点,算法成功。
    • 如果不在,则存在一个被违反的约束(一个超平面 HaP 分离)。
  3. 构造一个新的、体积更小的椭球 E',它包含 P 在超平面 H 另一侧的部分。
  4. 重复步骤2-3。如果椭球体积变得小于一个预定阈值(与问题输入的比特复杂度 L 相关),则判定 P 为空(不可行)。

椭球算法的一个重要优点是,它只需要一个分离预言机:给定一个点,若能判断其是否在 P 内,若不在则返回一个被违反的约束。这使得它可以处理约束数量极大(甚至指数级)的问题,只要分离预言机可以高效实现。

通过解决一系列可行性问题,椭球算法也可以用于解决优化问题。

内点法初步

另一种多项式时间算法是内点法,它在实践中通常比椭球法更快。与单纯形法在边界顶点间“跳跃”不同,内点法试图从多面体内部逼近最优解。

基本思想

内点法通过引入障碍函数将约束优化问题转化为一系列无约束优化问题。

考虑问题:min c^T x, s.t. Ax ≥ b。引入松弛变量 s = Ax - b ≥ 0

我们定义一个新的目标函数,其中包含原始目标和一个障碍函数:
min Φ(x) = λ c^T x + ∑_{i=1}^m p(s_i(x))
其中 p(z) 是一个障碍函数,当 z → 0⁺ 时,p(z) → ∞。常用的选择是对数障碍函数p(z) = -log(z)

  • λ = 0 时,最小化 Φ(x) 就是寻找多面体的解析中心,这是一个深度位于内部的点。
  • λ 很大时,障碍项的影响变小,Φ(x) 的最小值趋近于原始问题的最优解。

路径跟踪

内点法(路径跟踪法)的工作流程如下:

  1. 初始化:通过添加人工变量等技术,获得一个初始内点。从一个很小的 λ 开始。
  2. 迭代
    a. 中心步骤:对于当前的 λ,使用牛顿法等优化算法,近似求解 min Φ(x),得到一个“中心点”。
    b. 参数更新:将 λ 增加一个小的倍数(例如 (1 + 1/√m))。
    c. 以上一个中心点为起点,返回步骤 (a),求解新的 λ 对应的中心点。
  3. 终止:当 λ 足够大时,得到的中心点非常接近原始问题的最优解。可以将其舍入到一个最优顶点。

内点法的迭代次数与约束矩阵的比特复杂度 L 和约束数量 m 有关,通常为 O(√m L) 量级。

总结

本节课中我们一起学习了:

  1. 完善了单纯形法,介绍了处理退化情况的布兰德规则。
  2. 从单纯形法推导出强对偶性,并介绍了互补松弛性这一重要概念。
  3. 简要了解了第一个多项式时间线性规划算法——椭球算法的基本框架和分离预言机的思想。
  4. 初步认识了内点法的核心思想,即通过引入障碍函数和路径跟踪,从多面体内部逼近最优解。

下一讲,我们将深入探讨内点法的具体细节和实现。

016:内点法(第一部分)🚀

在本节课中,我们将要学习一种用于求解线性规划(LP)的强大方法——内点法。我们将从基本概念和算法框架入手,逐步理解其工作原理。

算法概述

内点法的核心思想是:我们并不直接求解原始的线性规划问题,而是通过引入一个惩罚项(称为“障碍函数”),将问题转化为在可行域内部进行优化。随着惩罚参数的调整,最优解会从可行域内部的“解析中心”逐渐移向原始问题的最优顶点。

我们的原始线性规划问题如下:
最小化 cᵀx
约束条件 Ax ≥ b
其中 A 是一个 m × n 的矩阵。

基本框架

内点法的基本框架包含以下步骤:

  1. 问题转换:将原始线性规划问题 LP 修改为 LP',使得 LP' 存在一个显而易见的初始内点。
  2. 获取初始内点:为 LP' 找到一个初始内点 x'
  3. 寻找近似中心:对于一个极小的初始参数 λ₀,找到函数 F_λ₀(x) 的近似最小化解 x̃_λ₀。此时目标函数 cᵀx 的影响微乎其微,我们实际上是在寻找可行域内部的“中心点”。
  4. 路径跟踪:这是算法的主要循环部分。我们逐步增大参数 λλ_{k+1} > λ_k),并利用当前 λ_k 的近似解 x̃_λ_k 作为起点,通过迭代方法(如牛顿法)去求解 F_{λ_{k+1}}(x) 的近似最小化解 x̃_{λ_{k+1}}
  5. 终止与输出:当 λ 足够大时,障碍函数的影响变得很小,F_λ(x) 的最小化解非常接近原始线性规划的最优解。此时我们可以停止迭代,并将结果舍入到最近的顶点,得到原始问题的最优解。

上一节我们介绍了内点法的整体框架,本节中我们来看看如何确定迭代的终止条件以及参数 λ 的增长策略。

终止条件与参数分析

我们定义带障碍函数的优化目标为:
F_λ(x) = λ * cᵀx - Σ_{i=1}^m log(s_i(x))
其中 s_i(x) = A_i x - b_i 是第 i 个约束的松弛变量。对数函数 -log(s_i) 确保了当任何松弛变量接近零(即接近可行域边界)时,函数值趋于无穷大,从而将解限制在可行域内部。

我们的目标是理解,当参数 λ 多大时,F_λ(x) 的最小化解 x_λ 对应的目标值 cᵀx_λ 能够足够接近真正的最优值 cᵀx*

通过分析梯度为零的条件(∇F_λ(x_λ) = 0),我们可以推导出关键的不等式:
cᵀ (x_λ - x*) ≤ m / λ

这个不等式告诉我们,x_λ 的目标值与最优值之间的差距最多为 m/λ。因此,如果我们希望这个差距小于某个预设的极小精度 ε,只需令 λ ≥ m/ε 即可。ε 通常被设置为与问题精度相关的指数级小量。

接下来,我们需要确定在路径跟踪过程中,每次迭代应将 λ 增大多少。分析表明,我们可以安全地将 λ 乘以一个因子 (1 + 1/√m)。这样能保证前一个参数下的近似解,仍然是下一个参数下优化问题的一个足够好的起始点。

基于上述增长因子,从初始的极小值 λ₀ 增长到所需的 λ ≥ m/ε 所需的迭代次数约为 O(√m * L),其中 L 是一个与问题数值精度相关的对数因子。这个 O(√m) 的迭代复杂度是内点法效率的关键。

优化方法基础

为了在每一步中从 x̃_λ_k 高效地计算出 x̃_λ_{k+1},我们需要使用迭代优化算法。内点法通常使用牛顿法,这是一种二阶方法。在深入牛顿法之前,我们先了解一种更基础的一阶方法——梯度下降法,以建立对迭代优化过程的直观理解。

假设我们要最小化一个函数 F(x),并且我们有一个“预言机”,对于任何输入点 x,它能给出函数值 F(x) 和梯度 ∇F(x)。此外,我们假设对 F 的黑塞矩阵 H(x)(二阶导数矩阵)有控制:其特征值在 [μ, L] 区间内(0 < μ ≤ L),这意味着函数不会过于平坦或陡峭。

梯度下降法的更新规则非常简单:
x_{k+1} = x_k - (1/(2L)) * ∇F(x_k)
这个规则来源于用二次函数上界逼近 F(x),然后最小化这个上界。

在满足上述黑塞矩阵条件下,可以证明,每执行一次梯度下降更新,目标函数值与最优值的误差会以因子 (1 - μ/L) 减小。要将误差从初始值 Δ 减少到 εΔ,大约需要 O((L/μ) * log(1/ε)) 次迭代。

值得注意的是,存在更快的“加速梯度下降法”(如Nesterov加速法),可以将迭代复杂度改进到 O(√(L/μ) * log(1/ε))。虽然在内点法的常数参数背景下区别不大,但了解这一点对更广泛的优化问题很有价值。

本节课中我们一起学习了内点法的基本框架、终止条件分析以及作为基础的梯度下降优化方法。下一讲,我们将介绍更强大的牛顿法,并展示如何将其应用于内点法的每一步迭代中,以高效地跟踪中心路径,最终求解线性规划问题。

017:牛顿法与内点法 🚀

在本节课中,我们将学习牛顿法,这是一种二阶优化方法,并了解它如何应用于内点法来求解线性规划问题。我们将从牛顿法的基本概念和收敛性分析开始,然后探讨如何将其整合到内点法的框架中,最后简要讨论内点法的初始化过程。


牛顿法:二阶优化方法

上一节我们回顾了梯度下降法,它是一种一阶方法。本节中,我们来看看牛顿法,这是一种二阶优化方法。

在二阶方法中,我们不仅可以查询函数值及其梯度,还可以查询函数的海森矩阵。我们的目标是最小化一个函数 F,它从 R^n 映射到 R

我们从一个初始点 x_0 ∈ R^n 开始,并重复应用更新规则。牛顿法的更新规则如下:

x_1 = x_0 - (∇²F(x_0))⁻¹ ∇F(x_0)

这个规则来源于对函数 Fx_0 处的二阶泰勒展开式进行最小化。根据泰勒定理,对于点 y,有:

F(y) ≈ F(x_0) + ∇F(x_0)ᵀ (y - x_0) + ½ (y - x_0)ᵀ ∇²F(x_0) (y - x_0)

牛顿法假设这个近似是等式,并最小化右侧的二次型,其最小值恰好由上述更新规则给出。


牛顿法的收敛性分析

为了分析牛顿法的性能,我们需要一个关于海森矩阵变化程度的假设。

定理:对于 t ∈ [0, 1],定义 x_t = x_0 + t(x_1 - x_0)。假设存在一个正数 ε,使得对于所有 t ∈ [0, 1],以下条件成立:

(1 - ε) ∇²F(x_0) ≼ ∇²F(x_t) ≼ (1 + ε) ∇²F(x_0)

这里 A ≼ B 表示 B - A 是半正定矩阵。那么,在由 ∇²F(x_0) 定义的范数下,新迭代点的梯度范数满足:

‖∇F(x_1)‖{∇²F(x_0)⁻¹} ≤ (ε / (1 - ε)) ‖∇F(x_0)‖

其中,对于半正定矩阵 A,范数定义为 ‖x‖_A = √(xᵀ A x)

这个定理表明,只要海森矩阵在优化路径上变化不大(即 ε 很小),牛顿法就能以线性速率减小梯度范数,使我们更接近驻点(梯度为零的点)。


回到内点法

现在,我们将牛顿法应用到内点法的框架中。回忆一下,在内点法中,我们定义了一个障碍函数:

F_λ(x) = λ cᵀ x - Σ_{i=1}^m log((Ax - b)_i)

其中,松弛向量 s(x) = Ax - b。我们的目标是对于一系列递增的 λ 值,近似最小化 F_λ(x)

以下是内点法进度的核心定义:

  • 中心性:对于函数 F_{λ_k},点 x 的中心性定义为:
    Δ_k(x) = ‖∇F_{λ_k}(x)‖{(∇²F(x))⁻¹}
    如果 x 是精确最小化点,则 Δ_k(x) = 0

  • 精细中心点:如果 Δ_k(x) ≤ 1/100,则称 xF_{λ_k} 的精细中心点。

  • 粗糙中心点:如果 Δ_k(x) ≤ 1/3,则称 xF_{λ_k} 的粗糙中心点。

内点法的基本流程如下:

  1. 对于当前参数 λ_k,我们拥有一个精细中心点 x̃_k
  2. 将参数增加到 λ_{k+1} = (1 + α) λ_k
  3. 我们希望证明,x̃_k 对于新的 λ_{k+1} 虽然不再是精细中心点,但仍然是粗糙中心点(即 Δ_{k+1}(x̃_k) ≤ 1/3)。
  4. x̃_k 作为起点,对 F_{λ_{k+1}} 运行牛顿法。由于起点是粗糙中心点,我们可以应用之前的定理,证明在常数次牛顿迭代后,我们能得到一个新的点 x̃_{k+1},使其成为 F_{λ_{k+1}} 的精细中心点。
  5. 重复此过程。

参数 λ 的增量分析

关键问题是:我们每次能将 λ 增加多少(即 α 最大能取多少),才能保证旧的精细中心点对于新的参数仍然是粗糙中心点?

通过计算中心性度量 Δ_{k+1}(x̃_k) 的变化,我们可以得到以下上界:

Δ_{k+1}(x̃_k) ≤ (1 + α) Δ_k(x̃_k) + α √m

由于 x̃_kF_{λ_k} 的精细中心点,有 Δ_k(x̃_k) ≤ 1/100。我们希望 Δ_{k+1}(x̃_k) ≤ 1/3

因此,我们可以选择 α ≈ 1 / (10√m)。这意味着在每一步,我们可以将 λ 乘以一个因子 (1 + 1/O(√m))


海森矩阵的变化控制

为了应用牛顿法收敛定理,我们需要验证在从粗糙中心点向精细中心点优化的路径上,海森矩阵的变化满足定理条件。

对于内点法的障碍函数,其海森矩阵为 ∇²F_λ(x) = Aᵀ S(x)⁻² A,其中 S(x) 是以松弛向量 s(x) 为对角元的对角矩阵。海森矩阵的变化完全由松弛矩阵 S(x) 的变化决定。

分析表明,沿着牛顿法的优化路径,松弛矩阵的相对变化由当前点的中心性 Δ 控制。由于我们从一个粗糙中心点(Δ ≤ 1/3)开始,可以证明定理中的 ε 是一个小于 1/2 的常数,从而保证牛顿法能有效地以常数因子减少误差。


内点法的初始化

最后,我们需要一个初始的精细中心点来启动整个内点法过程。这里概述一个标准方法:

  1. 修改原问题:我们修改原始线性规划,引入一个大的惩罚项和一个辅助变量 z,构造一个新的线性规划 LP‘。可以证明,当惩罚系数 M 足够大(指数级于精度要求)时,新问题的最优解必然有 z=0,从而给出了原问题的最优解。
  2. 寻找内点:修改后的问题 LP‘ 有一个明显的内点(例如,设 x=0z 为足够大的向量)。
  3. 寻找中心点:这个内点对于某个特定的代价函数(例如,单位代价向量)可能是中心点。我们可以从这个点出发,通过反向运行内点法(即逐步减小 λ),来逼近新问题的解析中心
  4. 切换代价函数:当 λ 变得极小时,障碍函数占主导地位,代价函数的影响微乎其微。此时,我们可以将代价函数切换到我们实际关心的目标函数,而该点仍然保持近似中心性。
  5. 正向运行:获得一个对于实际目标函数的近似精细中心点后,我们就可以开始正向运行之前描述的内点法主循环了。

总结

本节课中我们一起学习了:

  1. 牛顿法的原理及其基于海森矩阵相似性的收敛性分析。
  2. 如何将牛顿法作为子程序嵌入到内点法中,通过维护“精细中心点”和“粗糙中心点”的概念,迭代地求解一系列障碍函数。
  3. 分析了参数 λ 的允许增量,得出每一步可以大约乘以 (1 + 1/√m) 的结论,这决定了算法的主迭代次数。
  4. 简要了解了内点法复杂的初始化过程,其核心思想是通过修改问题和反向优化来获得一个可用的起始中心点。

通过结合牛顿法的快速局部收敛性和内点法的路径跟踪策略,我们能够高效地求解线性规划问题。

018:乘性权重法

在本节课中,我们将学习一种名为“乘性权重法”的强大算法框架。它最初用于解决“专家建议”问题,但我们将看到,其应用远不止于此,甚至可以用于近似求解线性规划问题。

专家建议问题

上一节我们介绍了线性规划,本节中我们来看看一个看似不同的问题:如何根据多位专家的建议做出决策。

假设有 N 位专家,我们需要在 T 天内每天做出一个决策(例如,预测天气或股票涨跌)。每天,每位专家会给出“是”或“否”的建议。在每天结束时,我们会得知正确答案。我们的目标是,在整个 T 天里,我们犯的错误总数尽可能少。

当然,如果所有专家都很糟糕,我们也无法做出正确预测。因此,一个合理的衡量标准是:我们的表现应该与“事后看来”最好的那位专家(即犯错最少的专家)相竞争。

一个简单算法:多数投票与淘汰

以下是解决此问题的一个简单思路:

  1. 每天,我们听取所有“存活”专家的意见,并采取多数投票
  2. 在每天结束时,我们淘汰所有当天给出错误建议的专家。

定理:如果存在一位从不犯错的“完美专家”,那么此算法最多犯 log₂(N) 次错误。

证明:每次我们犯错时,意味着多数专家(至少一半)是错误的,因此我们至少淘汰了一半的专家。由于完美专家始终存活,我们最多能淘汰 log₂(N) 次,即最多犯 log₂(N) 次错误。

然而,这个算法有个明显缺陷:如果某一天所有专家都错了(包括那位最好的专家),我们就会淘汰所有人,之后便无法做出决策。为了与最好的专家竞争(而不仅仅是完美专家),我们需要改进算法。

改进算法:淘汰与复活

一个改进方案是:我们按上述规则淘汰专家,但如果所有专家都被淘汰了,我们就复活所有专家,重新开始一个阶段。

定理:假设最好的专家总共犯了 E 次错误。那么此改进算法最多犯 (E + 1) * log₂(N) 次错误。

证明:每次我们淘汰最好的专家时,就标志着一个阶段的结束。由于最好的专家只犯 E 次错,因此最多有 E + 1 个阶段(包括初始阶段)。在每个阶段内,根据之前的分析,我们最多犯 log₂(N) 次错误。因此总错误数最多为 (E + 1) * log₂(N)

这个算法的竞争比大约是 log₂(N)。我们能否做得更好?

乘性权重算法

上一节的算法过于极端:专家一旦犯错就被完全抛弃。一个更柔和的想法是:根据专家的历史表现来加权他们的投票,犯错多的专家权重低,但不会被完全忽略。

这就是乘性权重算法的核心思想。

算法描述

  1. 初始化:为每位专家 i 分配初始权重 w_i = 1
  2. 对于每一天 t = 1 to T
    • 根据当前权重进行加权多数投票做出决策。专家 i 的投票权重为 w_i
    • 当天结束时,对于每位给出错误建议的专家 i,将其权重更新为:w_i ← w_i * (1 - ε)。其中 ε 是一个小的正参数(例如 0 < ε ≤ 1/2)。

性能保证

定理:对于任意 ε ∈ (0, 1/2],乘性权重算法保证我们的总错误数 M_T 满足:
M_T ≤ 2(1 + ε) * E + (ln N) / ε
其中 E 是最好专家(犯错最少的那位)的总错误数。

这个界限比之前的 O(log N * E) 要好得多,因为它将 E 的系数从对数级降到了常数 2(1+ε),虽然增加了一个与 ln N1/ε 相关的附加项。

证明概要

证明的关键在于定义一个势函数 Φ_t = Σ_i w_i(t),即所有专家在时刻 t 的权重之和。

  • 上界:当我们犯错时,意味着加权多数是错误的,即至少一半的权重(按权重计)支持了错误答案。这部分专家的权重会乘以 (1-ε),而其他专家的权重不变。因此,势函数至少减少为原来的 (1 - ε/2) 倍。通过归纳,可以得到 Φ_{T+1} 的一个上界。
  • 下界:势函数 Φ_{T+1} 显然至少等于最好专家 i* 的最终权重 w_{i*}(T+1)。而该权重的值等于 (1-ε)^E
  • 结合:将上下界结合,并利用不等式 1 - x ≤ e^{-x} 和对数展开等技巧进行化简,即可得到定理中的不等式。

模型泛化

上一节我们处理了二元的对错判断。现在我们将模型泛化,使其能处理更一般化的“收益”或“损失”。

泛化模型

  • 每天,我们不再直接选择一个专家,而是输出一个在专家集合上的概率分布 p_t
  • 每天,每位专家 i 会给出一个“建议向量” m_i(t),其分量现在可以在区间 [-1, 1] 内,而不仅仅是 {0, 1}
    • m_i(t) > 0 可以理解为专家 i 的建议导致了损失(类似之前的“犯错”)。
    • m_i(t) < 0 可以理解为专家 i 的建议带来了收益(类似“正确”且可能有额外奖励)。
  • 我们的目标是使累计的期望损失 Σ_t (m_t · p_t) 尽可能小。

泛化算法

乘性权重算法可以自然地泛化:

  1. 初始化权重 w_i(1) = 1
  2. 对于 t = 1 to T
    • 根据权重计算概率分布:p_i(t) = w_i(t) / Σ_j w_j(t)
    • 观察到损失向量 m(t)
    • 更新权重:w_i(t+1) = w_i(t) * (1 - ε)^{m_i(t)}
      • 注意:当 m_i(t) = 1(最大损失)时,权重乘以 (1-ε),与之前一致。
      • m_i(t) = -1(最大收益)时,权重乘以 1/(1-ε) ≈ (1+ε),即权重增加。

性能定理

定理:对于上述泛化的乘性权重算法,对于任意专家 i,我们有:
Σ_{t=1}^T (m_t · p_t) ≤ Σ_{t=1}^T m_i(t) + ε Σ_{t=1}^T |m_i(t)| + (ln N)/ε
这意味着我们的累计期望损失,不超过任何专家 i 的累计损失,加上一个与专家 i 损失绝对值之和成正比、以及与 ln N 成正比的附加项。

证明思路与基础版本类似,通过分析势函数 Φ_t = Σ_i w_i(t) 的上下界,并利用凸函数性质(如 (1-ε)^x 的凸性)来分离正负损失项的影响,最后通过对数和代数运算得到结果。

应用于线性规划

乘性权重法不仅适用于专家建议问题,还是一个强大的通用工具。一个重要的应用是近似求解打包-覆盖线性规划

问题设置

假设我们有一个可行性问题:判断是否存在 x ∈ PPR^n 中的一个凸集)使得 A x ≥ b
如果问题不可行,我们需要报告;如果可行,我们希望找到一个 x ∈ P 使得 A x ≥ b - ε * 1(即每个约束近似满足,误差在 ε 以内)。

与乘性权重的联系

我们可以将线性规划的每个约束 i(即矩阵 A 的第 i 行)视为一位“专家”。

  • 在每次迭代(“天”)t,我们基于当前对约束的“关注度”(权重) p_t,调用一个预言机在集合 P 中寻找一个点 x_t。这个预言机的目标是,对于当前权重 p_t,最小化(或至少使)加权违反量 p_t · (b - A x_t) 较小。这通常是一个更容易的子问题。
  • 然后,我们根据 x_t 满足各个约束的程度(即 A_i x_t - b_i,经过适当归一化到 [-1,1] 区间)来定义损失向量 m(t)
    • 如果约束 i 被很好地满足(A_i x_t 远大于 b_i),则 m_i(t) 为负(“收益”),我们在下一轮降低其权重(减少关注)。
    • 如果约束 i 未被满足或勉强满足,则 m_i(t) 为正(“损失”),我们在下一轮增加其权重(加大关注)。
  • 经过多轮迭代后,我们将所有迭代中得到的点 x_t 进行平均,得到的 很可能就是原问题的近似可行解。

示例:分数集合覆盖

考虑分数集合覆盖问题的线性规划松弛:
最小化 Σ_s x_s,满足对于所有元素 eΣ_{s ∋ e} x_s ≥ 1,且 x_s ≥ 0

为了使用上述框架,我们首先猜测一个目标值上界 α(可通过二分查找确定)。然后定义集合 P = { x ≥ 0 | Σ_s x_s ≤ α }。可行性问题是:是否存在 x ∈ P 使得所有覆盖约束 Σ_{s ∋ e} x_s ≥ 1 成立?

应用乘性权重法后,我们可能得到一个 满足 Σ_{s ∋ e} x̄_s ≥ 1 - ε。虽然这不完全可行,但我们可以将其缩放 1/(1-ε) 倍得到一个可行解,其成本最多为 α/(1-ε) ≈ (1+ε)α。由于 α 是我们猜测的(接近最优值 OPT),这就给出了一个 (1+ε) 近似的分数解,再结合舍入技术即可得到整数解的近似算法。

乘性权重法的优势在于,对于许多组合优化问题,其所需的预言机(在 P 上优化一个线性目标或简单约束)可以高效实现,从而使得整体算法比通用的内点法或椭球法更快,尤其是在只需要近似解时。

总结

本节课中我们一起学习了乘性权重法。我们从简单的专家建议问题出发,介绍了多数投票淘汰算法及其改进。然后,我们深入探讨了更强大、更通用的乘性权重算法,它通过动态调整专家权重来达成近乎最优的累计损失。我们看到了该算法在二元判断和连续损失模型下的形式与理论保证。最后,我们探讨了乘性权重法一个深刻的应用:近似求解线性规划,特别是打包-覆盖问题,并以分数集合覆盖为例说明了其应用流程。乘性权重法是一个在不同领域被反复发现的强大工具,它巧妙地将在线学习与优化理论连接起来。

019:乘性权重与最大流算法 🚀

在本节课中,我们将学习如何将乘性权重方法应用于线性规划问题,并初步介绍最大流问题及其经典算法。

乘性权重方法回顾 📊

上一节我们介绍了乘性权重方法的基本原理。本节中,我们来看看如何将其应用于线性规划问题。

核心定理回顾

乘性权重方法的核心定理可以表述如下:

定理:假设有 N 个专家,在 T 天中,每天我们根据概率向量 P_t 选择专家,并收到成本向量 M_t(其每个分量在 [-1, 1] 之间)。那么,对于任意 ε ∈ (0, 1/2),我们有:

∑_{t=1}^{T} M_t · P_t ≤ min_{i} ∑_{t=1}^{T} M_t(i) + ε ∑_{t=1}^{T} |M_t(i)| + (ln N) / ε

通过选择合适的 ε(例如 ε = √( (ln N) / T )),我们可以得到平均遗憾以 O(√( (ln N) / T )) 的速率衰减。

应用于线性规划求解 🔧

现在,我们利用上述定理来近似求解线性规划问题。

问题设定与有界预言机

考虑一个线性规划可行性问题:是否存在 x ∈ P 使得 A x ≥ b?其中 P 是一个凸集,Am × n 矩阵。

我们引入一个 ρ-有界预言机 的概念。该预言机解决如下子问题:给定一个概率向量 p(即对 m 个约束的权重分布),判断是否存在 x ∈ P 使得 p^T A x ≥ p^T b

预言机的行为规范如下:

  • 如果存在这样的 x,则输出一个 x,满足对于所有约束 i,有 |A_i x - b_i| ≤ ρ
  • 如果不存在这样的 x,则输出“不可行”。

注意,如果原问题可行,那么对于任何概率向量 p,子问题总是可行的。

乘性权重算法流程

以下是利用乘性权重和上述预言机求解近似可行解的算法步骤:

  1. 初始化:将 m 个约束视为专家。设置初始权重分布 P_1 为均匀分布。
  2. 迭代过程:对于每一轮 t = 1, 2, ..., T
    • 将当前的权重分布 P_t 输入给 ρ-有界预言机
    • 如果预言机返回“不可行”,则算法终止并断言原问题不可行。
    • 否则,预言机返回一个解 x_t
    • 定义成本向量 M_t = (1/ρ) * (A x_t - b)。由于预言机的保证,M_t 的每个分量都在 [-1, 1] 内。
    • 根据乘性权重更新规则,利用成本向量 M_t 更新专家权重,得到 P_{t+1}
  3. 输出:计算所有 x_t 的平均值 x̃ = (1/T) ∑_{t=1}^{T} x_t。由于 P 是凸集,x̃ ∈ P

理论保证

可以证明,如果我们设置 ρ ≥ ε/2,并运行 T = O( ρ² log(m) / ε² ) 轮迭代,那么输出的 将是一个 ε-近似可行解,即满足:

A x̃ ≥ b - ε * 1

其中 1 是全1向量。

这意味着我们找到了一个轻微违反原约束(最多违反 ε)的解。通过调整 ε,我们可以控制解的近似程度。

应用实例:集合覆盖问题

我们以(分数)集合覆盖问题为例,说明如何构建所需的有界预言机。

问题:给定集合族 S 和全集 U,最小化 ∑_{S} x_S,使得对于每个元素 e ∈ U,有 ∑_{S: e∈S} x_S ≥ 1,且 x ≥ 0

构建预言机:给定概率分布 p(对 U 中元素的权重),预言机需要判断是否存在 x(满足 ∑ x_S ≤ βx ≥ 0)使得 ∑_S x_S * p(S) ≥ 1,其中 p(S) = ∑_{e∈S} p_e

  • 为了最大化 ∑_S x_S * p(S),最优策略是将所有权重 β 分配给 p(S) 最大的那个集合 S*
  • 因此,预言机只需找到 p(S*)。如果 β * p(S*) < 1,则返回“不可行”;否则,返回解 x(即 x_{S*} = β,其余为0)。
  • 可以验证,对于此 x,任意约束 e 的违反值 |A_e x - b_e| 最多为 max(1, β)。在二分搜索 β(最优值)的过程中,β ≤ m(集合总数),因此 ρ = O(m)

代入通用定理,我们得到求解分数集合覆盖的 (1+ε) 近似解需要 O( m² log m / ε² ) 轮乘性权重迭代。

最大流问题导引 🌊

现在,我们切换主题,开始讨论网络流中的经典问题——最大流。

问题定义

给定一个有向图 G=(V, E),每条边 e 有非负容量 c_e,以及源点 s 和汇点 t。一个 s-t 流 f 是一个满足以下条件的向量:

  1. 非负性f_e ≥ 0
  2. 流量守恒:对于所有非源非汇的顶点 v,流入 v 的总流量等于流出 v 的总流量。
  3. 源汇平衡:从 s 流出的总流量等于流入 t 的总流量,这个值称为流 f,记作 val(f)
  4. 容量约束f_e ≤ c_e

最大流问题的目标是找到一个值最大的可行流。

线性规划视角

最大流问题可以自然地表述为一个线性规划问题:最大化从 s 流出的流量,约束包括容量约束和所有顶点的流量守恒约束。因此,理论上我们可以使用单纯形法、椭球法或内点法等线性规划通用算法求解。

  • 内点法:对于一般LP,需要 O(√m) 次迭代,每次迭代涉及求解一个线性系统(O(m^ω) 时间,ω 是矩阵乘法指数)。对于流问题特有的结构,Daitch-Spielman (2008) 的工作将每次迭代优化到近线性时间 Õ(m)。Lee-Sidford (2014) 进一步将迭代次数优化到 O(√n)。综合可得 Õ(m√n) 的算法。
  • 然而,我们更感兴趣的是组合意义下更直观、更快速的算法。

Ford-Fulkerson 增广路算法

最经典的最大流算法是 Ford-Fulkerson 方法,其核心是 剩余网络增广路 的概念。

剩余网络 G_f:给定流 f,对于原图中的每条边 (u, v)

  • 其正向剩余容量为 c(u,v) - f(u,v),表示还能沿此方向推送多少流量。
  • 添加反向边 (v, u),其剩余容量为 f(u,v),表示可以沿此方向退回多少流量(从而将流量重新分配)。

算法流程

  1. 初始化流 f 为零流。
  2. 在剩余网络 G_f 中寻找一条从 st 的路径(增广路)。
  3. 如果找不到这样的路径,算法终止。
  4. 如果找到,设该路径上所有边的最小剩余容量为 δ,则沿该路径推送 δ 单位的流量(即更新 fG_f)。
  5. 返回步骤2。

关键定理(最大流最小割定理):一个流 f* 是最大流,当且仅当 其对应的剩余网络 G_{f*} 中不存在从 st 的路径(即 st 不连通)。

Ford-Fulkerson 方法的运行时间可能高达 O(m * val(f*)),如果容量很大或算法不幸地选择了低效的增广路,效率会很低。下次课我们将介绍如何通过更智能地选择增广路来获得多项式时间复杂度的算法,例如 Edmonds-Karp 算法(使用BFS寻找最短增广路)以及更现代的算法。

总结 📝

本节课中我们一起学习了:

  1. 乘性权重方法的应用:我们展示了如何将乘性权重框架与一个针对线性规划子问题的有界预言机结合,从而高效地找到原问题的近似可行解,并以集合覆盖问题为例进行了说明。
  2. 最大流问题基础:我们定义了最大流问题,从线性规划角度审视了它,并介绍了最基础的 Ford-Fulkerson 增广路算法及其依赖的最大流最小割定理。这为后续学习更高效的最大流算法奠定了基础。

020:更快的最大流算法 🚀

在本节课中,我们将学习如何改进最大流算法的运行时间。我们将介绍两种关键技术:容量缩放阻塞流。这些方法能将伪多项式时间的Ford-Fulkerson算法,提升为强多项式时间或弱多项式时间的算法。


算法背景回顾

上一节我们介绍了Ford-Fulkerson算法。其核心思想是:给定一个有向图 G=(V, E),每条边 e 有一个整数容量 c(e)(范围从1到U),目标是找到一个从源点 s 到汇点 t最大流

一个流 f 必须满足:

  1. 容量约束:对于每条边 e,有 0 ≤ f(e) ≤ c(e)
  2. 流量守恒:对于除 st 外的每个顶点,流入的流量等于流出的流量。

算法的值 |f| 是从 s 流出的总流量。Ford-Fulkerson算法通过反复在残差图 G_f 中寻找增广路径来工作。每次找到一条路径,就沿其推送尽可能多的流量(至少1个单位)。该算法是正确的,但其运行时间为 O(|f*| * m),其中 f* 是最大流的值。由于 f* 可能非常大(例如 n * U),这属于伪多项式时间。

我们定义:

  • 强多项式时间:运行时间为 poly(m, n),与容量值 U 无关。
  • 弱多项式时间:运行时间为 poly(m, n, log U),与容量的位数相关。
  • 伪多项式时间:运行时间为 poly(m, n, U),与容量值本身相关。

我们的目标是获得强多项式或弱多项式时间的算法。


容量缩放算法 ⚖️

本节我们将看到第一种加速技术:容量缩放。其核心思想是将具有大容量的通用问题,逐步转化为一系列容量较小的子问题,特别是近似于单位容量的情况。

以下是算法的步骤:

  1. 初始化流 f 为零流,初始化缩放后的容量 c' 为全零。
  2. 对于 klog₂U 递减到 0(即从最高位到最低位处理容量的每个二进制位):
    • 加倍:将所有边的当前容量 c' 和当前流值 f 加倍。这相当于在二进制表示中左移一位。
    • 添加低位:对于每条边 e,将原容量 c(e) 的第 k 位加到 c'(e) 上。
    • 增广:在当前的残差图(基于 c'f)中,通过寻找增广路径,将流 f 提升为关于新容量 c' 的最大流。

正确性分析:我们保持一个不变式:在每次循环开始时,f 是关于当前容量 c' 的一个最大流。在开始时(全零)显然成立。加倍操作后,因为最小割的容量和穿过它的流量都翻倍了,所以最大流性质得以保持。添加低位后,流仍然是可行的(但可能不是最大的),因此随后的增广步骤可以将其恢复为最大流。

运行时间分析

  • 迭代次数O(log U),因为我们需要处理每个二进制位。
  • 每次迭代的时间:关键在于每次迭代中需要增加的流量 F* 是有限的。在添加新的低位之前,存在一个被当前流 f 饱和的最小割 C(残差容量为零)。该割最多包含 m 条边。添加低位后,最多给这个割增加了 m 个单位的容量(因为每条边最多加1)。因此,新的最小割值(也就是最大流值)最多为 m。如果使用朴素的Ford-Fulkerson算法进行增广,每次迭代时间为 O(m * F*) = O(m²)
  • 总时间O(m² log U)。这是一个弱多项式时间算法。

容量缩放是一个框架,我们可以用更快的算法(如下文介绍的阻塞流算法)来替代其中的增广步骤,从而获得更好的整体性能。


阻塞流算法 🛑

上一节我们介绍了容量缩放,本节我们来看看另一种独立且强大的技术:阻塞流。它将帮助我们将最大流算法的运行时间改进到强多项式范围。

首先定义一些概念。在残差图 G_f 中:

  • 顶点 v层级 level(v)sv 的最短路径距离(边权为1)。
  • (u, v)允许的,如果 level(v) = level(u) + 1
  • 一条路径是允许的,如果其所有边都是允许的。
  • 一个阻塞流 f 是一个可以分解为若干条允许的 s-t 路径的流,并且满足:每一条允许的 s-t 路径都至少有一条边被 f 饱和(即流量等于残差容量)。

关键点:阻塞流不一定是最大流,但它“阻塞”了所有最短的增广路径。

阻塞流算法的核心步骤

  1. 在当前的残差图中,构建层级图(只包含允许边)。
  2. 在层级图中找到一个阻塞流 f
  3. f 加到当前的总流中,更新残差图。
  4. 重复步骤1-3,直到 st 在残差图中不再连通。

关键引理:在沿一个阻塞流增广后,残差图中从 st 的最短路径距离严格增加
证明概要:考虑增广前后的层级图 LL'L' 中的边可能来自 LL 中边的反向边,或者其他非允许的“后向边”。任何 s-t 路径如果只使用 L 中的边,那么根据阻塞流的定义,其中至少有一条边已被饱和,不可能存在于 L' 中。如果使用反向边或后向边,路径长度必然增加。因此,L' 中的最短路径长度严格大于 L 中的最短路径长度。

由于最短路径距离最多为 n-1,所以上述主循环最多执行 O(n) 次。因此,总运行时间 = O(n) × (找到单个阻塞流的时间)


寻找阻塞流:单位容量情况

在单位容量图中,我们可以通过一个修改的DFS在 O(m) 时间内找到一个阻塞流。

算法维护一个当前顶点 v(初始为 s)和一条从 sv 的路径。有三种操作:

  1. 推进:如果 v 有未访问的允许出边指向 u,则移动到 u
  2. 增广:如果 v = t,则找到了一条 s-t 路径。沿该路径推送1单位流量(饱和路径上所有边),删除这些饱和边,并将当前顶点重置为 s
  3. 回退:如果 v 没有未访问的允许出边,则沿着来边回退到上一个顶点 w,并删除边 (w, v)。如果 v = s,则算法终止。

运行时间分析:每条边最多被考虑两次(一次推进,一次回退或增广后删除),因此总时间为 O(m)

结合主循环最多 O(n) 次,单位容量图上的最大流算法总时间为 O(mn)。更精细的分析(考虑距离增长与剩余流量之间的关系)可以将其改进到 O(m √m)O(m √n),对于二分图匹配等问题特别有效。


寻找阻塞流:一般容量情况

对于一般容量的图,上述DFS式算法仍然可用,但运行时间分析不同:

  • 增广次数:最多 m 次,因为每次增广至少饱和一条边。
  • 回退次数:最多 m 次,因为每次回退删除一条边。
  • 推进次数:关键观察是,每进行 n 次推进,必然发生一次增广或回退(否则将形成环或到达 t)。而每次增广或回退都删除一条边。因此,总推进次数为 O(m * n)

因此,找到一个阻塞流的时间为 O(m + m + m*n) = O(mn)

由于主循环需要 O(n) 次阻塞流计算,一般容量图上的阻塞流算法总运行时间为 O(m n²)。这是一个强多项式时间算法。

进一步优化:使用一种称为 Link-Cut Tree 的动态树数据结构,可以将寻找一个阻塞流的时间从 O(mn) 加速到 O(m log n)。这将总运行时间提升到 O(m n log n),非常接近当前已知的最佳强多项式时间界限 O(mn)


当前算法研究概览 📊

在本课程中,我们一起学习了最大流算法的基础改进方法。为了让大家了解全貌,这里简要列出一些重要的研究成果:

  • 强多项式时间算法
    • O(m n log n):通过阻塞流与Link-Cut Tree获得(Sleator & Tarjan, 1983)。
    • O(m n):目前最佳已知结果(Orlin, 2013)。
  • 弱多项式时间算法(与容量相关):
    • O(m min(m^{1/2}, n^{2/3}) log(n²/m) log U):Goldberg-Rao算法。
    • O(m √n log² U):Lee-Sidford算法(基于内点法)。
  • 特殊图:在单位容量图上,有更快的算法,例如 O(m √m)O(m n^{10/7})

总结

本节课中我们一起学习了两种构建更快速最大流算法的核心技术:

  1. 容量缩放:通过逐位处理容量,将大容量问题转化为一系列小容量子问题,得到 O(m² log U) 的弱多项式算法。
  2. 阻塞流:通过反复寻找并增广阻塞流,确保每次增广后最短路径距离增加,将迭代次数限制在 O(n) 次。在单位容量图中,我们可以在 O(m) 时间内找到阻塞流;在一般容量图中,朴素实现需要 O(mn) 时间,导致总时间为 O(m n²)。结合Link-Cut Tree可以优化到 O(m n log n)

这些算法是理解现代最大流算法发展的基础,它们展示了如何通过巧妙的组合搜索策略、图结构和数据分析来攻克经典的计算难题。

021:Link-Cut Trees 🧩

在本节课中,我们将学习一种名为 Link-Cut Trees 的数据结构。这种数据结构可以用于加速阻塞流算法的计算,同时也有其他应用。我们将首先回顾阻塞流算法,然后详细介绍 Link-Cut Trees 支持的操作及其实现原理。


回顾阻塞流算法

上一节我们介绍了阻塞流算法。该算法的核心步骤如下:

  1. 在剩余图中找到一个阻塞流。
  2. 沿着该阻塞流进行增广。
  3. 重复上述步骤,直到在剩余图中找不到从源点 S 到汇点 T 的路径。

算法的时间复杂度为 O(MN),其中 M 是边数,N 是顶点数。这是因为最多有 N-1 次迭代,而每次寻找阻塞流需要 O(M) 时间。

本节中,我们将看到如何使用 Link-Cut Trees 将寻找阻塞流的时间复杂度降低到 O(M log N)


Link-Cut Trees 是由 Sleator 和 Tarjan 在 1983 年提出的一种数据结构。它用于维护一个 森林(即一组有根树),并支持一系列高效的操作,从而加速阻塞流算法。

在阻塞流算法中,我们处理的是 层次图。Link-Cut Trees 的核心思想是:记住那些仍有剩余容量的路径片段(即“树”),并在后续搜索中直接“跳过”这些已知的片段,而不是重新进行深度优先搜索。


以下是 Link-Cut Trees 支持的核心操作,这些操作正是实现阻塞流算法所需要的:

  1. make_tree(v):创建一个只包含顶点 v 的新树。
  2. find_root(v):返回包含顶点 v 的树的根节点。
  3. link(v, w, c):假设 v 是其所在树的根,并且 vw 不在同一棵树中。此操作将 v 作为 w 的子节点连接起来,并设置连接边的容量为 c
  4. cut(v):断开顶点 v 与其父节点之间的边。
  5. find_min(v):返回从顶点 v 到其所在树根节点的路径上,容量最小的边。如果有多条边容量相同,则返回最靠近根节点的那条。
  6. subtract(v, x):将从顶点 v 到其所在树根节点的路径上,所有边的容量都减少 x

现在,我们来看看如何利用上述操作来实现阻塞流算法。以下是算法的伪代码:

# 初始化:为每个顶点创建一棵单节点树
for each vertex v:
    make_tree(v)

while True:
    v = find_root(S)  # 找到从S出发的当前片段的根(即最远可达点)
    
    if v == T:
        # 情况1:找到了一条S到T的路径,进行增广
        while True:
            e, min_cap = find_min(S)  # 找到路径上的最小容量边
            if min_cap == 0:
                cut(e)  # 删除容量为0的边
            else:
                break
        subtract(S, min_cap)  # 减少路径上所有边的容量
        cut(e)  # 删除刚刚饱和的边(最靠近根的那条)
        # 从层次图L中删除边e
        continue

    # 情况2:尝试前进(添加新边)
    if 在层次图L中,v有出边指向某个顶点w:
        link(v, w, capacity(v, w))  # 将当前片段扩展到w
        continue
    else:
        # 情况3:无法前进,需要回溯
        if v == S:
            break  # 无法找到阻塞流,算法结束
        else:
            # 删除所有指向v的边(v成为无用节点)
            for each child y of v in the maintained tree:
                cut(y)  # 断开y与v的连接
                # 从层次图L中删除边 (y, v)

算法分析

  • link 操作最多执行 M 次(每条边首次被探索时)。
  • cut 操作最多执行 M 次(每条边被删除时)。
  • find_minsubtract 操作与 cut 操作相关联,总次数也为 O(M)
  • 因此,如果每个 Link-Cut Tree 操作都能在 O(log N) 时间内完成,那么整个阻塞流算法的时间复杂度就是 O(M log N)

Link-Cut Trees 的关键在于,它使用 平衡二叉搜索树 来高效地维护森林中的树结构,特别是路径。

首选路径分解

对于森林中的每棵树,我们维护一个 首选路径分解

  • 每个非叶子节点都有一个 首选子节点。一个节点的首选子节点是:在其子树中,最后被访问 的节点所在的那个子节点。如果节点自身是最后被访问的,则它没有首选子节点。
  • 由首选边(连接节点与其首选子节点的边)构成的极大链,称为 首选路径
  • 整个树被分解成若干条首选路径,这些路径之间通过“非首选边”连接,形成一棵“路径的树”。

数据结构

  • 每条 首选路径 存储在一棵 平衡二叉搜索树 中(例如伸展树)。树中的键值是节点在原始树中的 深度(深度小的节点键值小)。
  • 我们称这些平衡二叉搜索树为 辅助树
  • 每棵辅助树的根节点,保存一个 路径父指针,指向该路径顶部节点在原始树中的父节点。这样就连接起了所有的辅助树,形成了对原始森林的表示。

核心操作:access(v)

几乎所有操作都以 access(v) 开始。它的作用是:使从根节点到 v 的路径成为新的首选路径

  1. v 旋转(splay)到其所在辅助树的根。
  2. 由于 v 成为最后访问的节点,它不应再有首选子节点。因此,断开其右子树(在辅助树中,右子树代表深度更大的节点,即原首选路径上 v 下方的部分),并将其路径父指针指向 v
  3. 循环处理:沿着路径父指针向上,将 v 所在的路径与上层的路径拼接,并断开旧的首选路径。这通过一系列的 splay 和指针修改操作完成。

执行 access(v) 后,从根到 v 的整条路径都位于同一棵辅助树中,并且 v 是这棵辅助树的根。

其他操作的实现

基于 access 操作,其他操作可以相对简单地实现:

  • find_root(v):先执行 access(v),然后在其辅助树中找到键值最小的节点(即深度最浅的根节点)。
  • find_min(v):先执行 access(v),此时 v 是辅助树根。通过维护子树的最小容量信息,可以直接查询从 v(代表路径底端)到树根(路径顶端)的最小容量边。
  • cut(v):先执行 access(v),然后断开 v 与其左子节点(在辅助树中代表其在原始树中的父节点)的连接。
  • link(v, w):先执行 access(v)access(w),然后将 v 的左子节点设为 w,并建立父子关系。

subtract(v, x) 操作可以通过在辅助树的节点上维护 懒标记 来实现,从而高效地对整条路径进行更新。


时间复杂度

access(v) 操作的时间复杂度与 首选子节点发生改变的次数 有关。Sleator 和 Tarjan 证明了,使用伸展树作为辅助树,一系列 M 次操作的总时间复杂度为 O((M + N) log N),即每次操作的 摊还 时间复杂度为 O(log N)。因此,阻塞流算法的总时间复杂度为 O(M log N)


总结

本节课中我们一起学习了 Link-Cut Trees 数据结构。

  1. 我们首先回顾了阻塞流算法,并指出了其性能瓶颈。
  2. 接着,我们介绍了 Link-Cut Trees 需要支持的操作,并展示了如何用它们实现更快的阻塞流算法。
  3. 然后,我们深入探讨了 Link-Cut Trees 的实现原理,核心是 首选路径分解 和基于伸展树的 辅助树 结构。
  4. 最后,我们了解了其 O(log N) 摊还时间复杂度的核心操作 access(v),以及其他操作如何基于它构建。

通过 Link-Cut Trees,我们可以将阻塞流算法的时间复杂度从 O(MN) 优化到 O(M log N),这是网络流算法中的一个重要优化。

022:高级算法分析

在本节课中,我们将要学习动态树(Link-Cut Trees)的摊销复杂度分析,并介绍最小费用最大流问题的基本概念和算法。


动态树分析

上一节我们介绍了动态树的基本操作。本节中我们来看看如何分析这些操作的摊销时间复杂度。

动态树的核心操作是 access(v),它使节点 v 成为其辅助树(auxiliary tree)的根。其运行时间与“偏好子节点变更”(Preferred Child Changes, PCC)的次数成正比。

access 操作的运行时间可以表示为:

运行时间 ∝ 1 + PCC 次数

对于一系列共 M 次操作,总运行时间最多为:

总运行时间 ≤ (单次 splay 的最坏情况代价) × M + 所有操作中 PCC 的总次数

我们知道单次 splay 操作的摊销代价是 O(log n)。因此,我们只需要界定 PCC 的总次数。

轻重路径分解

为了界定 PCC,我们引入一个分析工具:轻重路径分解(Heavy-Light Decomposition)。注意,这仅用于分析,数据结构本身并不维护它。

对于一个顶点 v,定义 size(v) 为其在表示树(represented tree)中的子树大小。

对于一条从父节点 v 到子节点 u 的边 (v, u)

  • 如果 size(u) ≤ size(v) / 2,则称该边为轻边
  • 如果 size(u) > size(v) / 2,则称该边为重边

在操作过程中,每条边可以是“偏好的”或“非偏好的”。一次 access 操作会破坏旧的偏好边并创建新的偏好边。

核心论断:PCC 的总次数最多为 O(LPCC + M + N),其中 LPCC 是“轻偏好边创建”(Light Preferred Child Creations)的次数。

以下是该论断的简要证明思路:

  1. 当创建一个新的偏好边时,它要么是轻的(计入 LPCC),要么是重的。
  2. 对于重偏好边的创建:
    • 情况一:父节点之前没有偏好子节点。这发生在一个子树首次被访问,或上次访问的就是该节点本身时。这种情况总共最多发生 O(N + M) 次(每个节点的首次访问,或每次访问操作本身)。
    • 情况二:父节点之前已有偏好子节点。由于一个节点最多只能有一个重子节点,这意味着我们同时破坏了一条旧的轻偏好边。我们可以将这次重边创建的代价“记入”到当初创建那条被破坏的轻边时。因此,重边创建的总次数被轻边创建的总次数所限定。

因此,界定 PCC 的关键在于界定轻偏好边创建(LPCC)的次数。

界定轻偏好边创建次数

现在我们来分析 accesslinkcut 操作分别会产生多少次 LPCC。

1. Access 操作
access 操作不改变表示树的结构,只改变边的偏好状态。在从根到 v 的路径上,轻边的数量最多为 O(log n),因为每经过一条轻边,子树大小至少减半。因此,一次 access 操作最多创建 O(log n) 条新的轻偏好边。

2. Link 操作
link(v, w) 首先执行 access(v)access(w),这两个操作的 LPCC 已由上述分析涵盖。随后,它添加边 (v, w)。这条新边的添加可能改变其他边的轻重状态,但不会直接创建新的偏好边(因为 vaccess 后没有偏好子节点)。因此,link 操作本身不产生额外的 LPCC。

3. Cut 操作
cut(v) 首先执行 access(v),其 LPCC 已涵盖。随后,它切断 v 与其父节点的边。这可能导致原偏好路径上的一些边由重变轻,从而可能创建新的轻偏好边。然而,这些新创建的轻偏好边仍然位于一条从根出发的路径上,数量最多为 O(log n)

最终复杂度

综合以上分析,在 M 次操作中:

  • 每次 access 带来 O(log n) 次 LPCC。
  • linkcut 带来的额外 LPCC 也是每次 O(log n)
  • 因此,LPCC 总次数为 O(M log n)
  • 进而,PCC 总次数为 O(M log n + M + N) = O(M log n)(假设 M ≥ N)。

结合 splay 操作 O(log n) 的摊销代价,我们得到使用普通平衡二叉搜索树(BST)实现辅助树时,动态树每次操作的摊销时间复杂度为 O(log² n)

改进至 O(log n):在问题集(PSet)中,你将看到如何通过使用伸展树(splay tree)及其特定的势能分析,将摊销复杂度进一步改进到 O(log n)。其核心是利用讲座7中证明的伸展树访问引理(Access Lemma)。


最小费用最大流

现在,我们转向另一个主题:最小费用最大流。

问题定义

在最小费用最大流问题中,图中每条边 e 不仅有一个容量 u(e),还有一个费用 c(e)(可以是负数)。目标是找到一个最大流 f,使得总费用最小:

总费用 = Σ_{e} (f(e) * c(e))

在残差图中,反向边的费用是原边费用的相反数。

一个相关问题是最小费用环流(Min Cost Circulation),即满足所有节点流量守恒(流入等于流出)的流,目标也是最小化总费用。这两个问题可以高效地相互归约。

基本算法

一个直观的算法类似于最大流的 Ford-Fulkerson 方法,但寻找的是残差图中的负费用增广环

算法框架

当残差图中存在负费用环时:
    找到这样一个环 C
    沿环 C 推送尽可能多的流量(即环上边的最小剩余容量)

正确性:如果当前环流 f 不是最优的,则存在一个更优的环流 f*。差值 f* - f 是残差图中的一个负费用环流。任何环流都可以分解为若干个环的叠加,因此其中至少包含一个负费用环。找到并增广这个环即可降低总费用。

复杂度:每次迭代需要检测负环(例如使用 Bellman-Ford 算法,O(mn))。每次增广至少使总费用减少1。由于总费用的绝对值上界为 O(mUC)U 是最大容量,C 是最大费用绝对值),故迭代次数可能高达 O(mUC),这是一个伪多项式时间的算法。

改进:价格函数与简化费用

为了获得更高效的算法,我们引入价格函数 p(v),它为每个顶点分配一个势能。

对于边 (u, v),定义其简化费用为:

c_p(u, v) = c(u, v) + p(v) - p(u)

关键性质是:沿着任何环,简化费用之和与原费用之和相等(因为顶点势能项相互抵消)。

最优性条件:一个环流 f 是最小费用环流,当且仅当存在一个价格函数 p,使得在残差图 G_f 中,所有边的简化费用都非负

  • 充分性:如果所有简化费用非负,则残差图中无负费用环,因此当前流最优。
  • 必要性:如果无负环,可以定义 p(v) 为从某个新源点 sv 的最短路径距离(以原费用为边权)。根据三角不等式,由此定义的简化费用非负。

单位容量非负费用特殊情况

考虑一个简单情况:所有边容量为1,且初始图中没有负费用边。

算法

当存在从 s 到 t 的路径时:
    在残差图中,以简化费用为边权,找到从 s 到 t 的最短路径 P。
    沿路径 P 推送 1 单位流量。

关键观察:如果我们始终将价格函数 p(v) 维护为从源点 sv 的最短路径距离(以当前简化费用或原费用计算,在非负条件下等价),那么:

  1. 被增广的边都在最短路径上,其简化费用为0。
  2. 增广后,引入残差图的反向边,其简化费用为原边简化费用的相反数,即也为0。
  3. 因此,残差图中所有边的简化费用始终保持非负。这使得我们可以一直使用 Dijkstra 算法来寻找最短增广路。

复杂度:最多进行 O(nU) 次增广(对于单位容量,U 可视为常数或与 m 相关)。每次使用 Dijkstra 算法找最短路,若使用 Fibonacci 堆,则每次 O(m + n log n)。对于单位容量图,总复杂度可以控制得更好。通过容量缩放技术,此算法可以推广到处理任意容量和非负费用的图。对于包含负费用边的图,可以通过预处理将其转化为非负费用的情况。


本节课中我们一起学习了动态树操作 O(log² n) 摊销复杂度的分析,并介绍了最小费用最大流问题的基本概念、最优性条件以及一个针对特殊情形的高效算法。在问题集中,你将有机会把动态树的复杂度改进到 O(log n),并探索用于解决更一般最小费用流问题的缩放算法。

023:快速指数时间算法

在本节课中,我们将学习几种处理NP难问题的方法,重点介绍如何设计运行时间虽为指数级但尽可能“快速”的算法。我们将探讨指数分治法、剪枝暴力搜索、随机化算法以及容斥原理等技巧,并了解如何通过它们来降低时间复杂度或空间复杂度。


指数分治法

上一节我们提到了近似算法,本节我们来看看另一种思路:直接解决指数时间问题,但尽可能减小指数。指数分治法就是其中一种技巧。

旅行商问题 是一个经典例子。给定一组点 P、一个距离函数 d(满足度量性质)以及两个指定的起点 s 和终点 t,目标是找到一条从 st 的路径,恰好访问每个其他顶点一次,并使得路径总长度最小。

最直接的暴力算法是尝试所有中间顶点的排列,时间复杂度约为 O*(n!),空间复杂度为多项式级。另一种方法是动态规划,其状态数为 O*(2^n),因此时间和空间复杂度均为 O*(2^n)

指数分治法则提供了一种不同的权衡。其核心思想是递归地将顶点集 U 划分为两个大小大致相等的子集 ST,并考虑路径在某个中间点 m 处从 S 切换到 T。算法尝试所有可能的分割方式和中间点。

以下是该递归关系的核心公式:

OPT(U, s, t) = min over subsets S of U (where |S| ≈ |U|/2) and points m in (S ∪ {t}) ∩ (T ∪ {s}) of [ OPT(S, s, m) + OPT(T, m, t) ]

该算法的时间复杂度递归式大致为 T(n) = O(n * C(n, n/2) * T(n/2)),最终解得 T(n) = O*((4+ε)^n)。关键在于,其空间复杂度仅为多项式级,因为递归深度为 O(log n),且每层只需存储当前顶点集。


剪枝暴力搜索

接下来,我们看看如何通过“剪枝”来优化暴力搜索。我们以 3-SAT问题 为例。

给定一个由 m 个子句构成的合取范式 φ,每个子句是三个文字的析取,目标是判断是否存在一个对 n 个变量的赋值,使得整个公式为真。

最朴素的暴力搜索时间复杂度为 O*(2^n)。一个简单的递归算法是:选择一个未满足的子句,并依次尝试将其中的每个文字设为真,然后递归检查简化后的公式。这会产生形如 T(n) ≤ 3T(n-1) + poly(n) 的递归式,解为 O*(3^n)

然而,我们可以通过剪枝来优化:

  1. 在递归调用前,简化公式(例如,移除已满足的子句,缩短包含已赋值变量的子句)。
  2. 如果尝试将某个变量设为 后递归调用返回 ,那么在后续尝试中可以推断该变量必须为

经过这些优化,递归式变为 T(n) ≤ T(n-1) + T(n-2) + T(n-3) + poly(n),其解约为 O*(1.839^n)。通过更精细的分析和更多剪枝规则,这个常数可以进一步降低。

另一种思路是:假设存在一个满足赋值 A*。我们从一个随机赋值 A 开始,它与 A* 最多在 n/2 个变量上不同。算法不断寻找不满足的子句,并递归地尝试翻转该子句中的三个变量之一。如果递归深度超过 n/2 则放弃。这个算法的时间复杂度为 O*(3^(n/2)) = O*(1.732^n)


随机化算法

本节我们将介绍一种利用随机化来获得更好指数基数的算法:Schöning 算法

该算法非常简单:

  1. 均匀随机选择一个初始赋值 A
  2. 重复以下步骤最多 T 次:
    • 如果 φA 满足,则成功返回。
    • 否则,任选一个不满足的子句 C,并随机翻转 C 中的一个变量。

我们通过分析一个马尔可夫链来评估其成功率。链的状态表示当前赋值 A 与某个固定满足赋值 A* 之间的汉明距离 i。在每一步,我们选择一个不满足的子句,其中至少有一个变量在 A* 中取值不同,因此以至少 1/3 的概率减少距离(向 0 移动),以至多 2/3 的概率增加距离。

通过分析从距离 k 出发、恰好经过 3k 步到达 0 的概率(即向右走 k 步,向左走 2k 步),我们可以证明,单次运行算法在 3n 步内成功的概率 P 至少为 Ω((3/4)^n / √n)

因此,如果我们重复运行算法 O((4/3)^n * √n * log(1/δ)) 次,就能以至少 1-δ 的概率找到一个满足赋值(如果存在的话)。这给出了一个 O*((4/3)^n) 的随机算法。后续的研究结合了更多技巧,得到了当前最好的指数基数约 1.307^n


容斥原理与快速莫比乌斯变换

最后,我们探讨如何利用容斥原理设计算法,并关联到快速莫比乌斯变换。我们以 k-着色问题 为例。

给定一个无向图 G=(V,E),判断是否可以用 k 种颜色对顶点着色,使得任意相邻顶点颜色不同。这等价于判断能否将顶点集 V 划分成 k 个独立集。

暴力搜索的时间复杂度为 O*(k^n)。一个动态规划算法可以做到 O*(3^n) 的时间和 O*(2^n) 的空间。利用容斥原理,我们可以得到不同的权衡。

容斥原理的核心恒等式如下:对于任意集合 R ⊆ T,有

∑_{S: R ⊆ S ⊆ T} (-1)^{|T \ S|} = [R = T]

其中 [·] 是指示函数,条件为真时值为1,否则为0。

对于k-着色问题,定义函数 f(S),当 S 是非空独立集时为1,否则为0。再定义其 zeta变换(或称为和变换)为:

f̂(S) = ∑_{R ⊆ S} f(R)

它计算了 S 的子集中非空独立集的个数。

关键的结论是:图 Gk-可着色的,当且仅当

∑_{S ⊆ V} (-1)^{n - |S|} * (f̂(S))^k > 0

这个结论可以直接从容斥原理推导得出。

现在,问题转化为计算所有子集 Sf̂(S) 值。这可以通过 快速莫比乌斯变换O*(2^n) 的时间和空间内完成。更有趣的是,通过一种称为“子集卷积”的技巧或特定的递归计算,我们可以在 O*(3^n) 的时间和多项式空间内完成计算。这为我们提供了时间与空间的不同权衡选择。


本节课中,我们一起学习了多种设计快速指数时间算法的技术:指数分治法能在多项式空间内获得比暴力搜索更优的时间;剪枝暴力搜索通过逻辑推理减少递归分支;随机化算法利用随机游走分析获得优异的指数基数;容斥原理则提供了将计数问题转化为变换计算的新视角,并能与快速莫比乌斯变换结合,灵活权衡时间与空间复杂度。这些工具是处理NP难问题的重要武器。

024:容斥原理与流式算法

在本节课中,我们将完成对“更快的指数时间算法”或“小空间算法”的讨论。具体来说,我们将深入探讨容斥原理、Zeta变换和莫比乌斯反演。之后,我们将转向我研究的一个领域:流式计算与草图技术。

容斥原理与Zeta变换

上一节我们介绍了容斥原理。对于所有集合 R ⊆ T,我们证明了以下等式成立:

∑_{R ⊆ S ⊆ T} (-1)^{|T| - |S|} = [R = T]

其中,[布尔表达式] 表示当表达式为真时值为1,否则为0。

我们定义了Zeta变换。对于一个将集合映射到某个环的函数 F,其Zeta变换 定义为:

F̂(S) = ∑_{R ⊆ S} F(R)

上一讲我们以K染色问题结束。我们定义函数 F(S) 为指示函数,当集合 S 是图 G 的非空独立集时为1,否则为0。我们有一个结论:图 G 是K可染色的,当且仅当:

∑_{S ⊆ V} (-1)^{|V| - |S|} (F̂(S))^K > 0

我们提到,这个公式可以让我们更高效地解决问题。这个结论的证明细节已包含在笔记中。接下来,我将展示这个结论的证明,并将其作为更一般原理的一个例子。

莫比乌斯反演

我们定义了莫比乌斯反演公式。对于函数 F,其莫比乌斯反演 定义为:

F̃(S) = ∑_{R ⊆ S} (-1)^{|S| - |R|} F(R)

Zeta变换和莫比乌斯反演的核心关系由以下命题体现:

F̂̃ = F̃̂ = F

这意味着,对 F 先进行Zeta变换再进行莫比乌斯反演,或者先进行莫比乌斯反演再进行Zeta变换,都会得到原始的 F。我们将证明其中一个方向,另一个方向类似。

证明: 我们证明 F̃̂ = F。对于任意集合 T,计算 F̃̂(T)

F̃̂(T) = ∑_{S ⊆ T} F̃(S)
      = ∑_{S ⊆ T} ∑_{R ⊆ S} (-1)^{|S| - |R|} F(R)
      = ∑_{R ⊆ T} F(R) ∑_{R ⊆ S ⊆ T} (-1)^{|S| - |R|}

根据容斥原理,内层求和 ∑_{R ⊆ S ⊆ T} (-1)^{|S| - |R|} 仅在 R = T 时为1,否则为0。因此,整个表达式等于 F(T)。证毕。

K染色问题的推导

现在,我们来看K染色问题中的公式是如何从这个一般原理中自然得出的。

定义函数 G(S) 为:将集合 S 写成 K 个非空独立集之并的方式数。图 G 是K可染色的,当且仅当 G(V) > 0

我们注意到,Ĝ(S) 恰好等于 (F̂(S))^K。这是因为 Ĝ(S) 计算的是所有包含于 SK 个非空独立集的选择方式数,而 F̂(S) 是包含于 S 的非空独立集的数量,其 K 次方正好对应了有序选择 K 个独立集的方式数。

因此,根据莫比乌斯反演公式,我们有:

G(V) = Ĝ̃(V) = (F̂^K)̃(V)

这正是我们之前用于判断K可染性的求和公式的来源。

算法实现与复杂度

我们声称,可以顺序计算所有 F̂(S)F̃(S) 的值,在 O*(3^n) 时间和多项式空间内完成。这里的 O* 记号忽略了多项式因子。

朴素算法: 对于每个集合 S,计算 F̂(S) 需要求和所有 R ⊆ SF(R),这需要 2^{|S|} 时间。对所有 S 的总时间是 ∑_{S ⊆ V} 2^{|S|} = ∑_{i=0}^{n} C(n, i) 2^i = 3^n。空间上只需多项式开销。

更快的算法(Yates算法): 该算法可以在 O*(2^n) 时间和空间内计算Zeta变换。这是一种动态规划算法。

我们定义辅助函数 G(i, S),其中 S_i 表示集合 S 中大于 i 的元素构成的子集。G(i, S) 定义为:

G(i, S) = ∑_{R ⊆ S, S_i = R_i} F(R)

我们最终需要 F̂(S) = G(n, S)G(i, S) 满足以下递推关系:

  • 基础情况:G(0, S) = F(S)
  • 递推关系:G(i, S) = G(i-1, S) + [i ∈ S] * G(i-1, S \ {i})

状态数为 n * 2^n,每个状态的计算是常数时间,因此总时间和空间为 O*(2^n)。莫比乌斯反演的计算有极其相似的动态规划算法。

总结: 因此,我们可以在大约 2^n 的时间(忽略多项式因子)内判定K可染色性。值得注意的是,通过约简,我们也能在相似复杂度内实际找到一个K染色方案。

流式算法简介

在接下来的课程中,我们将讨论一个完全不同的主题:流式计算与草图技术。

流式算法关注的是在数据流持续到达时,使用远小于数据本身大小的内存空间来维护一些信息或回答查询。

模型: 我们有一个高维向量 x ∈ R^n,初始为零向量。数据流由一系列更新操作组成,每个更新是一个元组 (i, v),表示将 x_i 增加 v(在“严格递增”模型中,我们通常假设 v = 1)。在流结束后,我们需要回答关于 x 的某些查询。

目标: 使用远小于 n(存储整个向量)或 m log n(存储整个流,m 为流长度)的内存空间。

示例问题:F0估计(不同元素计数)
查询是向量 x 的支撑集大小,即非零坐标的个数。在IP流量监控的例子中,这对应于看到过的不同IP地址的数量。

  • 确定性精确算法不可能: 任何确定性算法解决精确F0问题都需要 Ω(n) 位空间。证明采用编码论证法:如果存在一个使用 S < n 位空间的算法,那么我们可以利用它来构造一个将 n 位字符串压缩到 S 位字符串的单射,这与鸽巢原理矛盾。
  • 随机化近似算法可行: 允许随机化和近似后,我们可以在亚线性空间内解决问题。具体来说,我们可以设计一个算法,以至少 2/3 的概率输出一个估计值,该估计值与真实F0的误差在 ε 倍以内。

Flajolet-Martin算法思想

我们描述一个理想化的随机算法来直观理解为什么近似是可能的。

  1. 假设我们有一个完全随机的哈希函数 h: [n] → [0, 1],将每个坐标均匀映射到 [0, 1] 区间。
  2. 算法维护一个值 Z。对于流中出现的每个索引 i,计算 h(i),并令 Z = min(Z, h(i))。初始化 Z = 1
  3. 查询时,输出估计值 1/Z - 1

直观分析: 假设有 F0 个不同的元素。将它们哈希到 [0, 1] 区间,我们期望这些值大致均匀分布。最小值 Z 的期望大约是 1/(F0 + 1)。因此,1/Z - 1 的期望大约是 F0。通过计算 Z 的方差,并应用切比雪夫不等式,可以证明估计值以一定概率接近真实值。

为了将误差控制在 (1 ± ε) 因子内并提高成功概率,我们可以并行运行 r = O(1/ε^2) 个独立的哈希函数和计数器,记录各自的 Z_j,最终输出 (1 / avg(Z_j)) - 1。取平均可以降低方差,从而满足精度要求。

实际实现: 上述是理想化版本。实际中,我们需要使用有限精度表示哈希值,并使用有限独立性的哈希函数族(如 pairwise independent hash)来替代完全随机的哈希函数,以节省存储哈希函数本身的空间。

总结

本节课我们一起学习了:

  1. 容斥原理、Zeta变换与莫比乌斯反演的一般形式,并看到K染色问题如何作为其一个具体应用。
  2. 计算这些变换的算法复杂度:朴素算法需要 O*(3^n) 时间和多项式空间;Yates动态规划算法可以优化到 O*(2^n) 时间和空间。
  3. 引入了流式计算模型,其核心目标是用极小的内存处理大规模数据流。
  4. F0估计(不同元素计数) 问题为例,说明了确定性精确算法的空间下界,并介绍了随机化近似算法(如Flajolet-Martin算法)的基本思想,它利用哈希和概率估计在亚线性空间内解决问题。

025:随机符号的力量 🎲

在本节课中,我们将学习如何利用随机符号矩阵(Random Sign Matrices)这一强大工具,来解决流式算法(Streaming Algorithms)中的几个核心问题。我们将重点探讨L2范数估计、快速线性回归、点查询以及降维技术。这些技术允许我们在仅使用少量内存的情况下,处理大规模、动态更新的数据流,并支持数据的插入和删除操作。


流式模型与线性草图 📊

上一节我们回顾了流式模型的基本设定。本节中,我们将正式引入线性草图(Linear Sketch)这一核心概念,它是支持数据删除操作的关键。

在流式模型中,我们有一个巨大的向量 x ∈ ℝⁿ。我们无法存储整个 x,只能接收一系列形如 xᵢ → xᵢ + v 的更新。我们的目标是,在流结束后,能够回答关于 x 的某个函数 f(x) 的查询。

线性草图的思想是:我们选择一个 m × n 的矩阵 Π(其中 m << n),并始终在内存中维护一个压缩后的向量:
y = Πx

初始时,x 是零向量,所以 y 也是零向量。当我们在流中看到更新 xᵢ → xᵢ + v 时,我们只需执行:
y → y + v · Πᵢ
其中 Πᵢ 是矩阵 Π 的第 i 列。无论 v 是正(插入)还是负(删除),这个更新操作都有效。

接下来的问题是:如何为不同的查询函数 f(x) 设计合适的矩阵 Π,以及如何从草图 y 中估计出 f(x)


L2范数估计 📏

我们首先考虑如何估计向量 xL2范数的平方,即 ||x||₂²。我们将看到,使用随机符号矩阵可以高效地解决这个问题。

算法描述

我们选择矩阵 Π,其每个元素 Πᵢⱼ 独立地随机取 +1/√m-1/√m,概率各为1/2。
我们维护草图 y = Πx
当查询到来时,我们输出 ||y||₂² 作为 ||x||₂² 的估计值。

算法分析

我们需要证明,以高概率,估计值 ||y||₂² 是真实值 ||x||₂²(1 ± ε) 近似。

我们使用切比雪夫不等式进行分析。首先计算期望:

E[||y||₂²] = E[∑ᵣ yᵣ²] = ∑ᵣ E[yᵣ²]

对于固定的 ryᵣ = (1/√m) ∑ᵢ σᵣᵢ xᵢ,其中 σᵣᵢ 是随机符号。
因此,

yᵣ² = (1/m) (∑ᵢ σᵣᵢ² xᵢ² + ∑_{i≠j} σᵣᵢ σᵣⱼ xᵢ xⱼ)

由于 σᵣᵢ² = 1,且对于 i≠jE[σᵣᵢ σᵣⱼ] = E[σᵣᵢ] E[σᵣⱼ] = 0(独立性)。
所以,

E[yᵣ²] = (1/m) ||x||₂²

进而,

E[||y||₂²] = ∑ᵣ (1/m) ||x||₂² = ||x||₂²

期望是正确的。

接下来分析方差 Var(||y||₂²)。由于 yᵣ 是独立的,方差可加:

Var(||y||₂²) = ∑ᵣ Var(yᵣ²)

通过计算 E[yᵣ⁴] 并利用随机符号的奇次矩为零的性质,可以证明:

Var(yᵣ²) ≤ (2/m²) ||x||₂⁴

因此,

Var(||y||₂²) ≤ m * (2/m²) ||x||₂⁴ = (2/m) ||x||₂⁴

现在应用切比雪夫不等式:

Pr[ | ||y||₂² - ||x||₂² | ≥ ε ||x||₂² ] ≤ Var(||y||₂²) / (ε² ||x||₂⁴) ≤ (2/m) / ε²

如果我们设置 m = O(1/ε²),那么失败概率可以降至一个常数(例如1/3)。

提升成功概率

为了将成功概率提升到 1 - δ,一个标准的技巧是并行运行 k = O(log(1/δ)) 个独立的草图,得到估计值 Y₁, ..., Yₖ,然后输出它们的中位数。利用切尔诺夫界限可以证明,这能将失败概率指数级降低。

空间与时间优化

  • 空间: 直接存储稠密矩阵 Π 需要 O(mn) 空间,这是不可接受的。但分析表明,我们只需要 Π 的条目是 4-wise独立 的。我们可以使用仅需 O(log n) 位种子描述的哈希函数族来生成这样的矩阵,从而将空间降至 O(log n * log(1/δ))
  • 更新时间: 上述稠密矩阵的更新需要 O(m) = O(1/ε²) 时间,因为要更新 y 的所有 m 个分量。为了加速,可以使用 稀疏化 的随机符号矩阵(例如,Thorup-Zhang 草图),每列只有一个非零元。这可以将更新时间降至 O(1),同时保持相同的空间和精度保证。

结论: 我们可以在 O(ε⁻² log(1/δ) log n) 的空间和 O(log(1/δ)) 的更新时间内,以 1-δ 的概率获得 ||x||₂²(1±ε) 近似估计。这对于检测网络流量中的突发(例如DDoS攻击)非常有用。


快速线性回归 🚀

上一节我们看到了如何估计单个向量的范数。本节中,我们将把随机符号矩阵应用于线性回归问题,显著加速求解过程。

问题回顾

在线性回归中,我们有观测矩阵 A ∈ ℝⁿˣᵈ(n >> d)和观测向量 b ∈ ℝⁿ。我们希望找到参数向量 x ∈ ℝᵈ,最小化残差平方和:

x* = argmin_x ||Ax - b||₂

经典解为 x* = (AᵀA)⁻¹ Aᵀb。计算 AᵀA 的瓶颈时间复杂度为 O(nd²)

子空间嵌入(Subspace Embedding)

关键思想是使用一个随机符号矩阵 Π ∈ ℝᵐˣⁿ(m << n)来压缩数据。我们称 Π 是子空间 V = span(A, b) 的一个 ε-子空间嵌入,如果对于所有 vV,都有:

(1 - ε) ||v||₂² ≤ ||Πv||₂² ≤ (1 + ε) ||v||₂²

这意味着 Π 近似保留下子空间 V 中所有向量的范数。

算法与保证

如果我们有这样的 Π,我们可以求解压缩后的回归问题:

x̃* = argmin_x ||ΠAx - Πb||₂

可以证明,这个解几乎和原问题的最优解一样好:

||A x̃* - b||₂ ≤ √((1+ε)/(1-ε)) * ||A x* - b||₂

计算优势: 原问题的瓶颈 AᵀA 需要 O(nd²) 时间。压缩后,我们需要计算 (ΠA)ᵀ(ΠA),这只需要 O(md²) 时间。如果我们能构造一个 m 仅依赖于 dε,而与巨大 n 无关的子空间嵌入,就能实现巨大加速。

构造与效率

可以证明,使用类似 Thorup-Zhang 的稀疏随机符号矩阵作为 Π,只要取 m = O(d²/ε²),它就以高概率成为子空间 V 的嵌入。

  • 计算 ΠA: 由于 Π 极度稀疏(每列一个非零元),计算 ΠA 的时间与 A 的非零元数量成正比,通常非常快。
  • 总时间: 因此,总运行时间从 O(nd² + d³) 降至 O(nnz(A) + d³/ε²),其中 nnz(A)A 的非零元个数。这对于大规模回归问题至关重要。

点查询(Point Query)🔍

现在,我们考虑流式模型中的另一种查询:点查询。即,用户询问特定坐标 i 的值 xᵢ。我们允许近似回答,形式为 xᵢ ± ε||x||₁

算法思路

我们希望找到一个矩阵 Π,使得其列向量 πᵢ 具有以下性质:

  1. ||πᵢ||₂ = 1
  2. 对于任何 i ≠ j,有 |⟨πᵢ, πⱼ⟩| ≤ ε

如果我们有这样的 Π,并维护草图 y = Πx。那么当查询 xᵢ 时,我们计算:

x̃ᵢ = πᵢᵀ y = πᵢᵀ (Πx)

展开可得:

x̃ᵢ = xᵢ + Σ_{j≠i} xⱼ ⟨πᵢ, πⱼ⟩

由于 |⟨πᵢ, πⱼ⟩| ≤ ε,所以 |x̃ᵢ - xᵢ| ≤ ε Σ_{j≠i} |xⱼ| ≤ ε ||x||₁。这正是我们想要的近似保证。

如何构造这样的 Π?

这可以通过 约翰逊-林登斯特劳斯引理(Johnson-Lindenstrauss Lemma, JL引理) 直接得到。
我们将JL引理应用于一个包含 0n 个标准基向量 e₁, ..., eₙ 的点集。
JL引理保证,存在一个映射(例如随机符号矩阵投影)到 m = O(ε⁻² log n) 维空间,能近似保持所有这些点对之间的距离。

  • 保持 eᵢ0 的距离,意味着 ||Πeᵢ||₂ ≈ 1(可后续归一化)。
  • 保持 eᵢeⱼ (i≠j) 之间的距离,因为原距离为 √2,而 ||Πeᵢ - Πeⱼ||₂² = ||Πeᵢ||₂² + ||Πeⱼ||₂² - 2⟨Πeᵢ, Πeⱼ⟩ ≈ 2,结合范数约等于1,可推导出 |⟨Πeᵢ, Πeⱼ⟩| ≈ 0,实际上可以 bounded by ε

因此,用JL引理构造的矩阵 Π 的列,就近似满足我们所需的正交性条件。


降维(Dimensionality Reduction)📉

最后,我们简要探讨随机符号矩阵在降维方面的经典应用,即JL引理本身。

JL引理陈述

对于任何 N 个点组成的集合 X ⊂ ℝⁿ,存在一个映射 f: ℝⁿ → ℝᵐ,其中 m = O(ε⁻² log N),使得对于所有 u, v ∈ X,都有:

(1 - ε) ||u - v||₂ ≤ ||f(u) - f(v)||₂ ≤ (1 + ε) ||u - v||₂

即,f 近似保持了集合中所有点对之间的距离。

证明思路

我们使用随机投影作为 f,具体来说,f(x) = (1/√m) Π x,其中 Πm × n 的随机符号矩阵。
证明的关键是,对于任意固定的单位向量 z(例如 z = (u-v)/||u-v||),我们需要证明:

Pr[ | ||Πz||₂² - 1 | > ε ] 非常小(例如 < 1/N²)

一旦对单个向量成立,再对 X 中所有 C(N,2) 个点对对应的向量进行联合界(Union Bound),就能得到全局结论。

对单个向量的分析,与L2范数估计类似,但需要比切比雪夫不等式更强的尾部分析(例如使用矩生成函数或更高阶矩),才能得到 exp(-Ω(ε²m)) 形式的失败概率,从而反推出 m = O(ε⁻² log N) 的维度足以使联合失败概率小于1。


总结 🎯

本节课我们一起探索了随机符号矩阵在流式算法和降维中的强大力量:

  1. L2范数估计: 通过维护随机投影草图 y = Πx,可以用 O(ε⁻² log(1/δ)) 的空间估计 ||x||₂²,并支持流式更新和删除。
  2. 快速线性回归: 利用随机投影作为子空间嵌入,可以将大规模回归问题的计算瓶颈从 O(nd²) 降至与 n 无关的量级,实现显著加速。
  3. 点查询: 通过设计列向量近似正交的投影矩阵,可以从草图中近似恢复单个坐标的值。
  4. 降维(JL引理): 随机投影可以将高维空间中的点集嵌入到低维空间,同时近似保持所有点对之间的距离,这是许多机器学习算法的理论基础。

这些技术背后的共同点是线性草图的抽象、随机投影的度量保持特性,以及通过概率分析(期望、方差、尾界限)来证明其有效性。它们构成了处理大规模数据算法的核心工具箱之一。

posted @ 2026-03-29 09:38  布客飞龙I  阅读(15)  评论(0)    收藏  举报