上一篇博文咱們提到了圖的最短路徑問題:兩個頂點間的最短路徑該如何尋找?其實這個問題不該該叫「最短」路徑問題,而應該叫「最便宜」路徑問題,由於有時候咱們會爲圖中的邊賦權(weight),也叫權重,至關於通過一條邊的「代價」,通常爲正數。好比下圖(邊旁的數字即該邊的權重)node
若是單純考慮一條路徑上邊的條數,那麼從v0到v6的最短路徑應該是:v0-v3-v6。可是若是考慮邊的權重,從v0到v6的「最便宜」路徑應該是:v0-v1-v4-v6,其總權重爲3(路徑中全部邊的權重之和),而若是走v0-v3-v6的路徑,總權重將是11。算法
邊有權重的圖咱們稱之爲賦權圖,反之稱爲無權圖,賦權圖顯然能夠比無權圖應用於更多場合,好比用賦權圖來表示城市間公路,權重越大路況越差,或者權重越大,過路費用越高等等。數組
其實不考慮權重的最短路徑問題就是全部邊的權重都是1的「最便宜」路徑問題,好比將上圖的全部邊去掉權重後的無權圖也能夠這樣表示:spa
方便起見,咱們就將「最便宜」路徑稱爲最短路徑。3d
接下來讓咱們先從簡單的無權狀況開始,看看如何找兩個頂點間的最短路徑。不過到了這一步,一件有意思的事情須要說明一下,那就是:找X到Y的最短路徑,比找X到全部頂點的最短路徑更慢(有權無權都是如此)。code
出現這個狀況的緣由咱們能夠簡單的分析一波:找X到Y的最短路徑,最直接的作法就是令程序從X出發沿着可行的邊不斷的走,直到走到Y處爲止,可是當走到Y處時,沒人能保證剛剛走的那條路就是最短的,除非你走遍了整個圖的頂點,換句話說,你要肯定走到Y處且走的路徑是最短的,你就得走遍全部頂點,並且在這個過程當中你必須不斷記錄各個路徑的長度,否則當你發現到一個頂點有多條路徑時怎麼比較它們呢?因此,你要找X到Y的最短路徑,你就得找出X到全部頂點的最短路徑。blog
固然,也存在專門尋找點對點最短路徑的思路,可是目前來講,單獨找X到Y的最短路徑不會比找X到全部頂點的最短路徑更快,因此咱們接下來探討的問題其實都是:單源最短路徑問題。即給定一個起點(源),求出其與全部頂點的最短路徑。有了到全部頂點的最短路徑,咱們天然也就有了到給定頂點Y的最短路徑。排序
對無權圖進行單源最短路徑尋找的思路,就是咱們上面所說的「最直接的作法」。爲了更方便講解,咱們假定若存在邊(A,B),則B是被A「指向」的頂點。那麼對無權圖進行單源最短路徑尋找就是這樣的:隊列
首先,咱們將起點的路徑長設爲0,其餘頂點路徑長設爲負數(也能夠是其餘不可能的值,圖例中用?表示),下例以v1做爲起點class
接着咱們將起點所指向的頂點的路徑長設爲1,能夠確定的是,只有被路徑長爲0的起點所指向的頂點的路徑長爲1,本例中即v3和v4:
接下來,咱們將路徑長爲1的頂點(v3和v4)所指向的頂點的路徑長設爲2,一樣能夠確定,只有被路徑長爲1的頂點所指向的頂點的路徑長爲2。不過此時會遇到一個問題:v3是v4所指向的頂點,但v3的路徑長顯然不該該被設爲2。因此咱們須要對已知路徑長的頂點設一個「已知」標記,已知的頂點再也不更改其路徑長,具體作法在給出代碼時將寫明。本例中,路徑長要被設爲2的頂點是v二、v五、v6:
而後咱們繼續這樣的步驟,將路徑長爲2的頂點所指向的頂點的路徑長設爲3。不過本例中路徑長爲2的頂點所指向的頂點中已經沒有未知頂點了,因此算法結束。
上述步驟隨着圖的規模變大而變多,但不難發現其規律就是:將路徑長爲i的頂點所指向的未知頂點的路徑長設爲i+1,i從0開始,結束條件即:當前路徑長爲i的頂點沒有指向其它頂點,或所指向的頂點均爲已知。
須要注意的是結束條件的說法,咱們並無要求全部頂點都變爲已知,由於肯定某頂點爲起點後,是有可能存在某個頂點沒法由起點出發而後到達的,好比咱們的例子中的v0,不存在從v1到v0的路徑。
接下來要作的事情就是用代碼實現咱們所說的思路,此時咱們須要注意是咱們並不想在圖上直接動手腳,由於圖可能還有他用,而且直接在圖上動手腳也不方便,由於圖中頂點可能並無用於表示是否已知的域和用於表示從起點到自身的最短路徑長的域。
因此咱們的作法是將最短路徑的計算結果存於一個線性表中,其結構以下:
其中「一行」爲線性表中的一個元素,每一行的四個單元格就是一個元素中的四個域:頂點、是否已知、與起點最短路徑長、最短路徑中自身的前一個頂點。
那麼以前計算最短路徑的過程用這個表來表示的話,就是下面這樣:
當咱們想知道從起點到頂點Y的最短路徑時,咱們只須要找到Y頂點,查看其distance域便可知道,而想知道整條路徑是怎麼走的,咱們也只要追溯Y的preV直到起點便可知道。下面是輸出起點到給定終點的最短路徑的一個例子:
//路徑表中的元素定義,咱們假設頂點vx即數字x,因此元素沒有vertex域 struct pathNode { bool known; int distance; size_t preV; } //路徑表 struct pathNode pathTable[numVertex];
void printPath(size_t end,struct node* pathTable) { size_t preV=pathTable[end].preV; if(pathTable[preV].distance!=0) printPath(preV,pathTable); else printf("%d",preV); printf("->"); printf("%d",end); }
下面是上述無權最短路徑思路的一個簡單僞代碼實現:
//無權最短路徑計算,圖存於鄰接表graph,結果存入pathTable,起點即start void unweightedPath(Node* graph,struct pathNode* pathTable,size_t start) { pathTable[start].known=true; pathTable[start].distance=0; //若pathTable[x].distance爲0,則其preV是無用的,咱們不予理睬 //初始化pathTable中的其餘元素 //curDis即當前距離,咱們要作的是令distance==curDis的頂點所指的未知頂點的distance=curDis+1 for(int curDis=0;curDis<numVertex;++curDis) { for(int i=0;i<numVertex;++i) { if(!pathTable[i].known&&pathTable[i].distance==curDis) { pathTable[i].known=true; //遍歷pathTable[i]所指向的頂點X { if(!pathTable[X].known) { pathTable[X].preV=i; pathTable[X].distance=curDis+1; } } } } } }
與上一篇博文的拓撲排序同樣,上面的最短路徑算法還有改進空間。當咱們尋找符合distance==curDis條件的頂點時,咱們用的是直接遍歷數組的方法,這使得咱們的算法時間複雜度達到了O(nv2)(nv爲頂點個數),因此咱們要改進的就是「尋找符合條件的頂點」的過程。咱們能夠建立一個隊列來存儲「須要處理的頂點」,該隊列初始時只有起點,當咱們修改了某個未知頂點的distance後,咱們就將該頂點入隊,而當咱們令curDis遞增後再次尋找distance==curDis的頂點時,咱們只須要令隊列元素出隊便可獲取到想要的頂點。這個過程口述難以表達清楚,下面是應該足夠清晰了的僞代碼:
//無權最短路徑計算,圖存於鄰接表graph,結果存入pathTable,起點即start void unweightedPath(Node* graph,struct pathNode* pathTable,size_t start) { //初始化pathTable //建立隊列pendingQueue //將起點start入隊 size_t curVertex;while(!empty(pendingQueue)) { curVertex=Dequeue(pendingQueue); pathTable[curVertex].known=true; //遍歷curVertex指向的頂點X { if(!pathTable[X].known) { pathTable[X].distance=pathTable[curVertex].distance+1; pathTable[X].preV=curVertex; Enqueue(X,pendingQueue); } } } }
這樣一來,咱們就將無權最短路徑算法的時間複雜度由O(nv2)下降到了O(nv+ne)(ne即邊的條數)。此外,上述算法對於無向有圈圖也是同樣生效的,緣由就不贅述了,道理是簡單的。
接下來的問題是如何對有權圖進行單源最短路徑的尋找。有權圖的最短路徑顯然比無權圖要難找,緣由在於咱們不能套用無權算法的思路,直接令已知頂點所指未知頂點的distance=curDis+weight(weight即兩頂點間路徑的權重,此處簡寫),如下圖爲例:
若咱們令v0做爲起點,而後令v0所指的未知頂點的distance=v0.distance+weight,那麼v3的distance就會變成5,但是實際上v3的distance應改成2。
解決的思路是:咱們羅列出全部已知頂點指向的全部未知頂點,看這些未知頂點中誰的distance被修改後會是最小的,最小的那個咱們就修改其distance,並認爲它已知。
以上圖爲例,咱們一步步走一遍來加深一下理解:
首先是正常的初始化(咱們將邊的權重也標識出來),假設起點爲v0:
接着咱們羅列出全部已知頂點(只有v0)指向的全部未知頂點:v一、v二、v3。而後發現若修改它們的distance,則v1.distance=v0.distance+1=1,v2.distance=v0.distance+3=3,v3.distance=v0.distance+5=5。顯然v1被修改後的distance是未知頂點中最小的,因此咱們只修改v1的distance,並將v1設爲已知,v二、v3不動:
接着咱們繼續羅列出全部已知頂點(v0、v1)指向的全部未知頂點:v二、v三、v4。而後發現若修改它們的distance,則v2.distance=v0.distance+3=3,v4.distance=v1.distance+1=2,v3.distance=v1.distance+1=2(雖然v0也指向v3,可是經過v0到v3的路徑長大於從v1到v3,因此v3的distance取其小者),其中v3和v4的新distance並列最小,咱們任選其一好比v4,而後只修改v4的distance,並將v4設爲已知,其它不動:
繼續,咱們羅列出全部已知頂點(v0、v一、v4)指向的全部未知頂點:v二、v三、v6,發現若修改,則v2.distance=3,v3.distance=2,v6.distance=3,因此咱們只修改v3的distance,並將v3設爲已知:
繼續,咱們羅列出全部已知頂點(v0、v一、v三、v4)指向的全部未知頂點:v二、v五、v6,發現若修改,則v2.distance=3,v5.distance=10,v6.distance=3,咱們在v2和v6中任選一個如v2,只修改v2.distance,並將v2設爲已知:
繼續,咱們羅列出全部已知頂點指向的全部未知頂點:v五、v6,發現若修改,則v5.distance=5,v6.distance=3,因此咱們只修改v6:
最後,羅列出的未知頂點只有v5,若修改,其distance=5,咱們將其修改並設爲已知,算法結束:
其實上述算法的核心部分就是:
1.找到全部已知頂點
2.將全部已知頂點指向的全部未知頂點羅列出來
3.計算這些未知頂點的最小distance,而後再肯定其中新distance最小的頂點X
4.只修改X的distance,並將X設爲已知
5.回到第二步,若全部已知頂點都沒有指向未知頂點,則結束
而這個算法就是Dijkstra算法的雛形。
Dijkstra算法核心部分簡化的說法就是:找到全部可肯定distance的未知頂點中新distance最小的那個,修改它並將它設爲已知。
用僞代碼描述就是這樣:
//有權最短路徑計算,圖存於鄰接表graph,結果存入pathTable,起點即start void weightedPath(Node* graph,struct pathNode* pathTable,size_t start) { //初始化pathNode數組 size_t curV; while(true) { //找到可肯定distance的未知頂點中新distance最小的那個,存入curV,若沒有則跳出循環 //令pathNode[curV].distance和pathNode[curV].prev修改成正確的值 pathNode[curV].known=true; } }
能夠肯定的是,Dijkstra算法也能夠應用於無權圖,只要給無權圖中的每條邊加個值爲1的權重便可。而且若是你將無權算法與Dijkstra算法進行對比,就會發現那個無權算法其實就是Dijkstra算法的「特例」,在無權算法中,咱們之因此不須要去找「distance最小的未知頂點」,是由於咱們能夠確定已知頂點所指向的未知頂點就是「distance最小的未知頂點」。
不用想都知道,Dijkstra算法中的循環中的兩行僞代碼其實意味着大量的操做:找到能夠肯定distance的未知頂點,計算它們的distance,比較出最小的那個,修改……
顯然,Dijkstra算法的核心部分是能夠改進的,改進的思路與無權算法也很相像,即「加快尋找符合條件的頂點的過程」。其中一種改進方式是計算出未知頂點的新distance後,將{未知頂點,新distance}對插入到以distance爲關鍵字的優先隊列中,而不是直接拋棄非最小distance的那些未知頂點(這是一個很大的浪費)。這樣在下一次尋找「distance最小的未知頂點」時,咱們能夠經過優先隊列的出隊來得到,從而避免了遍歷整個數組來尋找目標的狀況。這個想法要細化實現的話,還有很多坑要避開,不過我寫到這兒時深感表達之困難與疲憊,因此這篇博文就此打住,若是博文中有什麼不對之處,能夠在評論區指出,謝謝~
附:若是有權圖中存在權重爲負值的狀況,則計算單源最短路徑將會更加困難,不過能夠肯定的是,若是有權圖中存在圈與負權邊,且負權邊在圈中,使得圈的路徑長爲負,那麼單源最短路徑的計算是沒法進行的,由於你能夠在這個圈中永遠走下去來使路徑長不斷「減少」。解決有負值權重邊的圖的最短路徑算法是在Dijkstra的算法上進行改進得來的,本文不予講解(或許之後會有一篇文章介紹),有興趣的能夠自行搜索。