浅谈 Johnson 算法
1. 算法思想
Johnson 算法用于解决带权有向图中所有节点对之间的最短路径问题,它结合了 Bellman-Ford 算法和 Dijkstra 算法的优点。
首先,通过添加一个虚拟节点 s
到图中,将所有边的权重进行重新赋值(重赋权技巧),使得图中不存在负权环的同时,利用 Bellman-Ford 算法计算出从虚拟节点 s
到图中每个节点的最短距离 h(v)
。
然后,对于图中的每一对节点 u
和 v
,将原边权重 w(u, v)
替换为新的权重 w'(u, v) = w(u, v) + h(u) - h(v)
,这样处理后可以保证新的权重是非负的(通过利用最短距离的性质)。
最后,针对每一个节点作为源点,使用 Dijkstra 算法来计算经过重赋权后的图中到其他节点的最短路径,再根据重赋权的关系将计算出的距离转换回原图中的最短路径距离。
2. 代码示例
(Python 代码,以下是简化实现,假设输入的图用邻接矩阵表示,图中不存在负权环,节点编号从 0 开始)
1 |
|
你可以使用以下方式调用这个函数:
1 |
|
3. 复杂度分析
时间复杂度:
第一步 Bellman-Ford 算法的时间复杂度为 $O(V^3)$,其中
V
是图中节点的数量,因为它要对每条边进行V - 1
次松弛操作,每次松弛操作遍历所有边(在邻接矩阵表示下),时间复杂度为 $O(V^2)$,整体就是 $O(V^3)$。第二步对每个节点运行 Dijkstra 算法,若使用二叉堆实现优先队列的 Dijkstra 算法,每次时间复杂度为 $O((V + E) \log V)$,
E
是边数,总共运行V
次,这部分时间复杂度就是 $O(V (V + E) \log V)$。在稀疏图(E
接近V
)情况下,时间复杂度约为 $O(V^2 \log V)$,在稠密图(E
接近 $V^2$)情况下,时间复杂度约为 $O(V^3 \log V)$。所以 Johnson 算法总的时间复杂度在最坏情况下为 $O(V^3 \log V)$。
空间复杂度:需要存储图的相关信息(如邻接矩阵等)、距离数组等,在 Bellman-Ford 和 Dijkstra 算法执行过程中还需要一些临时空间,总体空间复杂度主要取决于图的表示方式以及节点数量,一般来说在 $O(V^2)$ 级别(使用邻接矩阵表示图时),
V
为节点数量。
4. 正确性证明
重赋权后不存在负权边:
根据 Bellman-Ford 算法计算出的h(v)
是从虚拟节点s
到节点v
的最短距离。对于图中任意边(u, v)
,有w'(u, v) = w(u, v) + h(u) - h(v)
,根据最短路径的三角不等式(从s
到v
的最短路径长度不会大于从s
经过u
再到v
的路径长度),可得h(v) ≤ h(u) + w(u, v)
,移项后就是w'(u, v) = w(u, v) + h(u) - h(v) ≥ 0
,所以重赋权后的边权重都是非负的。计算的最短路径等价于原图最短路径:
设d(u, v)
是原图中从u
到v
的最短路径距离,d'(u, v)
是重赋权后图中从u
到v
的最短路径距离。根据重赋权的定义以及最短路径的性质,对于任意节点对u
和v
,有d'(u, v) = d(u, v) + h(u) - h(v)
,在计算出重赋权后图中的最短路径d'(u, v)
后,通过还原操作d(u, v) = d'(u, v) - h(u) + h(v)
就可以得到原图中的最短路径距离,所以 Johnson 算法最终计算出的所有节点对之间的最短路径是正确的。