Unverified Commit 37765ed4 authored by Margatroid's avatar Margatroid Committed by GitHub
Browse files

add Johnson 全源最短路径算法 (#1789)

add Johnson 全源最短路径算法
parents 960c014b ec9cfd27
Loading
Loading
Loading
Loading
+12 KiB
Loading image diff...
+12 KiB
Loading image diff...
+92 −25
Original line number Diff line number Diff line
@@ -106,7 +106,7 @@ for (k = 1; k <= n; k++) {

三角形不等式: $dist(v) \leq dist(u) + edge\_len(u, v)$ 。

证明:反证法,如果不满足,那么可以用 $relax$ 操作来更新 $dist(v)$ 的值。
证明:反证法,如果不满足,那么可以用松弛操作来更新 $dist(v)$ 的值。

Bellman-Ford 算法如下:

@@ -114,7 +114,7 @@ Bellman-Ford 算法如下:
while (1) for each edge(u, v) relax(u, v);
```

当一次循环中没有 $relax$ 操作成功时停止。
当一次循环中没有松弛操作成功时停止。

每次循环是 $O(m)$ 的,那么最多会循环多少次呢?

@@ -124,11 +124,9 @@ while (1) for each edge(u, v) relax(u, v);

我们考虑最短路存在的时候。

由于一次 $relax$ 会使(被 $relax$ 的)最短路的边数至少 $+1$ ,而最短路的边数最多为 $n-1$ 。
由于一次松弛操作会使最短路的边数至少 $+1$ ,而最短路的边数最多为 $n-1$ 。

所以最多(连续) $relax$  $n-1$ 次……( $relax$ 一定是环环相扣的,不然之前就能被 $relax$ 掉)

所以最多循环 $n-1$ 次。
所以最多执行 $n-1$ 次松弛操作,即最多循环 $n-1$ 次。

总时间复杂度 $O(NM)$ 。 **(对于最短路存在的图)** 

@@ -152,7 +150,7 @@ for (i = 1; i < n; i++) {

给一张有向图,问是否存在负权环。

做法很简单,跑 Bellman-Ford 算法,如果有个点被 $relax$ 成功了 $n$ 次,那么就一定存在。
做法很简单,跑 Bellman-Ford 算法,如果有个点被松弛成功了 $n$ 次,那么就一定存在。

如果 $n-1$ 次之内算法结束了,就一定不存在。

@@ -160,11 +158,11 @@ for (i = 1; i < n; i++) {

即 Shortest Path Faster Algorithm。

很多时候我们并不需要那么多无用的 $relax$ 操作。
很多时候我们并不需要那么多无用的松弛操作。

很显然,只有上一次被 $relax$ 的结点,所连接的边,才有可能引起下一次的 $relax$ 
很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作

那么我们用队列来维护“哪些结点可能会引起 $relax$ ”,就能只访问必要的边了。
那么我们用队列来维护“哪些结点可能会引起松弛操作”,就能只访问必要的边了。

```text
q = new queue();
@@ -182,7 +180,7 @@ while (!q.empty()) {
}
```

SPFA 的时间复杂度为 $O(kM)~ (k\approx 2)$ (玄学),但 **理论上界** 为 $O(NM)$ ,精心设计的稠密图可以随便卡掉 SPFA,所以考试时谨慎使用(NOI 2018 卡 SPFA)。
虽然在大多数情况下 SPFA 跑得很快,但其最坏情况下的时间复杂度为 $O(NM)$ ,将其卡到这个复杂度也是不难的,所以考试时谨慎使用(在没有负权边时最好使用 Dijkstra 算法,在有负权边且题目中的图没有特殊性质时,若 SPFA 是标算的一部分,题目不应当给出 Bellman-Ford 算法无法通过的数据范围)。

#### SPFA 的优化之 SLF

@@ -190,7 +188,7 @@ SPFA 的时间复杂度为 $O(kM)~ (k\approx 2)$ (玄学),但 **理论上

即在新元素加入队列时,如果队首元素权值大于新元素权值,那么就把新元素加入队首,否则依然加入队尾。

该优化在确实在一些图上有显著效果,其复杂度也有保证,但是如果有负权边的话,可以直接卡到指数级。
该优化在确实在一些图上有显著效果,但是如果有负权边的话,可以直接卡到指数级。

* * *

@@ -212,9 +210,8 @@ IPA:/ˈdikstrɑ/或/ˈdɛikstrɑ/。

然后重复这些操作:

(1) $relax$ 那些刚刚被加入第一个集合的结点的所有出边。

(2)从第二个集合中,选取一个最短路长度最小的结点,移到第一个集合中。
1.  对那些刚刚被加入第一个集合的结点的所有出边执行松弛操作。
2.  从第二个集合中,选取一个最短路长度最小的结点,移到第一个集合中。

直到第二个集合为空,算法结束。

@@ -222,15 +219,15 @@ IPA:/ˈdikstrɑ/或/ˈdɛikstrɑ/。

如果用暴力: $O(n^2 + m) = O(n^2)$ 。

如果用堆 $O((n+m) \log n)$ 。
如果用堆 $O(m \log n)$ 。

如果用 priority_queue: $O((n+m) \log m)$ 。
如果用 priority_queue: $O(m \log m)$ 。

(注:如果使用 priority_queue,无法删除某一个旧的结点,只能插入一个权值更小的编号相同结点,这样操作导致堆中元素是 $O(m)$ 的)

如果用线段树(ZKW 线段树): $O(m \log n + n) = O(m \log n)$ 

如果用 Fibonacci 堆: $O(n \log n + m) = O(n \log n)$ (这就是为啥优秀了)。
如果用 Fibonacci 堆: $O(n \log n + m)$ (这就是为啥优秀了)。

等等,还没说正确性呢!

@@ -238,9 +235,9 @@ IPA:/ˈdikstrɑ/或/ˈdɛikstrɑ/。

再证明第一个集合中的元素的最短路已经确定。

第一步,一开始时成立(基础),在每一步中,加入集合的元素一定是最大值,且是另一边最小值, $relax$ 又是加上非负数,所以仍然成立。(归纳)(利用非负权值的性质)
第一步,一开始时成立(基础),在每一步中,加入集合的元素一定是最大值,且是另一边最小值,每次松弛操作又是加上非负数,所以仍然成立。(归纳)(利用非负权值的性质)

第二步,考虑每次加进来的结点,到他的最短路,上一步必然是第一个集合中的元素(否则他不会是第二个集合中的最小值,而且有第一步的性质),又因为第一个集合已经全部 $relax$ 过了,所以最短路显然确定了。
第二步,考虑每次加进来的结点,到他的最短路,上一步必然是第一个集合中的元素(否则他不会是第二个集合中的最小值,而且有第一步的性质),又因为第一个集合内的点已经全部松弛过了,所以最短路显然确定了。

```text
H = new heap();
@@ -256,15 +253,85 @@ for (i = 1; i <= n; i++) {
}
```

## Johnson 全源最短路径算法

Johnson 和 Floyd 一样,是一种能求出无负环图上任意两点间最短路径的算法。该算法在 1977 年由 Donald B. Johnson 提出。

任意两点间的最短路可以通过枚举起点,跑 $n$ 次 Bellman-Ford 算法解决,时间复杂度是 $O(n^2m)$ 的,也可以直接用 Floyd 算法解决,时间复杂度为 $O(n^3)$ 。

注意到堆优化的 Dijkstra 算法求单源最短路径的时间复杂度比 Bellman-Ford 更优,如果枚举起点,跑 $n$ 次 Dijkstra 算法,就可以在 $O(nm\log m)$ (取决于 Dijkstra 算法的实现)的时间复杂度内解决本问题,比上述跑 $n$ 次 Bellman-Ford 算法的时间复杂度更优秀,在稀疏图上也比 Floyd 算法的时间复杂度更加优秀。

但 Dijkstra 算法不能正确求解带负权边的最短路,因此我们需要对原图上的边进行预处理,确保所有边的边权均非负。

一种容易想到的方法是给所有边的边权同时加上一个正数 $x$ ,从而让所有边的边权均非负。如果新图上起点到终点的最短路经过了 $k$ 条边,则将最短路减去 $kx$ 即可得到实际最短路。

但这样的方法是错误的。考虑下图:

![](./images/shortest-path1.png)

 $1 \to 2$ 的最短路为 $1 \to 5 \to 3 \to 2$ ,长度为 $−2$ 。

但假如我们把每条边的边权加上 $5$ 呢?

![](./images/shortest-path2.png)

新图上 $1 \to 2$ 的最短路为 $1 \to 4 \to 2$ ,已经不是实际的最短路了。

Johnson 算法则通过另外一种方法来给每条边重新标注边权。

我们新建一个虚拟节点(在这里我们就设它的编号为 $0$ )。从这个点向其他所有点连一条边权为 $0$ 的边。

接下来用 Bellman-Ford 算法求出从 $0$ 号点到其他所有点的最短路,记为 $h_i$ 。

假如存在一条从 $u$ 点到 $v$ 点,边权为 $w$ 的边,则我们将该边的边权重新设置为 $w+h_u-h_v$ 。

接下来以每个点为起点,跑 $n$ 轮 Dijkstra 算法即可求出任意两点间的最短路了。

一开始的 Bellman-Ford 算法并不是时间上的瓶颈,若使用 `priority_queue` 实现 Dijkstra 算法,该算法的时间复杂度是 $O(nm\log m)$ 。

### 正确性证明

为什么这样重新标注边权的方式是正确的呢?

在讨论这个问题之前,我们先讨论一个物理概念——势能。

诸如重力势能,电势能这样的势能都有一个特点,势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走的路径无关。

势能还有一个特点,势能的绝对值往往取决于设置的零势能点,但无论将零势能点设置在哪里,两点间势能的差值是一定的。

接下来回到正题。

在重新标记后的图上,从 $s$ 点到 $t$ 点的一条路径 $s \to p_1 \to p_2 \to \dots \to p_k \to t$ 的长度表达式如下:

 $(w(s,p_1)+h_w-h_{p_1})+(w(p_1,p_2)+h_{p_1}-h_{p_2})+ \dots +(w(p_k,t)+h_{p_k}-h_t)$ 

化简后得到:

 $w(s,p_1)+w(p_1,p_2)+ \dots +w(p_k,t)+h_s-h_t$ 

无论我们从 $s$ 到 $t$ 走的是哪一条路径, $h_s-h_t$ 的值是不变的,这正与势能的性质相吻合!

为了方便,下面我们就把 $h_i$ 称为 $i$ 点的势能。

上面的新图中 $s \to t$ 的最短路的长度表达式由两部分组成,前面的边权和为原图中 $s \to t$ 的最短路,后面则是两点间的势能差。因为两点间势能的差为定值,因此原图上 $s \to t$ 的最短路与新图上 $s \to t$ 的最短路相对应。

到这里我们的正确性证明已经解决了一半——我们证明了重新标注边权后图上的最短路径仍然是原来的最短路径。接下来我们需要证明新图中所有边的边权非负,因为在非负权图上,Dijkstra 算法能够保证得出正确的结果。

根据三角形不等式,图上任意一边 $(u,v)$ 上两点满足: $h_v \leq h_u + w(u,v)$ 。这条边重新标记后的边权为 $w'(u,v)=w(u,v)+h_u-h_v \geq 0$ 。这样我们证明了新图上的边权均非负。

这样,我们就证明了 Johnson 算法的正确性。

* * *

## 不同方法的比较

| Floyd      | Bellman-Ford | Dijkstra           |
| ---------- | ------------ | ------------------ |
| 每对结点之间的最短路 | 单源最短路        | 单源最短路              |
| 没有负环的图     | 任意图          | 非负权图               |
|  $O(N^3)$  |  $O(NM)$     |  $O((N+M)\log M)$  |
| Floyd      | Bellman-Ford    | Dijkstra       | Johnson         |
| ---------- | --------------- | -------------- | --------------- |
| 每对结点之间的最短路 | 单源最短路           | 单源最短路          | 每对结点之间的最短路      |
| 没有负环的图     | 任意图(可以判定负环是否存在) | 非负权图           | 没有负环的图          |
|  $O(N^3)$  |  $O(NM)$        |  $O(M\log M)$  |  $O(NM\log M)$  |

注:表中的 Dijkstra 算法在计算复杂度时均用 `priority_queue` 实现。

## 输出方案