最短路徑演算法(Shortest-path Algorithms)
0) 引論
正如名字所言,最短路徑演算法就是為了找到一個圖中,某一個點到其他點的最短路徑或者是距離。
最短路徑演算法一般分為四種情況:
a) 無權重的最短路徑
b) 有權重的最短路徑
c) 邊的權重為負的圖
d) 無圈的圖
ps:上面的情況針對的都是有向圖。
1) 無權重的最短路徑
下圖是一個例子:假設我們取點v3作為初始點,計算點v3到圖中所有點的路徑以及距離(包括點v3)。
a) v3到v3的路徑長為0。
b) 沿著v3的鄰接點查詢,找到v1,那麼v3到v1的路徑長為1;找到v6,那麼v3到v6的路徑長為1。
c) v3的鄰接點已經找完了,接下來找v1,v6的鄰接點。
d) v1的鄰接點為v2,v4,它們對應的路經長為2;v6無鄰接點,v6部分結束。
e) v2的鄰接點為v4,v5,而v4已經查詢過了,捨棄;v2到v5的路徑長為3;v4的鄰接點為v5,v7,而v5已經查詢過了,捨棄;v4到v7的路徑長為3。
f) 至此,結束。
對於這一步驟,可以用一個表格來表示,以便於程式設計理解:
表中有3個表示狀態的變數:
a) dv表示點s到圖中每個點的距離,結合上面的例子,s=v3.
b) pv表示圖中點的路徑前繼點。
c) Known表示點是否被處理,處理了,則標記為1 。
虛擬碼:
void Unweighted(Table T) { int CurrDist; Vertex V,W; for(CurrDist=0;CurrDist<NumVertex;CurrDist++) { for each vertex V if(!T[V].Known&&T[V].Dist==CurrDist) { T[V].Known = True; for each W adjacent to V if(T[W].Dist == Infinity) { T[W].Dist = CurrDist+1; T[W].Path = V; } } } }
通拓撲排序一樣,對於上面的程式也是存在改進的地方的(第07行),我們沒有必要去對each vertex V做一下這麼多的步驟,只需要對相鄰接的點做就好了。因此用一個佇列來使每一個處理的點入隊,然後處理其鄰接的點。
void Unweighted(Table T) { Queue Q; Vertex V,W; Q = CreateQueue(NumVertex); MakeEmpty(Q); Enqueue(S,Q);//S is the start Vertex while(!IsEmpty(Q)) { V = Dequeue(Q); T[V].Known=True; for each W adjacent to V if(T[W].Dist == Infinity) { T[W].Dist = T[V].Dist+1; T[W].Path = V; Enqueue(W,Q); } } Free(Q); }
2) 有權重的最短路徑
有權重的最短路徑演算法,稱之為Dijkstra‘s algorithm。相對於無權重的最短路徑演算法,有權重的最短路徑演算法會顯得難一點,這是因為權重的引入會使某些路徑的加權重長度發生顛倒。
Dijkstra‘s algorithm是一種greedy algorithm。貪婪演算法就是在演算法的每一個階段都取最大值。一般情況下,貪婪演算法能取得較好的效果。但是貪婪演算法是有其缺陷的,也就是說能達到區域性最優,而未必能達到全域性最優,舉個例子,假設要兌換15分的硬幣,而硬幣有12分的,10分的,5分的,1分的,那麼根據貪婪演算法,最終的兌換結果為1個12分的,3個一分的;而最優結果應該是一個10分的,一個5分的。(這裡我們定義最優為硬幣個數最少)。
Dijkstra‘s algorithm的步驟是:
對於每一個階段,Dijkstra‘s algorithm會在所有未處理的點中選取一個最小距離的點v,然後把給定點s到v的路徑定義為最小路徑。
下面是一個例子:
對應的表的形式表示:
下面是具體的實現說明:
a) 最初的點s=v1;計算s到圖中所有點的最短路徑。
b) 找到v1鄰接的點v2,v4,分別標出s到v2,v4的路徑長,同時對v1標註T[v1].Known=1。
c) 由於v4路徑長較小,因此選擇v4,找到v4鄰接的點v3,v5,v6,v7,標註出他們的路徑長;同時對v1標註T[v4].Known=1。
d) 現有的所有未處理路徑中v2最短,因此處理v2,找到v2鄰接的點v4,v5; v2到v4,v5的路徑長遠大於v4,v5已有的路徑長,因此不更新。同時T[v2].Known=1。
e) 現有的所有未處理路徑中v3,v5最短,處理v3,鄰接點為v1,v6,路徑分別為7,8;對於v3到v6的距離,因為8<9,因此需要更新;對於v3到v1的距離不需要更新。同時T[v3].Known=1。
f) 對於v5,鄰接點為v7,v5到v7的路徑長為 3+6>5,因此不更新;同時T[v5].Known=1。
g) 現在處理v7,v7的鄰接點為v6,v7到v6的路徑長為5+1<8,因此需要更新;同時T[v7].Known=1。
h) 現在處理v6,v6沒有鄰接點了,因此可以結束了。同時T[v6].Known=1。
虛擬碼實現:
typedef int Vertex;
typedef int DistType;
struct TableEntry
{
List Header;
int Known;
DistType Dist;
Vertex Path;
}
#define NotAVertex (-1)
typedef struct TableEntry Table[NumVertex];
void InitTable(Vertex Start,Graph G,Table T)
{
int i;
ReadGraph(G,T);
for(i=0;i<NumVertex;i++)
{
T[i].Known = 0;
T[i].Dist = Infinity;
T[i].Path = NotAVertex;
}
T[Start].Dist = 0;
}
void Dijkstra(Table T)
{
Vertex V,W;
for(;;)
{
V = FindSmallestUnknownDistanceVertex();
if(V==NotAVertex)
break;
T[V].Known = Ture;
for each W adjacent to V
if(!T[V].Known)
if(T[V].Dist+Cvw<T[W].Dist)
{
Decrease(T[W].Dist to T[V].Dist+Cvw);
T[W].Path = V;
}
}
}
相對於無權重的最短路徑,這裡權重的更新為Dist = Dist + Cvw;
3) 邊的權重為負的圖
邊的權重為負,這將是一個比較糟糕的事情,這無法利用上面的兩種方法處理,因為只要迴圈負邊,則路徑可以無限變小,如下圖所示:
點1到點6的最短路徑是2麼?當然不是,我們沒法求出其最短路徑,因為我們可以沿著路徑1,6,5,7,6這樣一次下來,路徑為-3,由於6,5,7,6是一個圈,我們可以一直迴圈下去且路徑為負值,因此無法確定點1到點6的最短路徑。
那麼遇到這種問題該怎麼解決呢?
a) 給所有權重加上一個正值,是所有權重均為非負,上面的例子中,所有權重加上11,則就可以應用Dijkstra方法處理了。
4) 無圈的圖
對於無圈圖,也可以應用Dijkstra方法處理,無圈圖也可以看做是上面的一種特例。對於無圈圖,可以利用拓撲排序的順序選擇起始點,權重的更新也可以按照拓撲排序的順序進行,因此演算法可以一次進行,這個可以看做是對Dijkstra方法的改進,並且是向簡單方向的改進。
無圈圖可以模擬像下坡滑雪等問題,一直需要沿著向下的方向,不應該有圈。
而無圈圖的最大應用不在這裡,而是一個被稱之為關鍵路徑分析法的應用。
如下圖所示:
這幅圖表示的意思是要想做D中的事情,必須先做完A和B,而A,B則是可以並行的。因此這個圖中的問題不是尋找最短路徑,而是找到並行完成圖中的所有點所需要的最短時間,以及最晚的時間。
解決這個問題需要把上面的動作節點圖轉換為下面的事件節點圖
對於事件節點圖,只需要找出第一個事件到最後一個事件的最長路徑的長就好了。
假設ECi表示節點i的最早完成時間,那麼利用下面的法則
最早完成時間結果為: