單源最短路徑之Bellman-Ford演算法
今天介紹一種計算單源最短路徑的演算法Bellman-Ford演算法,對於圖G=(V,E)來說,該演算法的時間複雜度為O(VE),其中V是頂點數,E是邊數。Bellman-Ford演算法適用於任何有向圖,並能報告圖中存在負環路(邊的權重之和為負數的環路,這使得圖中所有經過該環路的路徑的長度都可以通過反覆行走該環路而使路徑長度變小,即沒有最短路徑)的情況。以後會介紹執行速度更快,但只適用於沒有負權重邊的圖中的Dijkstra演算法。Dijkstra演算法可以參考我的下一篇部落格
單源最短路徑之Dijkstra演算法
在介紹Bellman-Ford演算法之前,先介紹在計算圖的單源最短路徑的各種演算法中都會用到的鬆弛操作。
// 鬆弛操作,檢查<s, ..., v>的距離是否比<s, ..., u, v>大,是則更新<s, ..., v>為<s, ..., u, v>
void relax(Vertex *u, Vertex *v, int w)
{
if (u->weight == INF || w == INF) return;
if (v->weight > u->weight + w)
{
v->weight = u->weight + w;
v->p = u;
}
}
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開始)。
typedef struct GNode
{
int number; // 頂點編號
struct GNode *next;
} GNode;
typedef struct Vertex
{
int number;
int weight; // 該頂點到源點的距離
struct Vertex *p;
} Vertex;
typedef struct Graph
{
GNode *LinkTable;
Vertex *vertex;
int VertexNum;
} Graph;
/** * Bellman Ford 單源最短路徑演算法 * @return true 沒有負環路; false 有負環路,最短路徑構造失敗 */ bool Bellman_Ford(Graph *g, int **w, int s) { initialize(g, s); GNode *linkTable = g->LinkTable; for (int i = 1; i < g->VertexNum; i++) { // 反覆將邊加入到已有的最小路徑圖中,檢查是否有更優路徑 for (int j = 0; j < g->VertexNum; j++) { GNode *node = (linkTable + j)->next; Vertex *u = g->vertex + j; while (node != NULL) { Vertex *v = g->vertex + node->number - 1; int weight = *((int*)w + j * g->VertexNum + node->number - 1); relax(u, v, weight); node = node->next; } } } // 通過檢查是否都已達到最短路徑來檢查是否存在負環路 for (int j = 0; j < g->VertexNum; j++) { GNode *node = (linkTable + j)->next; Vertex *u = g->vertex + j; while (node != NULL) { Vertex *v = g->vertex + node->number - 1; int weight = *((int*)w + j * g->VertexNum + node->number - 1); if (v->weight > u->weight + weight) { return false; } node = node->next; } } return true; }
void initialize(Graph *g, int s)
{
Vertex *vs = g->vertex;
for (int i = 0; i < g->VertexNum; i++)
{
Vertex *v = vs + i;
v->p = NULL;
v->weight = INF;
}
(vs + s - 1)->weight = 0;
}
上述演算法程式碼實現的Bellman-Ford演算法進行了V次對所有邊的鬆弛操作,這是考慮到了最壞情況,假設圖G是一條單鏈,則從表頭s到表尾的路徑計算需要進行V次鬆弛操作。下面給出一個演示例子。
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演算法執行過程中各頂點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>
演算法執行的最終結果如下圖所示。
完整的程式可以看到我的github專案 資料結構與演算法
這個專案裡面有本部落格介紹過的和沒有介紹的以及將要介紹的《演算法導論》中部分主要的資料結構和演算法的C實現,有興趣的可以fork或者star一下哦~ 由於本人還在研究《演算法導論》,所以這個專案還會持續更新哦~ 大家一起好好學習~