《算法分析与设计》学习笔记
作业占 \(20\%\),期中占 \(40\%\),项目(读 Paper,尝试实现或改进)占 \(40\%\)。\(\newcommand{\de}{\delta}\newcommand{\al}{\mathcal}\newcommand{\fr}{\frac}\newcommand{\tio}{\tilde {\al O}}\)
Lecture 1
内容:Dijkstra,APASP,近似距离查询。
很多内容 xtq 在 WC2024 上讲过。
最短路问题
记 \(\de(s, t)\) 表示 \(s\) 到 \(t\) 的最短路,满足三角形不等式。
使用斐波那契堆优化的 Dijkstra 的复杂度为 \(\mathcal{O}(m + n \log n)\)。
对于 APSP,Floyd-Warshall 的复杂度为 \(\mathcal{O}(n ^ 3)\)。更优?无向无权图 \(\al{O}(n ^ {\omega})\approx \al{O}(n ^ {2.38})\),有向无权图 \(\mathcal{O}(n ^ {\mu})\approx \mathcal{O}(n ^ {2.53})\)。猜想:当边权是 \(1\sim n\) 的整数时,不存在 \(\mathcal{O}(n ^ {3 - \de})\) 的 APSP 算法。
无向无权图 APASP
设 \(d(u, v)\) 为近似距离。
- stretch \(k\) 近似满足 \(\de(u, v) \leq d(u, v)\leq k\de(u, v)\)。
- surplus \(t\) 近似满足 \(\de(u, v) \leq d(u, v)\leq \de(u, v) + t\)。
\(\tio(n ^ {2.5})\) surplus \(2\)
准备工作
独立随机以 \(\fr 1 k\) 的概率选中一个点,得到期望大小为 \(\al {O}(\fr n k)\) 的点集 \(D\)。对于度为 \(d\) 的点,其有 \(1 - (1 - \fr 1 k) ^ d\) 的概率和 \(D\) 相邻。取 \(d = ck\log n\),则概率为 \(1 - \fr 1 {n ^ c}\)。
定理
给定度数 \(d\),可以找到大小为 \(\tio(\fr n d)\) 的点集 \(D\),满足所有度不小于 \(d\) 的点大概率和 \(D\) 相邻。
设 \(V_1\) 是所有度不小于 \(\sqrt n\) 的点,\(D_1\) 是大小为 \(\tio (\sqrt n)\) 的点集,满足 \(V_1\) 的每个点和 \(D_1\) 相邻。
设 \(E_2\) 是所有 \((u, v)\) 满足 \(u\notin V_1\) 或 \(v\notin V_1\),则 \(\deg(u)\) 或 \(\deg(v)\) 不超过 \(\sqrt n\),\(|E_2| = \al O(n\sqrt n)\)。
算法流程
- 从每个 \(u\in D_1\) 跑 BFS 求出 \(\de(u, v)\),其中 \(u\in D_1\),\(v\in V\)。时间 \(\al O(|D_1||E|) = \tio (n ^ 2\sqrt n)\)。
- 从每个 \(u\in V\) 在 \(E_2\cup \de(D_1\times V)\) 上跑 Dijkstra。时间 \(\tio(n(|E_2| + n|D_1|)) = \tio(n ^ 2\sqrt n)\)。
算法的时间复杂度 \(\tio(n ^ 2\sqrt n)\)。
算法分析
- 若 \(u\in D_1\) 或 \(v\in D_1\),则 \(\de(u, v)\) 在第一步求出。
- 若 \(u, v\) 最短路径上所有点的度小于 \(\sqrt n\),则每条边在 \(E_2\) 上,\(\de(u, v)\) 在第二步求出。
- 若存在度不小于 \(\sqrt n\) 的点 \(w\),则 \(w\) 和 \(w'\in D_1\) 相邻,\(d(u, v)\leq \de(u, w') + \de(w', v) \leq \de(u, v) + 2\)。
通过 \(D_1\) 把路径上度数较大的点移动到较小的集合内。
\(\tio(n ^ {7 / 3})\) surplus \(2\)
这个算法还有可以利用的性质:最短路径要么只有 \(\de(D_1\times V)\),要么只有 \(E_2\),两部分独立。\(\delta(D_1\times V)\) 的部分已经是近似长度,但 \(E_2\) 的部分求出了精确值,这里还有优化空间。
改改阈值会发生什么?
设 \(V_1\) 是所有度不小于 \(n ^ {2 / 3}\) 的点,则 \(|D_1| = \tio(n ^ {1 / 3})\)。算法的第一步时间 \(\tio (n ^ {7 / 3})\),但 \(|E_2| = \al O(n ^ {5 / 3})\),无法 Dijkstra。这一步解决了 \(u\) 或 \(v\) 度不小于 \(n ^ {2 / 3}\) 或最短路径上有度不小于 \(n ^ {2 / 3}\) 的点的情况,且 接下来只需考虑边集为 \(E_2\) 的情况,设新图为 \(G'\)。
再分一层 \(V_2\) 表示所有度不小于 \(n ^ {1 / 3}\) 的点,则 \(|D_2| = \tio(n ^ {2 / 3})\)。设 \(V_2\) 对应 \(E_3\),则 \(|E_3| = \al{O}(n ^ {4 / 3})\)。这一步解决了最短路径上所有点度小于 \(n ^ {1 / 3}\) 的情况(实际上,没有两个相邻的点度均不小于 \(n ^ {1 / 3}\) 的情况)。
对于路径上有度在 \(n ^ {1 / 3}\) 和 \(n ^ {2 / 3}\) 之间的点的情况,设 \(w\) 是最后一个这样的点,则 \(w\) 到 \(v\) 都在 \(E_3\) 上,存在 \(w'\in D_2\) 和 \(w\) 相邻。我们要求 \(u\) 和 \(w'\) 之间在 \(G'\) 上的最短路 \(\de'(u, w')\)。这可以通过从 \(D_2\) 的每个点 BFS 得到,时间 \(\al{O}(|D_2||E_2|) = \tio(n ^ {7 / 3})\)。
但这样还有一个问题,就是 \(\de'(D_2\times V)\) 的大小是 \(\tio(n ^ {5 / 3})\),不能直接丢进 Dijkstra。设 \(E ^ *\) 为每个 \(w\in V_2\) 和一个邻居 \(w'\in D_2\) 之间的边集,则 \(|E ^ *| = \al O(n)\)。将 \(E ^ *\) 丢入最终的 Dijkstra,这样就只要求最终能考虑到 \(\de'(\{u\}\times D_2)\)。所以从 \(u\) 跑 Dijkstra 的时候只要把 \(\de'(\{u\}\times D_2)\) 丢进去就行。
算法流程
- 从每个 \(u\in D_1\) 跑 BFS 求出 \(\de(u, v)\),其中 \(u\in D_1\),\(v\in V\)。时间 \(\al O(|D_1||E|) = \tio (n ^ {7 / 3})\)。
- 从每个 \(u\in D_2\) 在 \(E_2\) 上跑 BFS 求出 \(\de'(u, v)\),其中 \(u\in D_2\),\(v\in V\)。时间 \(\al O(|D_2||E_2|) = \tio (n ^ {7 / 3})\)。
- 从每个 \(u\in V\) 在 \(E_3\cup \de(D_1\times V)\cup \de'(\{u\}\times D_2)\cup E ^ *\) 上跑 Dijkstra。时间 \(\tio(n(|E_3| + n|D_1| + |D_2| + n)) = \tio(n ^ {7 / 3})\)。
通过 \(D_1\) 把路径上度数较大的点移动到较小的集合内,并删去 \(E_2\) 以外的所有边。
通过 \(D_2\) 把路径上度数适中的点移动到较小的集合内,只需保证最后一个度数适中的点被考虑到,并以此减少 Dijkstra 算法的边数。
算法分析
- 若 \(u\in D_1\) 或 \(v\in D_1\),则 \(\de(u, v)\) 在第一步求出。
- 若 \(u, v\) 最短路径上所有点的度小于 \(n ^ {1 / 3}\),则每条边在 \(E_3\) 上,\(\de(u, v)\) 在第三步求出。
- 设 \(w\) 是路径上最后一个度在 \(n ^ {1 / 3}\) 和 \(n ^ {2 / 3}\) 之间的点,\(w'\in D_2\) 且 \((w, w')\in E ^ *\)。路径 \(\de'(u, w') + \de(w', w) + \de(w, v)\leq \de(u, v) + 2\) 且三部分分别属于 \(\de'(\{u\}\times D_2)\),\(E ^ *\) 和 \(E_3\)。
扩展
分更多层会出现更多中转点,可以做到 \(\tio(kn ^ {2 + \fr 1 {3k - 4}})\) 的无向无权图 surplus \(2(k - 1)\) APASP。当 \(k = \al O(\log n)\) 时,存在 \(\tio (n ^ 2)\) APASP。
无向无权图 surplus \(2\) APASP 在 2022 年被邓老师等人改进到了 \(\tio(n ^ {2.29})\)。
\(\tio(n ^ {2.29})\) surplus \(2\)
普通 \(\min +\) 矩阵乘法(距离乘法)暂无 \(\al O(n ^ {3 - \de})\) 的算法,但 BD 矩阵(只需一个方向的 BD)存在 \(\tio(n ^ {2.687})\) 的距离乘法。
大概思想是观察到 \(\de(D_1\times V)\) 的部分只有两条边,相当于 \(\de(V\times D_1)\) 和 \(\de(D_1\times V)\) 的距离乘法。用欧拉序将矩阵化为 BD,把这部分复杂度变优,就可以更改阈值做到更优复杂度了。
没讲怎么改进 \(\de(D_1\times V)\) 的求解。
分成 \(\al O(\log n)\) 层可以进一步做到 \(\al O(n ^ {2.2593})\)。
\(\tio(n ^ 2)\ (2, 1)\) 近似
\(\de(u, v)\leq d(u, v)\leq 2\de(u, v) + 1\)。
算法流程
对每个 \(0\leq i\leq \log_2 n\),设 \(V_i\) 为所有度不小于 \(\fr {n} {2 ^ i}\) 的点,得到大小为 \(\tio(2 ^ i)\) 的覆盖集 \(D_i\) 和对应的 \(E ^ *\)。设 \(E_i\) 为所有 \(u\notin V_{i - 1}\) 或 \(v\notin V_{i - 1}\) 的边。
- 从 \(D_i\) 的每个点在 \(G_i = (V, E_i\cup E ^ *)\) 上跑 BFS 求出 \(\de_i(D_i\times V)\)。时间 \(\al O(|D_i|(|E_i| + n)) = \tio(n ^)\)
设 \(c_i(u)\) 为 \(G_i\) 上距离 \(u\) 最近的 \(D_i\) 的点。
- 用 \(\de_i(u, c_i(u)) + \de_i(c_i(u), v)\) 和 \(\de_i(u, c_i(v)) + \de_i(c_i(v), v)\) 更新 \(d(u, v)\)。
算法分析
考虑 \(u, v\) 最短路上度数最大的点 \(x\)。因为 \(V_0 = \varnothing\),所以存在 \(i\) 使得 \(x\in V_i\) 但 \(x\notin V_{i - 1}\)。此外,因为 \(x\) 的度数最大,所以路径上所有边都在 \(G_i\) 上。设 \(w\in D_i\) 和 \(x\) 相邻且 \((w, x)\in E ^ *\),则 \(u\rightsquigarrow x\to w\) 和 \(w\to x\rightsquigarrow v\) 都在 \(G_i\) 上。
设 \(u' = c_i(u)\),\(v' = c_i(v)\),于是 \(\de_i(u, u') \leq \de(u, x) + 1\),\(\de(v, v')\leq \de(v, x) + 1\),不妨设 \(\de_i(u, u') \leq \lfloor\fr {\de(u, v)} {2}\rfloor + 1\),则
- 当路径长度是偶数时,如果 \(x\) 不在中点,不妨设靠近 \(u\) 一侧,那么 \(\de_i(u, u') + \de_i(u', v)\leq 2\de(u, v)\)。如果 \(x\) 在中点,那么考虑度数次大的点 \(x'\) 以及对应的 \(j\),根据 \(E_j\) 的定义可知路径上所有边在 \(E_j\) 上,此时 \(x'\) 一定不在中点。
- 当路径长度是奇数时,\(\de_i(u, u') + \de_i(u', v) \leq 2\de(u, v) + 1\)。当 \(x\) 和 \(x'\) 分别是最中间一条边的两端时,不等式可能取等。
为什么无向无权
对于有向图,以上算法显然行不通。
对于带权图,和一个点相邻不一定距离这个点近。可以考虑乘法近似。
无向带权图 APASP
截断 Dijkstra
考虑求出距离一个点最近的 \(b\) 个点,普通 Dijkstra 需要 \(\tio(n\cdot b)\) 的时间。截断 Dijkstra 在普通算法上加入改进:只考虑一个点权值最小的 \(b\) 条出边。
时间 \(\tio(b ^ 2)\)。
\(\tio(n ^ {7 / 3})\) stretch \(3\)
类似的思想。
设 \(B(u)\) 是距离 \(u\) 最近的 \(n ^ {2 / 3}\) 个点,\(D\) 是随机选出的大小为 \(\tio(n ^ {1 / 3})\) 的点集,满足所有 \(B(u)\) 和 \(D\) 有交。
这样,从 \(D\) 出发跑 Dijkstra,如果 \(u\in D\) 或 \(v\in D\) 或 \(v\in B(u)\),得到最短路。否则存在 \(w\in B(u)\cap D\)。因为 \(v\notin B(u)\),所以 \(\de(u, w)\leq \de(u, v)\)。同时 \(\de(w, v)\leq \de(w, u) + \de(u, v)\leq 2\de(u, v)\),所以 \(\de(u, w) + \de(w, v) \leq 3\de(u, v)\)。
显然复杂度为 \(\tio(n ^ {7 / 3})\)。
Cohen-Zwick 在 1997 年做到了 \(\tio(n ^ {3 / 2}m ^ {1 / 2})\) stretch \(2\),\(\tio(n ^ {7 / 3})\) stretch \(\fr 7 3\),\(\tio(n ^ 2)\) stretch \(3\)(可用 \(\tio(n ^ 2)\ (2, 1)\) 近似解决)。
近似最短路查询
以上讨论给出了使用 \(\al O(n ^ {5 / 3})\) 空间,\(\tio(n ^ {7 / 3})\) 预处理的 \(\al O(1)\) stretch \(3\) 近似最短路查询。
可以用 \(\al{O}(kn ^ {1 + \fr 1 k})\) 的空间做 \(\al O(k)\) stretch \(2k - 1\) 近似最短路查询 [Thorup, Zwick, 2001]。
Lecture 2
内容:二叉堆、二项树、斐波那契堆。
以下所有内容考虑小根堆:每个点的权值不大于其儿子权值。
二叉堆
堆的结构是一棵完全二叉树。根是最小值,树高 \(\log_2 n\)。
如何实现?用指针维护父亲和儿子关系,或左儿子两倍右儿子两倍加一。
查询最小值
\(\al O(1)\)。
插入
将 \(x\) 插入下一个叶子的位置,向上冒泡。\(\al O(\log n)\)。
减法
将元素的值减去 \(k\),向上冒泡。\(\al O(\log n)\)。
删除最小值
将根和最右侧叶子交换,向下冒泡(和较大的儿子交换)。\(\al O(\log n)\)。
合并
暴力合并。\(\Omega(n)\)。
二项堆
二项树
\(B_0\) 是一个点,\(B_k\) 是 \(B_{k - 1}\) 在根底下接一棵 \(B_{k - 1}\)。
性质:
- \(|B_k| = 2 ^ k\)。
- \(B_k\) 深度为 \(k\)。
- \(B_k\) 的第 \(i\) 层有 \(\binom k i\) 个点。
定义一个 结点的阶 是其儿子个数,一棵 二项树的阶 是其根的阶。
原则:只合并同阶二项树。
二项堆
二项堆维护的过程借助了二进制分组的思想保证复杂度:一旦有两棵同阶二项树,则合并。因此,若二项堆的大小为 \(n\),则该二项堆恰含所有 \(n\) 在 \(2\) 进制下为 \(1\) 的位对应的二项树。
一个二项堆最多有 \(\lceil \log_2 n\rceil\) 棵二项树。
合并
模拟二进制加法。\(\al O(\log n)\)。
删除最小值
找到最小值,删去。对应二项树分裂成若干二项树,与原有二项树合并。\(\al O (\log n)\)。
减法
向上冒泡。\(\al O(\log n)\)。
删除
减到 \(-\infty\) 再删除最小值。\(\al O(\log n)\)。
插入
二进制模拟加 \(1\)。\(\al O(\log n)\),均摊 \(\al O(1)\)。
查询最小值
\(\al O(\log n)\)。
斐波那契堆
斐波那契堆只有删除(最小值)的复杂度为 \(\al O (\log n)\)。其余操作均为均摊 \(\al O(1)\)。
从空斐波那契堆开始,任何 \(a_1\) 次插入、\(a_2\) 次删除和 \(a_3\) 次减法的操作序列需要 \(\al O(a_1 + a_2\log n + a_3)\) 时间。
做到 \(\al O(1)\) 插入、删除和减法是不可能的,因为基于比较的排序有下界 \(\Omega(n\log n)\)。因此,斐波那契堆相当优秀。
其基本想法和二项堆类似,但对结构的要求更宽松。二项堆在每次操作后立刻重构,斐波那契堆采用 懒重构 的思想,直到删除最小值之后再重构。
维护:
- 一些堆。
- 根的列表和最小值指针。
- 结点的标记:表示该结点已经失去了一个儿子。如果再失去一个,则以它为根的子树会被剪下来。
设 \(R(x)\) 和 \(R(H)\) 表示阶,\(T(H)\) 表示树的个数,\(M(H)\) 表示被标记的结点数量。定义势能 \(\Phi(H) = 2T(H) + M(H)\)。
定义均摊时间为实际用时加上势能的变化量,则总用时不超过总均摊时间之,因为势能非负且初始为 \(0\)。势能可能需要乘以一些常数。
插入
将结点插入根的列表,更新最小值指针。
时间 \(\al O(1)\),势能 \(+1\),均摊 \(\al O(1)\)。
合并两棵树
将较大的根连到较小的根下面。
删除最小值
把根的所有子树全部融到根列表,重构使得没有根的阶相同。
时间 \(\al O(R(H) + T(H))\),势能 \(\al O(R(H')) - T(H)\),均摊(给势能乘以一定常数)\(\al O(R(H))\)。
在只有前述操作时,\(R(H) = \al O(\log n)\)。
减法
如果堆性质没有被破坏,则直接减。
否则把以 \(x\) 为根的树剪下,加入根列表,标记其父亲(除非其父亲为根)。
如果不特殊处理,这一步会扭曲树的形态,导致 \(R(H)\) 过大。为了保证树的平整,如果一个点已经被标记两次,则取消标记,并将以该点为根的子树剪下,标记其父亲(除非其父亲为根)。这一步可能递归。
设剪下子树的次数为 \(c\),则时间 \(\al O(c)\),势能 \(\al O(1) - c\),均摊 \(\al O(1)\)。
分析
将 \(x\) 的儿子按照连边顺序记为 \(y_1, \cdots, y_k\)。当 \(y_i\) 连向 \(x\) 时,\(R(y_i)\) 等于当时的 \(R(x) \geq i - 1\)。而接下来 \(y_i\) 最多失去一个儿子。于是 \(R(y_i)\geq i - 2\)。
设 \(F_k\) 为 \(k\) 阶树的最小大小,则 \(F_1 = 1\),\(F_1 = 2\),\(F_k = F_{k - 2} + \cdots + F_0 + 1 = F_{k - 2} + F_{k - 1}\),这是斐波那契数列。于是 \(R(H)\leq \log_{\phi} n\)。
合并
用双向列表维护根列表。
时间 \(\al O (1)\),势能 \(0\),均摊 \(\al O(1)\)。
应用
\(\al O(m + n\log n)\) 最小生成树
在 Dijkstra 或 Prim 的过程中使用斐波那契堆,\(n\) 次插入,\(n\) 次删除和 \(m\) 次减法,时间 \(\al{O}(m + n\log n)\)。
\(\al O(m + n\log ^ *n)\) 最小生成树
Fredman & Tarjan [1987] 通过限制堆的大小做到了更优的复杂度:考虑结合 Boruvka 和 Prim,从某个点开始,如果堆里有 \(k\) 个元素或碰到了当前生成森林,则停止本次 Prim,从另一个未被访问的点开始。一轮过后,将森林里的所有树缩点,则每个点至少 \(k\) 条出边,结点数量不超过 \(\fr {2m} k\)。
取 \(k = 2 ^ {2m / n}\),则一轮复杂度 \(\al O(m + n\log k) = \al O(m)\)。新的 \(k' = 2 ^ {2m / n'} = 2 ^ {2 ^ {2m / n}}\)。可知总轮数为 \(\beta(m, n) = \min\{i: \log ^ {(i)} n \leq \fr m n\}\),其中 \(\log ^ {(i)} n\) 表示取 \(i\) 次 \(\log\)。
因此,当 \(m = \al O(n)\) 时,时间为 \(\al O(m \log ^ * n)\)。一旦存在常数 \(k\) 使得 \(m = n\log ^ {(k)} n\),则时间为 \(\al {O}(m)\)。
目前学术界的 MST
随机算法 \(\al O(m)\) [Karger, Klein, Tarjan 1995]。
确定算法 \(\al O(m\alpha(m, n))\) [Chazalle 2000]。
最优算法(复杂度仍为 open problem) [Pettie, Ramachandran 2002]。
判定 MST \(\al O(m)\) [Dixon, Rauch, Tarjan 1992]。
Tarjan 老爷子是真牛啊。