1. 程式人生 > 實用技巧 >動態規劃法解多段圖最短路徑問題

動態規劃法解多段圖最短路徑問題

目錄

動態規劃法

動態規劃法將待求解問題分解成若干個相互重疊的子問題,每個子問題對應決策過程的一個階段,一般來說,子問題的重疊關係表現在對給定問題求解的遞推關係稱為動態規劃函式中,將子問題的解求解一次並填入表中,當需要再次求解此子問題時,可以通過查表獲得該子問題的解,從而避免了大量重複計算。具體的動態規劃法多種多樣,但都具有相同的填表形式。一般來說,動態規劃法的求解過程由以下三個階段組成:

  1. 劃分子問題:將原問題分解為若干個子問題,每個子問題對應一個決策階段,並且子問題之間具有重疊關係。
  2. 確定動態規劃函式:根據子問題之間的重疊關係找到子問題滿足的遞推關係式即動態規劃函式,這是動態規劃法的關鍵。
  3. 填寫表格:設計表格,以自底向上的方式計算各個子問題的解並填表,實現動態規劃過程。

上述動態規劃過程可以求得問題的最優值即目標函式的極值,如果要求出具體的最優解,通常在動態規劃過程中記錄必要的資訊,再根據最優決策序列構造最優解。

多段圖最短路徑問題

設圖 G =(V,E)是一個帶權有向圖,如果把頂點集合 V 劃分成 k 個互不相交的子集 Vi(2<=k<=n,1<=i<=k),使得 E 中的任何一條邊 <u,v>,必有 u∈Vi, v∈Vi + m(1<=i<k, 1<i+m<=k),則稱圖 G 為多段圖,稱 s∈V1 為源點,t∈Vk 為終點。多段圖的最短路徑問題為從源點到終點的最小代價路徑。

問題分析

根據多段圖的性質,我們可以將這種特殊的圖結構劃分為多個子集,例如如圖所示的多段圖就可以分成 5 個子集,在圖中以 5 種不同顏色來表示。可以明顯地看到想要到達某一個子集的頂點,就必須從上一個相鄰頂點集的頂點出發,不相鄰的子集之間不存在可達的邊。
針對這個特性可以推匯出解決問題的方法,例如我想要到達頂點 10,那就必須要先到達頂點 8 或者頂點 9。換句話說,到達頂點 10 的最短距離就是在到達頂點8的最短距離 d(1,8) 加上邊 (8,10) 的權重,和到達頂點 9 的最短距離 d(1,9) 加上邊 (9,10) 的權重中取最小值。因為不相鄰的頂點集之間不存在邊,所以到達頂點 10 的方式有且僅有上述 2 種。設 C 為某條邊的權重,d(m,n) 為從點 m 到點 n 的最短距離,則使用數學語言的描述如下:

再看一個例子,假設要分析到達頂點 8 的最短距離,則只有 3 種情況。即到達頂點 5 的最短距離 d(1,5) 加上邊 (5,8) 的權重,和到達頂點 6 的最短距離 d(1,6) 加上邊 (6,8) 的權重,和到達頂點 7 的最短距離 d(1,7) 加上邊 (7,8) 的權重三者之間取最小值。使用數學語言的描述如下:

根據上面 2 個例子的論述,我們可以把情況從特殊推廣到一般情況,設 Cuv 為多段圖有向邊 <u,v> 的權值,源點 s 到終點 v 的最短路徑長為 d(s,v),終點為 t,則可以得到該問題的狀態轉移方程為:

最優子結構證明

設一個多段圖有且僅有一個起點 S,有且僅有一個終點 T,S->S1->S2->…Sn->T 為從起點 S 到終點 T 的最短路徑。設 S->S1 的開銷已經求出,則從起點 S 到終點 T 的最小開銷的求解將轉換為對點 S1 到終點 T 的最小開銷進行求解。
假設 S1->S2->S3…Sn->T 不是點 S1 到終點 T 的最短路徑,則必然存在另一條路徑 S1->R1->R2…Rn->T 的開銷小於 S1->S2->S3…Sn->T 的路徑開銷,進而推出起點 S 到終點 T 的最短路徑為 S->S1->R1->R2…Rn->T。然而已知路徑 S->S1->S2->…Sn->T 為起點 S 到終點 T 的最短路徑,不可能存在其他路徑的總開銷比該路徑的開銷還要小,產生了矛盾,因此多段圖的最短路徑問題滿足最優子結構。

問題求解

例如上述例子中的多段圖,可以建立圖的鄰接矩陣 topography.edges[MAXV][MAXV] 為:

需要一個輔助結構:一維陣列 cost[MAXV] 儲存到每個頂點的最小開銷。例如我們已經求出了到達頂點 1~9 的最小開銷如下:

則根據狀態轉移方程,可以得出從頂點 1 到頂點 10 的最小開銷為:

如果我們要找出最短路徑,還需要一個一維陣列 path[MAXV],用於儲存每個頂點的前驅頂點。

找到最短路徑的方式為從 path[10] 開始依次訪問對應的頂點,訪問的次序即為所求的最短路徑,直到訪問了起點為結束。

程式編寫

首先定義圖結構的結構體,使用鄰接矩陣來儲存:

typedef struct    //圖的定義
{
      int edges[MAXV][MAXV];    //鄰接矩陣
      int n;    //頂點數
} MGraph;

定義求解問題需要的輔助結構:

MGraph topography;    //儲存城市關係的鄰接矩陣 
int path[MAXV] = {};    //儲存到該頂點的最短路徑對應的前驅 
int min_cost[MAXV] = {};    //儲存到每個頂點的最短路徑長 

要根據測試樣例資料建立多段圖的鄰接矩陣:

MGraph CreateMGraph(int num)    //建圖 
{
	MGraph topography;
	int n;
	int point1, point2;
	int value;
	
	//初始化邊為不存在 
	for(int i = 1; i <= num; i++)
	{
		for(int j = 1; j <= num; j++)
		{
			topography.edges[i][j] = 0;
		}
	}
	cout << "請輸入邊數:";
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		cin >> point1 >> point2 >> value;
		topography.edges[point1][point2] = value;
	}
	cout << "\n建立的鄰接矩陣為:" << endl; 
	for(int i = 1; i <= num; i++)
	{
		for(int j = 1; j <= num; j++)
		{
			printf("%2d ",topography.edges[i][j]);
		}
		cout << endl;
	}
	cout << endl;
	topography.n = num;
	return topography;
}

根據狀態轉移方程求解問題,最後輸出最短路徑及其開銷:

int main()
{
	int cities_num = 0;    //城市數量 
	int a_cost;    //當前路徑的開銷
	int pre;
	
	cout << "城市數量為:"; 
	cin >> cities_num;
	//建圖
	topography = CreateMGraph(cities_num);
        //初始化路徑開銷
	min_cost[1] = 0; 
	for(int i = 2; i <= topography.n; i++) 
	{
		min_cost[i] = 99999;
	}
	//依次計算到達所有點的最短路徑 
	for(int i = 2; i <= cities_num; i++)
	{
		//遍歷之前的所有點,計算到達該點的最短路徑 
		for(int j = 1; j < i; j++)
		{
			if(topography.edges[j][i] != 0)    //若路徑存在 
			{
				a_cost =  min_cost[j] + topography.edges[j][i];
				if(a_cost < min_cost[i])    //更新最短路徑長 
				{
					min_cost[i] = a_cost;
					path[i] = j;    //記錄前驅頂點 
				}
			}
		}
	}
	//輸出到所有頂點的最短路徑 
	for(int i = 1; i <= cities_num; i++)
	{
		cout << "到頂點" << i << "的最小開銷為:" << min_cost[i] << ",路徑:" << i;
		pre = i;
		while(path[pre])
		{
			cout << "<-" << path[pre];
			pre = path[pre];
		}
		cout << endl;
	}
        return 0;
}

測試樣例

樣例一

輸入資料

10
18
1 2 4
1 3 2
1 4 3
2 5 9
2 6 8
3 5 6
3 6 7
3 7 8
4 6 4
4 7 7
5 8 5
5 9 6
6 8 8
6 9 6
7 8 6
7 9 5
8 10 7
9 10 3

輸出資料

樣例二

輸入資料

16
30
1 2 5
1 3 3
2 4 1
2 5 3
2 6 6
3 5 8
3 6 7
3 7 6
4 8 6
4 9 8
5 8 3
5 9 5
6 9 3
6 10 3
7 9 8
7 10 4
8 11 2
8 12 2
9 12 1
9 13 2
10 12 3
10 13 3
11 14 3
11 15 5
12 14 5
12 15 2
13 14 6
13 15 6
14 16 4
15 16 3

輸出資料

樣例三

輸入資料

12
21
1 2 7
1 3 6
1 4 4
1 5 2
2 6 4
2 7 2
2 8 1
3 6 2
3 7 7
4 8 11
5 7 11
5 8 8
6 9 6
6 10 5
7 9 4
7 10 3
8 10 5
8 11 6
9 12 4
10 12 2
11 12 5 

輸出資料

演算法分析

演算法的時間複雜度主要由兩部分組成:第一部分是依次計算從源點到各個頂點的最短路徑長度,由兩層巢狀的迴圈組成,外層迴圈執行 n-1 次,內層迴圈對所有入邊進行計算,並且在所有迴圈中,每條入邊只計算一次。假定圖的邊數為 m,則時間效能是 O(m)。第二部分是輸出最短路徑經過的頂點,設多段圖劃分為 k 段,其時間效能是 O(k)。綜上所述,時間複雜度為 O(m+k)

參考資料

《演算法設計與分析(第二版)》——王紅梅,胡明 編著,清華大學出版社