图的表示方法
本书中的图通过空间复杂度 $O(|V| + 3 * |E|)$ 表示(无向图的边需要通过两条有向边表示):
first
:长度为 $|V|$ 的数组,每个元素 x 表示从 x 出发的第一条边;u
、v
:长度为 $|E|$ 的数组,分别存储着边关联的两个节点;next
:长度为 $|E|$ 的数组,边的邻接链表的下一条边;
树
无根树转化为有根树
转化代码:
|
|
表达式树
二叉树是表达式处理的常用工具,其中每个非叶子节点都表示一个运算符,它的左子树是这个运算符的第一个运算数,右子树则是这个运算符的第二个运算数。
如何通过一个字符串建立表达式树呢,方法有很多种(详见《编译原理》)。
最小生成树
在无向图中,连通且不含圈的图称为树。
如果给定无向图 G=(V,E),如果存在一个 E 的子集连接了 V 中的所有点,那我们称这个子集为 G 的生成树,而权值最小的生成树则被称为最小生成树(Minimal Spanning Tree, MST)。
求一个 MST 的方法有很多种,最常见的有两种:Kruskal 算法和 Prim 算法,https://zhihu.com/question/27566032/answer/287968877
并查集
在上面的最小生成树问题中,有一个常见的图论问题要处理:
- 如何判断图中的两个点是否属于同一个“连通分量”(即一个连通图的所有点的集合);
- 通过添加一个边的方法将两个“连通分量”合并。
上面这两个诉求有这样的特点:
- 我们只关心连通这一个信息,不关心具体的连通方式,类似于一个集合;
这个问题可以用一个树状结构“并查集”表示,并查集由多个集合构成,每个集合通过一颗树状的结构表示,两个节点如果属于同一个“连通分量”,那么它们一定属于同一棵树,这棵树的根节点被称为这个集合的“代表元”(representative)。
实现一个并查集核心在与 find
方法 ,即如何找到一个给定节点所在的树的代表元:
|
|
最短路问题
Dijkstra 算法
之前动态规划的章节中有提到 DAG 中的动态规划可以用于解决最短路问题。但是如果图中有环,那么之前的方法就不适用了。
Dijkstra 算法可以用于计算正权图上的单源最短路问题(SSSP, Single-Source Shortest Paths)。用 w[i][j]
表示两个边之间的权值(INF 表示不连接),d[i]
表示源节点到节点 i 的距离,那么 Dijkstra 算法是实现如下:
|
|
简单地来说,Dijkstra 算法分为以下两步:
- 在所以未访问的节点中,找出距离源节点最近的节点 x;
- 给 x 做标记,并且更新所有与 x 响铃的点到源节点的距离;
我们通常把这个算法的第二步称为松弛操作(relexation)。
优化 Dijkstra 算法
邻接表(Adjacency List):一种稀疏图(Sparse Graph)的表示方式。在这种表示法中,每个结点 i 都有维护一个链表,里面保存着从 i 出发的所有边。
以下是一个从读入开始的例子:
|
|
其中:
u
/v
/w
都是原始输入,分别为编号为 e 的边连接的两个节点,以及这个边的权值;first
表示编号为 i 的节点的第一个边,用next
连接所有的边;
使用邻接表可以优化 Dijkstra 算法的第二步,从 O(n)
优化到 O(m)
。对于算法的第一步,即“找出未标号节点中的最小 d 值”,可以使用优先级队列进行优化。
使用优先级队列的代码实现如下:
|
|
使用优先级队列可以把第一步的复杂度从 O(n)
降低到 O(logn)
。
Bellman-Ford 算法
与 Dijkstra 算法类似,Bellman-Ford 算法主要分为以下两步:
- 遍历所有节点,对每个结点执行松弛操作;
- 更新当前遍历节点的所有相邻节点,对他们也执行松弛操作。
这个算法的朴素实现是 O(mn)
,具体在实现时可以使用 FIFO 进行优化:
|
|
Floyd 算法
如果你仅仅需要计算两个点之间的最短路,不必调用 n 次 Dijkstra(边权均为正)或者 Belman-Ford(有负权)。只需要使用下面的 Floyd-Warshall 算法:
|
|
网络流初步
增广路定理
对于一个网络拓扑图
G=(V, E)
,对于每条边 $$(u, v) \in E$$,有一个运送物品的上限称为容量c(u,v)
,它在一个特定的问题下实际运送的物品数量称为流量f(u, v)
。最大流问题:求一个方案,把最多的物品从 s 运送到 t(其中 $$s, t \in V$$)
问题中通过 f、c 建模描述的最大流问题满足这样的一些性质:
- 容量限制:$$f(u, v) \le c(u, v)$$
- 斜对称性:$$f(u, v) = -f(v, u)$$
- 流量平衡:对于除了 s、t 之外的任意节点 u,$$\displaystyle \sum_{(u, v) \in E} f(u, v) = 0$$
残量网络(residual network):
- 对于 E 中的每一条边 (u,v) 都有 f、c 两个属性值,对图中的每一条边进行计算,将 (u, v) 这样一个有向边分解成两个有向边:u 到 v 且权值为 f;v 到 u 且权值为 c-f;
- 通过这样的方式构造出来的一张新图称为残量网络
增广(augmenting):
- 在残量网络中找出任意一条从 s 到 t 的有向路径,找出道路中所有残量的最小值 d,把对应所有边上的流量增加 d。
- 上面描述的这个过程称为增广。
增广路定理:
- 当且仅当残量网络中不存在 s-t 有向路径时,此时状态对应的流是 s 到 t 的最大流。
根据增广路定理我们可以使用 BFS 遍历所有路径,这被称为 Edonds-Karp 算法:
|
|
其中 a[i] 表示在当前路径下,从源节点 s 到 i 的最小残量,那么 a[t] 就是整个链路的最小残量。
最小割最大流定理
割:
- 把图的所有顶点分成 S、T 两个集合,其中 $$s \in S, t \in T$$,把集合 $${e = (u, v)| u \in S, v \in T}$$ 中的所有边删除,就无法从 s 达到 t 了。
- 我们把这样的集合划分方式称为一个 s-t 的割。
一个割的容量可以定义为:$$\displaystyle c(S, T) = \sum_{u \in S, v \in T} c(u, v)$$,求容量最小割的问题即最小割问题。
最小割最大流定理:
在增广路算法中,循环结束的条件是找不到一个从 s 到 t 的路径,在最后一个循环周期中,从 s 通过 BFS 到达过的节点可以组成一个集合 S,它与剩下节点的集合 T 就构成一个 s-t 割;
通过上面的映射关系从最大流映射到的割,就是最小割;
证明:通过 $$|f| \le c(S, T)$$
最小费用最大流问题
建模时可以用负数表示容量,使用 Bellman-Ford 算法而不是 BFS。
全图最小割
参考:
全局最小割(或“全局割”)即图的所有割中,容量最小的割成为这个图的全局最小割。
随机算法 Karger’s Algorithm:
- 算法描述:对一个图 $$G = (V, E)$$,随机选择图中的一条边 $$e = (v_1, v_2) \in E$$,对这条边链接的两个点 $$v_1, v_2$$ 进行“缩点”,知道图中只剩下两个点,这两个点对应着原图的一个割。随机执行多次之后可以找到概率上的最小割。
- 概率计算:即每次缩点结束后得到的割,是原图的最小割的概率是多少:
- 设图的最小割中,两个集合之间的边集为 $$E’$$,那么考虑随机算法的第一步,成功的概率为 $$\displaystyle 1 - \frac{|E’|}{|E|}$$;
- 因为边集 $$E’$$ 构成的割是原图最小割,每个节点的度都应该小于他的势,那么一定满足条件 $$\displaystyle \sum_{v \in V}|E’| \le 2|E| \Rightarrow |E| \ge \frac{|V||E’|}{2} $$
- 综合以上两个不等式,对第一次成功的概率有不等式:$$\displaystyle p_{first} = 1 - \frac{|E’|}{|E|} \ge 1 - \frac{2 |E’|}{|V| |E’|} = 1 - \frac{2}{|V|}$$
- 上面的概率不等式与 $$|E’|$$ 无关的,所以我们可以递归地计算综合概率:$$\displaystyle p_n \ge (1 - \frac{2}{|V|}) p_{n-1} = \prod_{i = 0}^{n-3}\frac{|V| - i - 2}{|V| - i} = \frac{1}{C_{|V|}^2}$$
- 时间复杂度计算:考虑执行 $$T = C_{|V|}^2 \ln{|V|}$$ 次随机的计算:
- 可以认为 $$C^{2}{n}$$ 是一个极大的数字,在进行了 T 次计算后失败概率 $$\displaystyle (1 - \frac{1}{C{|V|}^2})^{C_{|V|}^2 \ln{|V|}} \rightarrow (\frac{1}{e})^{\ln{|V|}} = \frac{1}{|V|}$$
- 执行 T 次计算的总时间复杂度为:$$O(T |E|) \approx |V|^2 |E| \ln{|V|}$$
迭代算法 Stoer-Wagner Algorithm:
- 算法描述:对于一个图 $$G = (V, E)$$:
- 随机初始化一个点集 $$A = { v | v \in V }$$,依次将缩点成本($\displaystyle v \in V/A \rightarrow \sum_{u \in A, uv \in E} w(uv)$)最大的点加入点集;
- 直到加入倒数第二个点 $v_{penult}$,剩下最后一个点 $$v_{last}$$,那么 $v_{penult}-v_{last}$ 的最小割即 ${v | v \in A, v \not= v_{last}}, {v_{last}}$
- 重复前面两个步骤 $|V| - 1$ 次,每次可以得到一个最小割,这些最小割中最小的即全局最小割;