1. 程式人生 > >結點對最短路徑之Floyd演算法原理詳解及實現

結點對最短路徑之Floyd演算法原理詳解及實現

上兩篇部落格介紹了計算單源最短路徑的Bellman-Ford演算法和Dijkstra演算法。Bellman-Ford演算法適用於任何有向圖,即使圖中包含負環路,它還能報告此問題。Dijkstra演算法執行速度比Bellman-Ford演算法要快,但是其要求圖中不能包含負權重的邊。

在很多實際問題中,我們需要計算圖中所有結點對間的最短路徑。當然,我們可以使用上述兩種演算法來計算每一個頂點的單源最短路徑,對於圖G=(V,E)來說,使用Bellman-Ford演算法計算結點對最短路徑的時間複雜度為O(V^2 * E),使用Dijkstra演算法計算結點對最短路徑的時間複雜度為O(V^3)。本文將會介紹一種應用更廣泛的演算法,而且它可以應用於有負權重邊但沒有負環路的圖中,其時間複雜度為O(V^3),那就是Floyd-Warshall演算法。

1. Floyd演算法的原理

在上一篇部落格 中,提到了圖的一個重要性質:一條最短路徑的子路徑也是一條最短路徑。因此,一條最短路徑要麼只包含一條直接相連的邊,要麼就經過一條或多條到達其它頂點的最短路徑。

上圖給出的是頂點i到頂點j的路徑示意圖。i到j的路徑為<i,...,k,...,j>,其中頂點k是路徑i到j的一個編號最大的中間頂點,即路徑<i,...,k>中的所有頂點編號求取自集合{1,2,3,...,k-1},路徑<k,...,j>也是一樣的。因為路徑<i,...,k,...,j>為最短路徑,那麼路徑<i,...,k>路徑<k,...,j>

也是最短路徑。對路徑<i,...,k>路徑<k,...,j>也可以遞迴做出上述操作。

於是,我們可以推出如下遞迴公式。

dij(k) = wij                                                        當k=0;

dij(k) = min(dij(k-1), dik(k-1)+dkj(k-1))             當k>0;

上述公式中dij為頂點i到頂點j的當前路徑的長度,k是當前遞迴中路徑的最大頂點編號。當k=0時,路徑的中間頂點的編號不大於0,即不存在任何中間頂點,這種情況頂點i到頂點j的路徑必然只是一條連線這兩個頂點的邊,因此其長度為該邊的權重。當k>0,每次遞迴時加入編號為k的頂點,可以根據其它"當前最短路徑"構造頂點i到頂點j的一條新路徑,並與其原路徑進行比較,從中選擇更短的。這是一種自底向上的動態規劃演算法。

2. Floyd演算法的C實現

本文實現的Floyd演算法所需要的輸入與前面的部落格介紹的不一樣。前面介紹的所有圖演算法需要的圖都是用鄰接表表示的。下面給出的Floyd演算法需要的圖使用鄰接矩陣表示的,即權重圖。該實現使用前驅子圖(二維矩陣)來記錄結點對的最短路徑的目的頂點的前驅頂點編號(前一個頂點的編號)。

/**
* Floyd 尋找結點對的最短路徑演算法
* w 權重圖
* vertexNum 頂點個數
* lenMatrix 計算結果的最短路徑長度儲存矩陣(二維)
* priorMatrix 前驅子圖(二維),路徑<i, ..., j>重點j的前一個頂點k儲存在priorMatrix[i][j]中
*/
void Floyd_WallShall(int **w, int vertexNum, int **lenMatrix, int **priorMatrix)
{
	// 初始化
	for (int i = 0; i < vertexNum; i++)
	{
		for (int j = 0; j < vertexNum; j++)
		{
			*((int*)lenMatrix + i*vertexNum + j) = *((int*)w + i*vertexNum + j);
			if (*((int*)w + i*vertexNum + j) != INF && i != j)
			{
				*((int*)priorMatrix + i*vertexNum + j) = i;
			}
			else
			{
				*((int*)priorMatrix + i*vertexNum + j) = -1;
			}
		}
	}

	// Floyd演算法
	for (int k = 0; k < vertexNum; k++)
	{
		for (int i = 0; i < vertexNum; i++)
		{
			for (int j = 0; j < vertexNum; j++)
			{
				int Dij = *((int*)lenMatrix + i*vertexNum + j);
				int Dik = *((int*)lenMatrix + i*vertexNum + k);
				int Dkj = *((int*)lenMatrix + k*vertexNum + j);
				if (Dik != INF && Dkj != INF && Dij > Dik + Dkj)
				{
					*((int*)lenMatrix + i*vertexNum + j) = Dik + Dkj;
					*((int*)priorMatrix + i*vertexNum + j) = *((int*)priorMatrix + k*vertexNum + j);
				}
			}
		}
	}
}
上述程式需要輸入一個鄰接矩陣,頂點的個數,以及用於儲存結果路徑長度的矩陣和前驅子圖矩陣。這些矩陣本質上均是一個二維陣列。該演算法首先對長度矩陣和前驅子圖進行初始化,也就是遞推公式當k=0時的操作,然後就進入迴圈反覆更新結點對的路徑。演算法沒計算一次所有結點對的路徑,需要進行V^2次運算,而演算法需要從小到大依次將V個頂點加入到圖中進行運算,於是整個演算法的時間複雜度為O(V^3)。

這裡簡單說一下前驅子圖priorMatrix。我們可以通過前驅子圖找到任意結點對的最短路徑。例如我們要找到頂點i到頂點j的一條最短路徑,可以先找到k=priorMatrix[i][j],此時就知道路徑為<i,...,k,j>,然後我們再找到路徑<i,...,k>的前驅頂點,即priorMatrix[i][k],如此類推。這一操作的正確性由上面提到的性質(一條最短路徑的子路徑也是一條最短路徑)保證。

下面給出一個應用上述演算法的例子。

	int w[5][5] = {	0,		3,		8,		INF,	-4,
					INF,	0,		INF,	1,		7,
					INF,	4,		0,		INF,	INF,
					2,		INF,	-5,		0,		INF,
					INF,	INF,	INF,	6,		0};
	int lenMatrix[5][5];
	int priorMatrix[5][5];

	Floyd_WallShall((int**)w, 5, (int**)lenMatrix, (int**)priorMatrix);
	for (int i = 0; i < 5; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			if (lenMatrix[i][j] == INF)
			{
				printf("從%d到%d\t\t長度:INF\n", i, j);
			}
			else
			{
				printf("從%d到%d\t\t長度:%d\t\t路徑:", i, j, lenMatrix[i][j]);
				printIJPath((int**)priorMatrix, 5, i + 1, j + 1);
			}
		}
	}
printIJPath方法的定義如下。
/**
* 根據前驅子圖列印i到j的路徑,輸入頂點編號從1開始,輸出頂點編號從1開始
*/
void printIJPath(int **prior, int vertexNum, int i, int j)
{
	i--; j--;
	printf("%d", j + 1);
	int k = *((int*)prior + i*vertexNum + j);
	while (k != -1)
	{
		printf(" <- %d", k + 1);
		k = *((int*)prior + i*vertexNum + k);
	}
	printf("\n");
}
上述例程構造的圖以及執行結果如下圖所示。前驅子圖總priorMatrix[i][i]=-1。

長度矩陣
0 1 -3 2 -4
3 0 -4 1 -1
7 4 0 5 3
2 -1 -5 0 -2
8 5 1 6 0
前驅子圖
-1 2 3 4 0
3 -1 3 1 0
3 2 -1 1 0
3 2 3 -1 0
3 2 3 4 -

3. 總結

Floyd演算法的時間複雜度為O(V^3),因為其實現程式碼很緊湊,所以時間複雜度的常數項很小。Floyd演算法是一種應用非常廣泛的計算結點對最短路徑的演算法。其實還有一種結合了Bellman-Ford演算法和Dijkstra演算法的Johnson演算法,該演算法在用於稀疏圖時執行速度比Floyd演算法更快,並且能夠報告圖中存在負環路的情況(得益於Bellman-Ford演算法)。Johnson演算法的時間複雜度為Bellman-Ford演算法的時間複雜度加上Dijkstra演算法的時間複雜度。如果使用二叉堆實現Dijkstra演算法的最小優先佇列,那麼Johnson演算法時間複雜度為O(VElgV+VE)=O(VElgV)。Johnson演算法的具體介紹可以參考其它資料,下面給出的個github專案中也有具體的C實現程式碼。

完整的程式可以看到我的github專案 資料結構與演算法

這個專案裡面有本部落格介紹過的和沒有介紹的以及將要介紹的《演算法導論》中部分主要的資料結構和演算法的C實現,有興趣的可以fork或者star一下哦~ 由於本人還在研究《演算法導論》,所以這個專案還會持續更新哦~ 大家一起好好學習~