1. 程式人生 > >最短路徑演算法(Shortest-path Algorithms)

最短路徑演算法(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的最早完成時間,那麼利用下面的法則


最早完成時間結果為: