解决单源最短路径问题(Single Source Shortest Paths Problem)的算法包括:
- Dijkstra 单源最短路径算法:时间复杂度为 O(E + VlogV),要求权值非负;
- Bellman-Ford 单源最短路径算法:时间复杂度为 O(VE),适用于带负权值情况;
对于全源最短路径问题(All-Pairs Shortest Paths Problem),可以认为是单源最短路径问题的推广,即分别以每个顶点作为源顶点并求其至其它顶点的最短距离。例如,对每个顶点应用 Bellman-Ford 算法,则可得到所有顶点间的最短路径的运行时间为 O(V2E),由于图中顶点都是连通的,而边的数量可能会比顶点更多,这个时间没有比 Floyd-Warshall 全源最短路径算法 O(V3) 更优。那么,再试下对每个顶点应用 Dijkstra 算法,则可得到所有顶点间的最短路径的运行时间为 O(VE + V2logV),看起来优于 Floyd-Warshall 算法的 O(V3),所以看起来使用基于 Dijkstra 算法的改进方案好像更好,但问题是 Dijkstra 算法要求图中所有边的权值非负,不适合通用的情况。
在 1977 年,Donald B. Johnson 提出了对所有边的权值进行 "re-weight" 的算法,使得边的权值非负,进而可以使用 Dijkstra 算法进行最短路径的计算。
我们先自己思考下如何进行 "re-weight" 操作,比如,简单地对每条边的权值加上一个较大的正数,使其非负,是否可行?
1 1 1 s-----a-----b-----c \ / \ / \______/ 4
比如上面的图中,共 4 条边,权值分别为 1,1,1,4。当前 s --> c 的最短路径是 {s-a, a-b, b-c} 即 1+1+1=3。而如果将所有边的权值加 1,则最短路径就会变成 {s-c} 的 5,因为 2+2+2=6,实际上导致了最短路径的变化,显然是错误的。
那么,Johnson 算法是如何对边的权值进行 "re-weight" 的呢?以下面的图 G 为例,有 4 个顶点和 5 条边。
首先,新增一个源顶点 4,并使其与所有顶点连通,新边赋权值为 0,如下图所示。
使用 Bellman-Ford 算法 计算新的顶点到所有其它顶点的最短路径,则从 4 至 {0, 1, 2, 3} 的最短路径分别是 {0, -5, -1, 0}。即有 h[] = {0, -5, -1, 0}。当得到这个 h[] 信息后,将新增的顶点 4 移除,然后使用如下公式对所有边的权值进行 "re-weight":
w(u, v) = w(u, v) + (h[u] - h[v]).
则可得到下图中的结果:
此时,所有边的权值已经被 "re-weight" 为非负。此时,就可以利用 Dijkstra 算法对每个顶点分别进行最短路径的计算了。
Johnson 算法描述如下:
- 给定图 G = (V, E),增加一个新的顶点 s,使 s 指向图 G 中的所有顶点都建立连接,设新的图为 G’;
- 对图 G’ 中顶点 s 使用 Bellman-Ford 算法计算单源最短路径,得到结果 h[] = {h[0], h[1], .. h[V-1]};
- 对原图 G 中的所有边进行 "re-weight",即对于每个边 (u, v),其新的权值为 w(u, v) + (h[u] - h[v]);
- 移除新增的顶点 s,对每个顶点运行 Dijkstra 算法求得最短路径;
Johnson 算法的运行时间为 O(V2logV + VE)。
Johnson 算法伪码实现如下:
Johnson 算法 C# 代码实现如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 5 namespace GraphAlgorithmTesting 6 { 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 // build a directed and negative weighted graph 12 Graph directedGraph1 = new Graph(5); 13 directedGraph1.AddEdge(0, 1, -1); 14 directedGraph1.AddEdge(0, 2, 4); 15 directedGraph1.AddEdge(1, 2, 3); 16 directedGraph1.AddEdge(1, 3, 2); 17 directedGraph1.AddEdge(1, 4, 2); 18 directedGraph1.AddEdge(3, 2, 5); 19 directedGraph1.AddEdge(3, 1, 1); 20 directedGraph1.AddEdge(4, 3, -3); 21 22 Console.WriteLine(); 23 Console.WriteLine("Graph Vertex Count : {0}", directedGraph1.VertexCount); 24 Console.WriteLine("Graph Edge Count : {0}", directedGraph1.EdgeCount); 25 Console.WriteLine(); 26 27 int[,] distSet1 = directedGraph1.Johnsons(); 28 PrintSolution(directedGraph1, distSet1); 29 30 // build a directed and positive weighted graph 31 Graph directedGraph2 = new Graph(4); 32 directedGraph2.AddEdge(0, 1, 5); 33 directedGraph2.AddEdge(0, 3, 10); 34 directedGraph2.AddEdge(1, 2, 3); 35 directedGraph2.AddEdge(2, 3, 1); 36 37 Console.WriteLine(); 38 Console.WriteLine("Graph Vertex Count : {0}", directedGraph2.VertexCount); 39 Console.WriteLine("Graph Edge Count : {0}", directedGraph2.EdgeCount); 40 Console.WriteLine(); 41 42 int[,] distSet2 = directedGraph2.Johnsons(); 43 PrintSolution(directedGraph2, distSet2); 44 45 Console.ReadKey(); 46 } 47 48 private static void PrintSolution(Graph g, int