.[算法]圖論專題之最短路徑
作者:jasonkent27
轉載請注明出處:www.cnblogs.com/jasonkent27
1. 前言
1.1 最短路引入
小明和小天現在住在海口(C1),他們倆計划暑假到三亞(C4)玩一趟,在海口和三亞之間有許多中間城市(文昌,臨高,樂東,萬寧...)圖中的邊上的數字是他們到達該城市必須的花費,現在需要你幫他們倆找出一條從海口到三亞的最省錢的路徑出來。
等等,圖中的邊的weight怎么會有負的呢?你暫且可以這么理解吧。圖中的邊上的weight可以當作他們旅途中必須的花費,但是他們倆在去三亞圖中把錢花光了(真是敗家)不得不通過搬磚賺錢,通過搬磚賺的錢不僅抵掉他們應該的花費,而且還賺了點小錢(但是他們不能一直搬磚啊,不然他們早就累掛了,哪里還能去玩)。
1.2 松弛技術
對於每個節點v,我們用dist[v] 來表示原點s到v的估算距離,用delta(s,v)表示s到v的最短距離
對邊進行松弛操作的過程如下:
void relax(int u, int v, int w) {
if ( dist[v]> dist[u] + w )
dist[v] = dist[u] + w ;
}
1.3 初始化
void init(int v0) {
for (int i = 1 ; i <= n ; i ++)
dist[i] = infinity ;
dist[v0] = 0 ;
}
2. 最短路徑的幾個性質
最短路徑具有最優子結構性質
路徑松弛性質
上界性質
收斂性質
最短路徑一定是簡單路徑
這幾條性質是后面最短路算法的數學依據,如果想要徹底理解最短路的算法,有必要對其深究
2.1 最短路徑的最優子結構性質
即最短路徑的子路徑也是最短路徑
簡單證明:如果最短路徑P的子路徑不是最短路徑,則可以找到一條更短的路徑使得P更短,這與P是最短路徑不符.
2.2 路徑松弛性質
如果P=<v0,v1, ..., vk>是從源點s=v0到vk的一條最短路徑,並且我們對P中的邊所進行松弛的次序為(v0,v1),(v1,v2), ..., (vk-1,vk),則dist[vk] = delta(s,vk),why?
我們可以用歸納法進行證明:
1.當v = s,時 dist[v] = delta(s,s) = 0,顯然成立
2.假設當v = vk-1時,dist[v] = delta(s,v) = delta(s,vk-1)成立,如果我們能推出dist[vk] = delta(s,vk),則命題成立。
因為
dist[vk] >= dist[vk-1] + w(vk-1,vk)
= delta(s,vk-1) + w(vk-1,vk)
= delta(s,vk) (收斂性質)
則原命題也是成立的.
2.3 上界性質
對於所有節點v, 有dist[v] >= delta(s,v),且當dist[v]=delta(s,v)之后,dist[v]的值不會再變化.
此性質亦可用歸納法證明:
1.進行init(v0)后,顯然滿足對所有v, dist[v] >= delta(s,v)
2.假設松弛前,對所有v, dist[v] >= delta(s,v),我們只要再證明松弛后,該性質也成立,則原命題即能成立
2.4 收斂性質
假設s-->u-->v為s-->v的一條最短路徑,在某個時刻dist[u] = delta(s,u),則在邊(u,v)松弛之后有dist[v] = delta(s,v)
證明:在邊松弛之后應該有
dist[v] <= dist[u] + w(u,v)
= delta(s,u) + w(u,v)
= delta(s,v) (最短路最優子結構性質)
而根據上界性質,dist[v] >= delta(s,v),因此dist[v] = delta(s,v)
2.4 最短路徑一定是簡單路徑
why? 為何最短路徑一定是簡單路徑(即無環)?
理由如下:假設最短路徑有環,則有以下三種情況:
1.正環,然后去掉正環會使最短路徑更短,不符合
2.零環,零環對最短路徑沒有影響,可以去掉
3.負環,如果存在負環,則不會有最短路徑,因為我循環一圈都可以是u到v的路徑更短.
3. Bellman-Ford算法
3.1 算法思路
Bellman-Ford算法通過不斷構建以源點為根的最短路徑樹,不斷擴展.
簡單來說,先找出,擴展一條邊,源點到達其他節點的最短路徑,再找出擴展兩條邊,源點到達其他節點的最短路徑,以此類推,找到通過擴展n-1條邊,源點到達其他節點的最短路徑,算法完畢。因為我們知道最短路徑是簡單路徑,因此它無環,所以它最多有n-1條邊.算法思路類似bfs.其正確性可以用路徑松弛性質證明.
示意圖如下(圖片來自算法導論):
先是從s擴展一條邊,可以達到t,y(圖b),此時我們知道,在只擴展一條邊的前提下,s到t,y的最短路徑分別是dist[t],dist[y],然后再通過t,y繼續擴展邊(圖c),這時我們有,在只擴展兩條邊的前提下,s到x,z的最短路徑分別是dist[x],dist[z]。當然有個前提假設是s到其他節點都有最短路徑.看看整個過程有沒有覺得特別像通過bfs找最短路的過程.
3.2 算法偽代碼
bool Bellman_Ford(int v0) {
init(v0) ;
for(int i=1 ; i<=n-1 ; i++)
for(int j=1 ; j<=m ; j++)
relax(edge[j].u,edge[j].v,edge[j].w);
for(int i=1 ; i<=m ; i++)
{
int u = edge[i].u ;
int v = edge[i].v ;
int w = edge[i].w ;
if (dist[v] > dist[u] + w)
return false ;
}
return false ;
}
4. SPFA算法
4.1 算法思想
我們可以輕松知道Bellman-Ford算法的時間復雜度是O(nm)的,我們再想想,是不是每次都需要對M條邊進行松弛操作的,顯然沒必要,而且我們發現如果在某次循環中,發現對M條不管怎么relax,dist數組都不會變化,那么算法就可以停止了(上界性質).SPFA算法就是在這2點上對Bellman-Ford算法進行優化的。SPFA算法時間復雜度為O(qm),其中q遠遠小於n.在實戰中有不錯的效率,不過可以造出讓SPFA效率低的數據。因此在如果題目沒有負邊,個人傾向於基於優先隊列實現的Dijkstra算法.
4.2 算法偽代碼
void SPFA(int v0) {
queue<int> q ;
init(v0) ;
q.push(v0) ;
inq[v0] = true ;
while (!q.empty())
{
int u = q.front(); q.pop();
inq[u] = false ;
for (int i=1 ; i<=n ; i++) //此處若用鄰接表則更快
if (g[u][i] !=0 && dist[i] > dist[u]+g[u][i])
{
dist[i] = dist[u]+g[u][i] ;
if (inq[i] == false)
{
q.push(i);
inq[i] = true ;
}
}
}
}
5. Dijkstra算法
5.1 算法思路
維護集合U,用來保存那些已經計算過最短路的節點即dist[v] = delta(s,v)的節點,不斷增大集合U,直到U包含圖中所有節點.
1.選出V-U集合中最小的dist[x]
2.把x加入到U中
3.用dist[x]去更新所有V-U集合的所有節點dist值,(U集合中的dist值已經達到下限delta(s,v)了,沒必要再更新)
5.2 算法偽代碼
void Dijkstra(int v0) {
init(v0) ;
for (int i=1 ; i<=n-1 ; i++)
{
min = infinity ;
for (int j=1 ; j<=n ; j++)
if (!inq[j] && dist[j]<min)
{
min = dist[j] ;
k = j ;
}
inq[k] = true ;
for(int j = 1 ; j<=n ; j++)
if (!inq[j] && dist[j]>dist[k]+g[k][j])
dist[j]>dist[k]+g[k][j] ;
}
}
5.3 算法理解
算法執行過程
1.首先從V-U集合中選出dist值最小的節點s(圖a),然后拿它去更新其他節點(圖b)
2.把節點s加入到U集合中
3.不斷重復此過程,直到U=V位置(圖c,d,e,f)
要證明算法的正確性,我們只需要證明當節點u加入到集合U時,有dist[u]=delta(s,u)即可.
用反證法:假設u是第一個加入到集合U時,滿足dist[u] != delta(s,u)的節點.
由於節點s是第一個加入到集合U的,並且有dist[s] = delta(s,s)=0,因此節點u與節點s必然不同,因此一定存在某條從節點s到節點u的路徑,否則dist[u] = infinity,是不會被選入集合U中的.因為至少存在一條從s到u的路徑,因此也存在一條從s到u的最短路徑.
考慮這樣一條路徑s---->x-->y---->u,其中,s,x在集合U中,y,u在V-U集合中,
因為x在u加入之前就已經加入到U集合中了,因此有dist(x) = delta(s,x),而x是y前驅,對邊(x,y) relax后,有dist[y] = > delta(s,y)(收斂性質)
因為y是u的前驅,且邊權值非負,因此有dist[y] = delta(s,y) <=delta(s,u)<=dist[u].
另外,算法選擇u節點放進集合U的前提是u是所有V-U集合中dist值最小的,因此有dist[y] >= dist[u].
綜上,有dist[y] = dist[u] = delta(s,u).這與假設dist[u] != delta(s,u)不符.因此原命題是成立的.
6. floyd算法
6.1 算法引入
floyd算法用來求圖中任意點對之間的最短路徑.將floyd算法之前我們先來看看另外一個用O(V^4)來解決圖中任意點對之間最短路徑的算法。
通過前面的知識我們知道,任意兩個節點之間的最短路徑無非就兩種情況
1.i, j 直接相連
2.i, j 間接相連
若是直接相連,則delta(i,j) = g[i][j] ;
若間接相連,設其路徑為i------>k-->j(k為j前驅),則有
delta(i,j) = delta(i,k) + g[k][j] (收斂性質)
我們定義dist[m][i][j] 表示最多可以通過m條邊,節點i,j之間的最短距離,則有dist[m][i][j] = min(dist[m-1][i][j],dist[m-1][i][k] + g[k][j]) = min(dist[m-1][i][k] + g[k][j])
,其中1<=k<=V,dist[0][i][j] = g[i][j]
.
偽代碼如下
void Shortest_Path_Pair() {
for (int u = 1 ; u<=n-1 ; u++) //因為最短路最多只有n-1條邊
for (int i=1 ; i<=n ; i++)
for (int j = 1 ; j<=n ;j++ )
for (int k=1 ; k<=n ; k++)
//當然此處dist[u][i][j]可以降維成dist[i][j].
dist[u][i][j] = min(dist[u-1][i][j],dist[u-1][i][k]+g[k][j]) ;
}
6.2 算法思路
OK,熱身結束!我們來看看floyd算法,上面算法類似於Bellman_Ford,他們都是從路徑中邊的性質來考慮的。現在我們換個思路,從中間節點開始考慮,考慮節點i,j間最短路徑p與中間節點均取自{1,2,...,k-1}的關系.
如果節點k不是路徑p上的中間節點,則節點i,j路徑上的中間節點均取自{1,2,...,k-1}的最短路徑也是節點i,j路徑上的中間節點均取自{1,2,...,k}的最短路徑
如果節點k是路徑p上的中間節點,則我們可以把路徑分解成i--->k--->j,對於路徑i-->k,其路徑上的節點也肯定全部來自{1,2,...,k-1},因為k不是路徑i-->k的中間節點,路徑k-->j同理,看下圖:
因此我們可以定義d[k][i][j] 表示i,j之間節點編號最大為k時的最短距離,我們有
d[k][i][j] = min(d[k-1][i][j], d[k-1][i][k]+d[k-1][k][j])
當然d[k][i][j]也是可以降維成d[i][j].其中1<=k<=V,d[0][i][j] = g[i][j].
6.3 偽代碼
void Floyd() {
for (int k = 1 ; i<=n ; k++)
for (int i = 1; i<=n ; i++)
for (int j = 1 ; j<=n ; j++)
if (d[i][j] > d[j][k] + d[k][j])
d[i][j] = d[j][k] + d[k][j] ;
}
后記
本文對求最短路徑的幾種基本算法做了簡單的講解,力求做到簡單,易懂,更嚴格的算法正確性的證明可以查閱參考書目,文中有任何錯誤之處請指出,我會盡快改正.另外有何問題,歡迎留言討論.
參考書目
《算法導論》 第三版