基本概念
圖:
有頂點和邊組成。又分爲
有向圖:
在這裏只能從A到B,不能從B到A。
無向圖:
能從A到B,也能從B到A,也可以用下圖表示:
還有就是給邊加上權重,變成加權圖:
權重代表了兩個頂點連接的程度,它可以是時間、距離、路費等等,根據實際情況而定。
最短路徑:
如上圖,從A到D,有三種路徑:ABD、AD、ACD。
考慮到邊的權重(比如路費),三條線路中最短路徑不是兩點直連的AD(10),而是ABD(2+3=5)。
負環路:
雖然從現實場景中,人們很難想象邊的權重是負數——沒聽說過走高速從A到B,不用交高速費,還倒找錢的。
但是從理論上來說,還是要考慮負權邊的存在,這就導致一個問題:負環路的存在導致無法找出最短路徑。
看下圖:
從A到A,花費的是0,這是最短路徑了,但是因爲有了負權邊的存在,會造成:
A-B-C-A,權重爲2+2-5=-1,也就是說A繞了一圈,變成-1,比0小,最短路徑是ABCA了。
這還沒完,再繞一圈,-1+2+2-5=-2,A變成了-2,照此循環下去,A到A的權重會越來越小。
這就是負環路,永遠找不到最短路徑。
當然,有負權邊不代表一定有負環路,如下圖:
這就沒有形成負環路,A到C的最短路徑就是ABC=3
廣度搜索優先:
簡單地說,就是從根節點開始,搜索完其子節點後,再搜索子節點的子節點,直至找到目標節點或所有節點都被遍歷一遍。
如上圖,將根節點A的子節點BCD放入隊列,取出B,再將B的子節點EF放入隊列,接着取出C,再將C的子節點G放入隊列,按照隊列先進先出的特性,遍歷所有節點。
深度搜索優先:
從根節點開始,搜索完一條分支後,再搜索另一分支。
如上圖,取根節點A壓入棧。
取出A,獲取A的子節點BCD,壓入棧。
取出B,獲取B的子節點EF,壓入棧。
取出F,並無子節點,且不是目標節點,拋出。E同理。
取出C,獲取C的子節點G,壓入棧。
取出G,同F。
取出D,獲取D的子節點H,壓入棧。
取出H,同F。
鬆弛操作:
如上圖,算出A到D的最短路徑。
一開始我們只知道A到A的路徑是0,到BCD的路徑未知,就設爲∞。
計算A-D路徑爲0+10=10 <∞,故將D由∞改爲10。這就是一次鬆弛操作。
貝爾曼-福特算法
簡單地說,就是對圖中所有訂單、所有邊都進行鬆弛操作,直到找到最短路徑。所以其時間複雜度應該是O(頂點數*邊數)。
僞代碼應該是:
for(int i=0;i<頂點數-1;i++){
for(int j=0;j<邊數;j++){
鬆弛操作;
}
}
但在實際情況中,在小於“頂點數-1”次的遍歷中,已經求出了最短路徑,所以在內部循環結束後,校驗一下有沒有進行鬆弛操作,如果沒有,則說明已求出最短路徑,直接跳出即可。
僞代碼:
for(int i=0;i<頂點數-1;i++){
是否進行了鬆弛操作=false;
for(int j=0;j<邊數;j++){
if(終點權重>起點權重+邊權重){
鬆弛操作:終點權重=起點權重+邊權重;終點的起點設爲起點名稱;
是否進行了鬆弛操作=true;
}
}
if(沒有進行鬆弛操作){
break;
}
}
再結合之前談到的負環路,在執行完最多頂點數-1次循環後,理應得到最短路徑,如果我們額外再遍歷一次所有的邊,看看有沒有進行鬆弛操作。如果有,說明存在負環路。
添加僞代碼:
是否存在負環路=false;
for(int j=0;j<邊數;j++){
if(終點權重>起點權重+邊權重){
是否存在負環路=true;
break;
}
}
操作步驟:
如上圖,共有5個頂點:ABCDE。
16條邊(無向圖,每條線代表兩個邊):AB、AC、AD、BA、BC、BE、CA、CB、CD、CE、DA、DC、DE、EB、EC、ED
以A爲起點,計算到其他頂點的最短路徑。
初始狀態下,A的權重應爲0,其他節點皆爲∞。
1、處理AB,B的權重改爲1。此時A=0,B=1,其餘爲∞。B的起點爲A。
2、處理AC,C的權重改爲7。此時A=0,B=1,C=7。C的起點爲A。
3、處理AD,D的權重改爲6。此時A=0,B=1,C=7,D=6。D的起點爲A。
4、處理BA,BA=1+1=2>0,無須進行鬆弛操作。
5、處理BC,BC=1+1=2<7,C的權重改爲2,此時A=0,B=1,C=2,D=6。C的起點改爲B。
6、接着繼續處理,本次對所有邊的循環,得出如下結果:
A到各點最低消耗:A=0,B=1,C=2,D=6,E=4
各點的起點:B<--A,C<--B,D<--A,E<--C。
7、接着開啓下一輪對所有邊的循環,在此次循環中,沒有進行鬆弛操作,故跳出循環。
8、判斷沒有負環路,至此得出最終結果。