算法筆記-----單源最短路徑之Bellman-Ford算法
今天介紹一種計算單源最短路徑的算法Bellman-Ford算法,對於圖G=(V,E)來說,該算法的時間復雜度為O(VE),其中V是頂點數,E是邊數。Bellman-Ford算法適用於任何有向圖,並能報告圖中存在負環路(邊的權重之和為負數的環路,這使得圖中所有經過該環路的路徑的長度都可以通過反復行走該環路而使路徑長度變小,即沒有最短路徑)的情況。以後會介紹運行速度更快,但只適用於沒有負權重邊的圖中的Dijkstra算法。Dijkstra算法可以參考我的下一篇博客 單源最短路徑之Dijkstra算法
在介紹Bellman-Ford算法之前,先介紹在計算圖的單源最短路徑的各種算法中都會用到的松弛操作。
1 // 松弛操作,檢查<s, ..., v>的距離是否比<s, ..., u, v>大,是則更新<s, ..., v>為<s, ..., u, v> 2 void relax(Vertex *u, Vertex *v, int w) 3 { 4 if (u->weight == INF || w == INF) return; 5 if (v->weight > u->weight + w) 6 { 7 v->weight = u->weight + w;8 v->p = u; 9 } 10 }
Vertex是頂點的數據類型,<u,v>是圖G中的一條邊。頂點Vertex的屬性weight記錄了該頂點當前距離源點的最短距離,p記錄了頂點在其最短距離中的前一個頂點。松弛操作要做的工作就是檢查路徑<s,...,v>的距離是否比<s,...,u,v>大,是則更新之,並把s到v的距離修改為s到u的距離加上<u,v>的長度,其中<s,...,v>為源點s到頂點v的原來的路徑,<s,...,u>為源點s到頂點u的路徑。 Bellman-Ford算法的思想就是反復對圖G中的邊<u,v>進行松弛操作,知道所有頂點到s的距離都被最小化為止。這裏的圖使用鄰接表表示,下面給出圖的定義和算法程序,Bellman-Ford算法需要的參數包括圖g、權重矩陣w和源點編號s(頂點編號從1開始)。
1 typedef struct GNode 2 { 3 int number; // 頂點編號 4 struct GNode *next; 5 } GNode; 6 7 typedef struct Vertex 8 { 9 int number; 10 int weight; // 該頂點到源點的距離 11 struct Vertex *p; 12 } Vertex; 13 14 typedef struct Graph 15 { 16 GNode *LinkTable; 17 Vertex *vertex; 18 int VertexNum; 19 } Graph;
1 /** 2 * Bellman Ford 單源最短路徑算法 3 * @return true 沒有負環路; false 有負環路,最短路徑構造失敗 4 */ 5 bool Bellman_Ford(Graph *g, int **w, int s) 6 { 7 initialize(g, s); 8 9 GNode *linkTable = g->LinkTable; 10 for (int i = 1; i < g->VertexNum; i++) 11 { 12 // 反復將邊加入到已有的最小路徑圖中,檢查是否有更優路徑 13 for (int j = 0; j < g->VertexNum; j++) 14 { 15 GNode *node = (linkTable + j)->next; 16 Vertex *u = g->vertex + j; 17 while (node != NULL) 18 { 19 Vertex *v = g->vertex + node->number - 1; 20 int weight = *((int*)w + j * g->VertexNum + node->number - 1); 21 relax(u, v, weight); 22 node = node->next; 23 } 24 } 25 } 26 27 // 通過檢查是否都已達到最短路徑來檢查是否存在負環路 28 for (int j = 0; j < g->VertexNum; j++) 29 { 30 GNode *node = (linkTable + j)->next; 31 Vertex *u = g->vertex + j; 32 while (node != NULL) 33 { 34 Vertex *v = g->vertex + node->number - 1; 35 int weight = *((int*)w + j * g->VertexNum + node->number - 1); 36 if (v->weight > u->weight + weight) 37 { 38 return false; 39 } 40 node = node->next; 41 } 42 } 43 return true; 44 }
1 void initialize(Graph *g, int s) 2 { 3 Vertex *vs = g->vertex; 4 for (int i = 0; i < g->VertexNum; i++) 5 { 6 Vertex *v = vs + i; 7 v->p = NULL; 8 v->weight = INF; 9 } 10 (vs + s - 1)->weight = 0; 11 }
上述算法代碼實現的Bellman-Ford算法進行了V次對所有邊的松弛操作,這是考慮到了最壞情況,假設圖G是一條單鏈,則從表頭s到表尾的路徑計算需要進行V次松弛操作。下面給出一個演示例子。
1 Graph graph; 2 graph.VertexNum = 5; 3 Vertex v[5]; 4 Vertex v1; v1.number = 1; v1.p = NULL; v[0] = v1; 5 Vertex v2; v2.number = 2; v2.p = NULL; v[1] = v2; 6 Vertex v3; v3.number = 3; v3.p = NULL; v[2] = v3; 7 Vertex v4; v4.number = 4; v4.p = NULL; v[3] = v4; 8 Vertex v5; v5.number = 5; v5.p = NULL; v[4] = v5; 9 graph.vertex = v; 10 11 GNode nodes[5]; 12 GNode n1; n1.number = 1; 13 GNode n2; n2.number = 2; 14 GNode n3; n3.number = 3; 15 GNode n4; n4.number = 4; 16 GNode n5; n5.number = 5; 17 GNode a; a.number = 2; GNode b; b.number = 4; n1.next = &a; a.next = &b; b.next = NULL; 18 GNode c; c.number = 3; GNode x; x.number = 4; GNode z; z.number = 5; n2.next = &c; c.next = &x; x.next = &z; z.next = NULL; 19 GNode d; d.number = 2; n3.next = &d; d.next = NULL; 20 GNode f; f.number = 5; GNode g; g.number = 3; n4.next = &f; f.next = &g; g.next = NULL; 21 GNode h; h.number = 1; GNode i; i.number = 3; n5.next = &h; h.next = &i; i.next = NULL; 22 nodes[0] = n1; 23 nodes[1] = n2; 24 nodes[2] = n3; 25 nodes[3] = n4; 26 nodes[4] = n5; 27 graph.LinkTable = nodes; 28 29 int w[5][5] = { 0, 6, INF, 7, INF, 30 INF, 0, 5, 8, -4, 31 INF, -2, 0, INF, INF, 32 INF, INF, -3, 0, 9, 33 2, INF, 7, INF, 0 }; 34 int s = 1; 35 if (Bellman_Ford(&graph, (int **)w, s)) 36 { 37 for (int i = 0; i < graph.VertexNum; i++) 38 { 39 if (i != s - 1) 40 { 41 Vertex *v = graph.vertex + i; 42 printf("路徑長度為%d , 路徑為 : ", v->weight); 43 while (v->p != NULL) 44 { 45 printf("%d <- ", v->number, v->p->number); 46 v = v->p; 47 } 48 printf("%d\n", s); 49 } 50 } 51 }
上面的例程構建的圖如下圖所示。
Bellman-Ford算法運行過程中各頂點v到源點s=1的距離變化如下所示。
0 INF INF INF INF
0 6 4 7 2
0 2 4 7 2
0 2 4 7 -2
以頂點1到頂點2的路徑變化為例,對應上面距離變化的順序,如下所示。
無路徑 ---> <1,2> ---> <1,4,3,2> ---> <1,4,3,2>
算法運行的最終結果如下圖所示。
Graph graph;
graph.VertexNum = 5;
Vertex v[5];
Vertex v1; v1.number = 1; v1.p = NULL; v[0] = v1;
Vertex v2; v2.number = 2; v2.p = NULL; v[1] = v2;
Vertex v3; v3.number = 3; v3.p = NULL; v[2] = v3;
Vertex v4; v4.number = 4; v4.p = NULL; v[3] = v4;
Vertex v5; v5.number = 5; v5.p = NULL; v[4] = v5;
graph.vertex = v;
GNode nodes[5];
GNode n1; n1.number = 1;
GNode n2; n2.number = 2;
GNode n3; n3.number = 3;
GNode n4; n4.number = 4;
GNode n5; n5.number = 5;
GNode a; a.number = 2; GNode b; b.number = 4; n1.next = &a; a.next = &b; b.next = NULL;
GNode c; c.number = 3; GNode x; x.number = 4; GNode z; z.number = 5; n2.next = &c; c.next = &x; x.next = &z; z.next = NULL;
GNode d; d.number = 2; n3.next = &d; d.next = NULL;
GNode f; f.number = 5; GNode g; g.number = 3; n4.next = &f; f.next = &g; g.next = NULL;
GNode h; h.number = 1; GNode i; i.number = 3; n5.next = &h; h.next = &i; i.next = NULL;
nodes[0] = n1;
nodes[1] = n2;
nodes[2] = n3;
nodes[3] = n4;
nodes[4] = n5;
graph.LinkTable = nodes;
int w[5][5] = { 0, 6, INF, 7, INF,
INF, 0, 5, 8, -4,
INF, -2, 0, INF, INF,
INF, INF, -3, 0, 9,
2, INF, 7, INF, 0 };
int s = 1;
if (Bellman_Ford(&graph, (int **)w, s))
{
for (int i = 0; i < graph.VertexNum; i++)
{
if (i != s - 1)
{
Vertex *v = graph.vertex + i;
printf("路徑長度為%d , 路徑為 : ", v->weight);
while (v->p != NULL)
{
printf("%d <- ", v->number, v->p->number);
v = v->p;
}
printf("%d\n", s);
}
}
}
算法筆記-----單源最短路徑之Bellman-Ford算法