1. 程式人生 > >【圖(中)】最短路徑問題

【圖(中)】最短路徑問題

1、最短路徑問題的抽象

  • 在網路中,求兩個不同頂點之間的所有路徑中,邊的權值之和最小的那一條路徑
    • 這條路徑就是兩點之間的最短路徑(Shortest Path)
    • 第一個頂點為源點(Source)
    • 最後一個頂點為終點(Destination)

2、問題分類

  • 單源最短路徑問題:從某固定源點出發,求其到所有其他頂點的最短路徑
    • (有向)無權圖
    • (有向)有權圖
  • 多源最短路徑問題:求任意兩頂點間的最短路徑

3、無權圖的單源最短路演算法

  • 按照遞增(非遞減)
    的順序找出到各個頂點的最短路
    在這裡插入圖片描述

無權圖的單源最短路演算法

void Unweighted(Vertex S)
{
	Enqueue(S, Q);
	while (!IsEmpty(Q))
	{
		V = Dequeue(Q);
		for (V 的每個鄰接點W)
			if (dist[W] == -1)
			{
				dist[W] = dist[V] + 1;
				path[W] = V;
				Enqueue(W, Q);
			}
	}
}

dist[W]:S到W的最短距離,初始化時,初始化為不可能的數,這裡初始化為-1
dist[S]:0
path[W]

:S到W的路上經過的某頂點,path[W] = V表示S到W的路徑上,W上一步為V。順著path陣列順著向前推,就是路徑,用棧可以得到順序的路徑。

下圖所示,源點為V3,經過上面的演算法,每一個頂點都被標記的到源點的距離,從每一個頂點反向推可以得到源點到該點最短路徑經過的頂點。
在這裡插入圖片描述

時間複雜度:

如果有|V|個頂點和|E|條邊的圖用鄰接表儲存,則
T = O( |V| + |E| )
每個頂點入棧一次,出棧一次,每條邊被訪問一次

4、有權圖的單源最短路演算法(Dijkstra演算法)

在這裡插入圖片描述
這裡不考錄負值圈

按照遞增的順序找出到各個頂點的最短路

Dijkstra 演算法

  • 令S={源點s + 已經確定了最短路徑的頂點vi
    }(先將路徑較小的頂點收錄進來,如剩下未收錄的頂點中路徑分別為3,5,2,無窮大,則下一步將距離為2的收錄進來)
  • 對任一未收錄的頂點v,定義dist[v]為s到v的最短路徑長度,但該路徑僅經過S中的頂點。即路徑在這裡插入圖片描述的最小長度(注意:這裡的最短路徑不是最終的最短路徑,隨著其他頂點加進來,dist[v]會逐漸變小,最終成為最終的最短路徑)
  • 若路徑是按照遞增(非遞減)的順序生成的,則
    • 真正的最短路必須只經過S中的頂點(為什麼?)
    • 每次從未收錄的頂點中選一個dist最小的收錄(貪心)
    • 增加一個v進入S,可能影響另外一個w的dist值!
    • dist[w] = min{dist[w], dist[v] + <v,w>的權重}
  1. 真正的最短路必須只經過S中的頂點,原因如下:
    現在假設在集合S之外存在一個頂點w,使得s經w到到v的距離小於s直接到v的距離,則s到w的距離一定小於s到v的距離。下一步,我們要收錄頂點v,s到w的距離小於s到v的距離,頂點w應該在v之前被收錄到集合S中了,自相矛盾。

  2. 如果收錄v使得s到w的路徑變短,則:s到w的路徑一定經過v,並且v到w有一條邊,原因如下:
    在這裡插入圖片描述
    現在假設v到w的路徑上還有一點a,則s到a的路徑長度一定大於s到v的路徑長度,由於若路徑是按照遞增(非遞減)的順序生成的,所以a的收錄應該在點v之後,自相矛盾。

  3. Dijkstra演算法中的dist應該如何初始化?
    如果s到w有直接的邊,則dist[w]=<s,w>的權重;否則dist[w]定義為正無窮

//不能解決有負邊的情況
void Dijkstra(Vertex s)
{
	while (1)
	{
		V = 未收錄頂點中dist最小者;
		if (這樣的V不存在)
			break;
		collected[V] = true;
		for (V 的每個鄰接點W)
			if (collected[W] == false)
				if (dist[V] + E<V, W> < dist[W])
				{
					dist[W] = dist[V] + E<V, W>;
					path[W] = V;
				}
	}
}

在這裡插入圖片描述

有權圖的單源最短路演算法時間複雜度

  • 方法1:直接掃描所有未收錄頂點– O( |V| )

    • T = O( |V|2 + |E| )
    • 對於稠密圖效果好
  • 方法2:將dist存在最小堆中– O( log|V| )

    • 更新dist[w]的值– O( log|V| )
    • T = O( |V| log|V| + |E| log|V| ) = O( |E| log|V| )
    • 對於稀疏圖效果好

5、多源最短路演算法

  • 方法1:直接將單源最短路演算法呼叫|V|遍
    • T = O( |V|3 + |E|*|V|)
    • 對於稀疏圖效果好
  • Floyd演算法
    • T = O( |V|3 )
    • 對於稠密圖效果好

Floyd 演算法

  • Dk[i][j] = 路徑{ i → { l<=k } → j }的最小長度,i到j的最短路徑,只經過編號小於等於k的頂點。

  • D0, D1, …, D|V|-1[i][j]即給出了i到j的真正最短距離

  • 最初的D-1是什麼?

  • 當Dk-1已經完成,遞推到Dk時:

    • 或者k不屬於最短路徑{ i → { l<=k } → j },則Dk= Dk-1
    • 或者k屬於最短路徑{ i → { l<=k } → j },則該路徑必定由兩段最短路徑組成: Dk[i][j]=Dk-1[i][k]+Dk-1[k][j]

i到j的頂點路徑經過頂點k時,i到j的頂點路徑包括兩段,i到k,k到j,這兩段距離中間都不包含k,所以應該在k-1步的時候已經求出。

D矩陣應該初始化:帶權的鄰接矩陣,對角元是0;如果i和j之間有直接邊相連,初始化為邊的權重,沒有直接的邊,D[i][j]應該定義為正無窮

void Floyd()
{
	for (i = 0; i < N; i++)
		for (j = 0; j < N; j++)
		{//初始化就是鄰接矩陣,有相鄰節點初始化為權重,
		//沒有直接邊相連,初始化為正無窮,對角元素為0,就是鄰接矩陣
			D[i][j] = G[i][j];
			path[i][j] = -1;
		}
	for (k = 0; k < N; k++)
		for (i = 0; i < N; i++)
			for (j = 0; j < N; j++)
				if (D[i][k] + D[k][j] < D[i][j])
				{
					D[i][j] = D[i][k] + D[k][j];
					path[i][j] = k;
				}
}

根據path列印路徑,path[i][j]=k;表示從結點i到結點j要經過結點k,遞迴呼叫,從節點i到節點k要經過那些頂點,從節點k到節點j要經過哪些頂點,最終得到路徑。