今天介紹最後一種路徑演算法,也是最難的。
對於全源最短路徑問題,可以認為是單源最短路徑問題的推廣,即分別以每個頂點作為源頂點並求其至其它頂點的最短距離。例如,對每個頂點應用 Bellman-Ford 演算法,則可得到所有頂點間的最短路徑的運行時間為 O(V^2E),由於圖中頂點都是連通的,而邊的數量可能會比頂點更多,這個時間沒有比 Floyd-Warshall 全源最短路徑演算法 O(V^3) 更優。那麼,再試下對每個頂點應用 Dijkstra 演算法,則可得到所有頂點間的最短路徑的運行時間為 O(VE + V^2logV),看起來優於 Floyd-Warshall 演算法的 O(V^3),所以看起來使用基於 Dijkstra 演算法的改進方案好像更好,但問題是 Dijkstra 演算法要求圖中所有邊的權值非負,不適合通用的情況。
在 1977 年,Donald B. Johnson 提出了對所有邊的權值進行 "re-weight" 的演算法,使得邊的權值非負,進而可以使用 Dijkstra 演算法進行最短路徑的計算。
我們先自己思考下如何進行 "re-weight" 操作,比如,簡單地對每條邊的權重加上一個數,使它們都變成正數,是否可行?
但顯然,原來a到d的最短距離是1+1+1=3,需要依次經過b,c兩點,大於直接連接兩點的路徑。如果簡單地全部加上1,還是通過b,c兩點,其距離變成2+2+2=6,而直接連接兩路的路徑變為5,直接導致最短路徑的變化,是不可行的。
那麼,Johnson 演算法是如何對邊的權值進行 **re-weight** 的呢?以下面的圖 G 為例,有 4 個頂點和 5 條邊。
首先,新增一個頂點 E,並使其與所有頂點連通,它到其他頂點的權重都為 0,即:
使用 Bellman-Ford 演算法 計算新的頂點到所有其它頂點(A, B,C, D)的最短路徑,得到[0,-5,-1,0], 存到一個變數h中 。然後移除E頂點,然後對所有邊的權重以下面的公式重新計算。
weight(start, end) = weight(start, end) + (h[start] - h[end])
於是有
ABw = -5 + h[0] - h[1] = -5 + 0 - (-5) = 0 BCw = 4 + h[1] - h[2] = 4 + -5 - (-1) = 0 CDw = 1 + h[2] - h[3] = 1 + -1 - 0 = 0 ADw = 3 + h[0] - h[3] = 3 + 0 - 0 = 3 ACw = 2 + h[0] - h[2] = 2 + 0 - (-1) = 3
現在所有邊為正數了,可以使用 Dijkstra演算法求解。
總結上面的過程,得到 Johnson 演算法的步驟如下:
1. 添加虛擬節點到這個圖裡,並添加指向所有節點的虛擬邊,這些邊的權重為 0
2. 以虛擬節點節點為起點,運行 Bellman-Ford演算法, 求出到每個節點的最短距離,存到h數組中
3. 移除虛擬節點,用上面求出的 h 值去更新圖裡的邊的權重,使得 `weight(start, end) = weight(start, end) + (h[start] - h[end])`
4. 對每個節點運行 Dijkstra 計算到其它節點的最短距離, 得到的distance數組的每個值需要減去 h[start] - h[end]
h[start] - h[end]
Bellman-Ford 演算法時間複雜度是 O(VE) ,Dijkstra 演算法的時間複雜度是O(Vlog(V)) ,因為每個節點都要做 Dijkstra,所以總的時間複雜度是O(V^2log(V)+VE) 。
O(VE)
O(Vlog(V))
O(V^2log(V)+VE)
其實現以下:
var COLOR = { //by司徒正美 WHITE: void 666, GRAY: 1, BLACK: -1 } class GraphByEdge { constructor(vertices, isDirected = false) {//略 } insertEdge(from, to, weight) {//略 } addEdge(a, b, weight = 1) { var from = this.map[a]; var to = this.map[b]; this.insertEdge(from, to, weight); if (!this.isDirected) { this.insertEdge(to, from, weight); } } toString(obj){//略 } johnsons(){ var vertices = this.vertices; var n = vertices.length; //添加新點 var virtual = "virtual"+n this.map[virtual] = n; this.vertices.push(virtual); //保存舊邊 var oldEdges = this.edges.concat(); //添加新邊 for(var i = 0; i < n; i++){ this.addEdge(virtual, this.vertices[i], 0) } var { distance } = this.bellmanFord(virtual) ; var h = distance //還原 delete this.map[virtual]; this.vertices.pop() this.edges = oldEdges; //re-weight this.edges.forEach(function(edge){ edge.weight = edge.weight+ (h[edge.from] - h[edge.to]) }) //對所有邊dijkstra var distSet = [] for (var i = 0; i < n; i++) { distSet[i] = [] for (var j = 0; j < n; j++) { distSet[i][j] = i == j ? 0 : Infinity } } for (var begin = 0; begin < n; begin++) { var obj = this.dijkstra(this.vertices[begin]); var dist = obj.distance for (var end = 0; end < dist.length; end++) { if (dist[end] != Infinity) { //減回之前加上的值 distSet[begin][end] = dist[end] - (h[begin] - h[end]); } } } return distSet } bellmanFord(start) { //略 return { hasCycle, distance } } dijkstra(start) { //略 return { distance, shortPaths } } }
做幾個例子測試一下:
function logJohnsons(g) { //by司徒正美 var distSet = g.johnsons() console.log(g.toString(distSet)) } //下面數據的來源見 // https://www.jianshu.com/p/36bbe0e86765 // https://www.cnblogs.com/gaochundong/p/johnson_algorithm.html // https://www.cnblogs.com/luweiseu/archive/2012/07/14/2591533.html var g = new GraphByEdge([A, B, C, D], true) g.addEdge(A, B, -5); g.addEdge(B, C, 4); g.addEdge(C, D, 1); g.addEdge(A, D, 3); g.addEdge(A, C, 2); logJohnsons(g) console.log("========")
var g1 = new GraphByEdge([0, 1, 2, 3, 4], true)
g1.addEdge(0, 1, -1); g1.addEdge(0, 2, 4); g1.addEdge(1, 2, 3); g1.addEdge(1, 3, 2); g1.addEdge(1, 4, 2); g1.addEdge(3, 2, 5); g1.addEdge(3, 1, 1); g1.addEdge(4, 3, -3); logJohnsons(g1) console.log("========") var g2 = new GraphByEdge([0, 1, 2, 3], true); g2.addEdge(0, 1, 5); g2.addEdge(0, 3, 10); g2.addEdge(1, 2, 3); g2.addEdge(2, 3, 1); logJohnsons(g2)