【圖(中)】最短路徑問題
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]
下圖所示,源點為V3,經過上面的演算法,每一個頂點都被標記的到源點的距離,從每一個頂點反向推可以得到源點到該點最短路徑經過的頂點。
時間複雜度:
如果有|V|個頂點和|E|條邊的圖用鄰接表儲存,則
T = O( |V| + |E| )
每個頂點入棧一次,出棧一次,每條邊被訪問一次
4、有權圖的單源最短路演算法(Dijkstra演算法)
這裡不考錄負值圈
按照遞增的順序找出到各個頂點的最短路
Dijkstra 演算法
- 令S={源點s + 已經確定了最短路徑的頂點vi
- 對任一未收錄的頂點v,定義dist[v]為s到v的最短路徑長度,但該路徑
僅經過S中的頂點
。即路徑的最小長度(注意:這裡的最短路徑不是最終的最短路徑,隨著其他頂點加進來,dist[v]會逐漸變小,最終成為最終的最短路徑) - 若路徑是按照
遞增(非遞減)
的順序生成的,則- 真正的最短路必須只經過S中的頂點(為什麼?)
- 每次從未收錄的頂點中選一個dist最小的收錄(貪心)
- 增加一個v進入S,可能影響另外一個w的dist值!
- dist[w] = min{dist[w], dist[v] + <v,w>的權重}
-
真正的最短路必須只經過S中的頂點,原因如下:
現在假設在集合S之外存在一個頂點w,使得s經w到到v的距離小於s直接到v的距離,則s到w的距離一定小於s到v的距離。下一步,我們要收錄頂點v,s到w的距離小於s到v的距離,頂點w應該在v之前被收錄到集合S中了,自相矛盾。 -
如果收錄v使得s到w的路徑變短,則:s到w的路徑一定經過v,並且v到w有一條邊,原因如下:
現在假設v到w的路徑上還有一點a,則s到a的路徑長度一定大於s到v的路徑長度,由於若路徑是按照遞增(非遞減)的順序生成的,所以a的收錄應該在點v之後,自相矛盾。 -
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要經過哪些頂點,最終得到路徑。