最短路径.fjx

引出

在我们平时图中找最短路径的时候,总是用搜索来做
但是 DFS 时间复杂度太高,相当于暴力的做法
BFS,虽然相对于 DFS 时间复杂度较低
但是只能处理边权为相同的图
这是,我们可以使用一种新的算法——最短路径

Floyd 算法

采用动态规划的思想。假设对每对 (s,t) 已经知道了 s 只经过编号在 [1,i] 间的节点到达 t 的最短路径长度。

每对点之间的距离会如何变化:我们只需要考虑经过点 i+1 的路径对每对点距离的影响。

考虑 s 经过 i+1t 的路径时,si+1 这段只用考虑最短的路径,i+1t 同理。

也就是只用考虑 \text{dis}(s,i+1)+\text{dis}(i+1,t) 是否小于 \text{dis}(s,t)。如果是,就用它更新 \text{dis}(s,t)。一次这个操作称为松弛。

那么算法的过程就是:

  • 枚举每个点 i
  • 枚举每个点对 (s, t)
  • \text{dis}(s, i) + \text{dis}(i, t) 更新 \text{dis}(s, t)

注意初始化的时候,\text{dis}(s, t) 就是两点之间的边权。如果没有边则设为 +\infty。时间复杂度为 O(n^3),空间复杂度为 O(n^2)

单源最短路

给定带权图 G(V, E) 和指定起点 s,求 s 到所有点的最短路。

Bellman-Ford 算法

先考虑图中没有负环(节点 u 到自己的一条路径,长度小于 $0$)的情况。

图中没有负环,则最短路中没有环。

考虑反证法。如果最短路中有环,则这个环必然长度 \geq 0。去掉这个环后,可以得到一条长度不比原来长的路径。
由于最短路中没有环,因此最短路中每个点只会被经过一次。因此最短路的边数最多是 n-1

因此考虑进行 n - 1 轮松弛。每次枚举一条边 e,用 su_e 的已知最短路和这条边,去更新 sv_e 的最短路。

也就是用 \text{dis}(s, u_e) + w_e 去更新 \text{dis}(s, v_e)

算法的时间复杂度为 O(nm)

当图中有负环的时候,依照定义环上的点之间距离是 -\infty

那么必然在每一轮都会产生成功的松弛操作。

而没有负环的图,从第 n 轮开始就不会产生成功的松弛操作。

因此可以通过第 n 轮是否有松弛操作来判断图中是否存在负环。

SPFA

{SPFA 算法——优化的 Bellman-Ford}

  • 使用队列优化松弛操作,只对距离更新的节点进行处理。
  • 如果一个节点被松弛超过 |V| 次,则存在负环。
  • 时间复杂度:平均 O(E),最坏 O(VE)

Dijkstra 算法

Dijkstra 算法是一个基于以下引理的更快的非负权图单源最短路算法:

引理 不妨假设图连通。如果已知距离起点 s 最近的 $k$($1 \leq k < n$)个点(设为点集 $S$),则距离 sk + 1 近的点,必然是 S 中某个点 u 的邻居 v,满足 \exists e \in [1, m], u = u_e, v = v_e, \text{dis}(u_e) + w_e 最小。

证明 首先一个不是 S 的邻居的点必定不是第 k + 1 近的点。考虑反证法,假设不是邻居的点是第 k + 1 近的点。取 s 到这个点的最短路上第一个不是 k 个点的点,取出的的点必然是 S 的邻居,且 s 到这个点的距离更近。

其次,所有 \text{dis}(u_e) + w_e 不是最小的 v_es 到它们的最短路径必然大于等于最小的 \text{dis}(u_e) + w_e。同样考虑反证,用上面的取点方法,可以找到一个更小的 \text{dis}(u_e) + w_e 的值。

基于这个引理,我们可以设计如下算法:

  • 用一个优先队列维护所有的 (dis(u_e) + w_e, v_e) 对;
  • 取出键值最小的一个加入 S
  • 更新优先队列(将取出的点 u 的邻居 v_e 加入)。

要注意,首先,我们只在这个值能松弛 v_e 的最短路的时候将 v_e 加入优先队列。其次,尽管如此,一个点还是有可能入队多次,我们只认可它第一次被弹出的时候。

由于每条边最多被遍历一次,每次都有可能被插入堆,算法的复杂度是 O(m \log m)。要注意:

  1. Dijkstra 不能正确处理带负权边的情况。
  2. 注意 std::priority_queue 默认是大根堆。
  3. 要先弹出堆顶,再检查是否需要跳过。
  4. 插入堆的 std::pair 第一维需要是距离。
  5. 距离可能会爆 int

除此之外,当图比较稠密的时候,可以直接进行 n 轮暴力挑选最小距离点,不需要用堆。这样复杂度是 O(n^2 + m),当 n^2 = O(m) 的时候这样更划算。

最短路树

在单源最短路径的过程中,每个点 v 都存在一些前驱 u,满足 \text{dis}(u) + w(u,v) = \text{dis}(v),这里 w(u,v)u, v 两点之间最短的一条边的边权。

如果我们得到一棵树,根节点为单源最短路径的起点 s,树上每个节点的父亲都是它的某个前驱,则起点到每个点的最短距离,就是这棵树上这个节点到根路径的长度。

这样的树就被称作最短路径树。

要得到最短路树,首先需要知道每个点的前驱。

在 Dijkstra 的过程中,我们只需要记录每个点 v 最后一次被松弛的时候是被哪个点 u 松弛的,则这个 u 就是 v 的前驱。

然后把每个节点的父亲设置成它的前驱就行了。

注意到这样做必定是没有问题的。因为将节点出队顺序看作一个拓扑序,节点之间的前驱关系是符合这个拓扑序的。也就是说每个点的前驱必然比它早出队。因此即使图上有零权环也没关系。

2 个赞