图论3之图的最短路径算法
上一篇我們討論了圖的遍歷,實際問題中圖的深度遍歷是我們更常用的,除了圖的遍歷,我們一般遇到的問題更多是關于圖的路徑的問題。本篇將介紹圖的四種常用遍歷算法
一、深度或廣度優先搜索算法(解決單源最短路徑)
從起始結點開始訪問所有的深度遍歷路徑或廣度優先路徑,則到達終點結點的路徑有多條,取其中路徑權值最短的一條則為最短路徑。
/***先輸入n個結點,m條邊,之后輸入有向圖的m條邊,邊的前兩元素表示起始結點,第三個值表權值,輸出1號城市到n號城市的最短距離***/ /***算法的思路是訪問所有的深度遍歷路徑,需要在深度遍歷返回時將訪問標志置0***/ #include <iostream> #include <iomanip> #define nmax 110 #define inf 999999999 using namespace std; int n, m, minPath, edge[nmax][nmax], mark[nmax];//結點數,邊數,最小路徑,鄰接矩陣,結點訪問標記 void dfs(int cur, int dst){ /***operation***/ /***operation***/ if(minPath < dst) return;//當前走過路徑大于之前最短路徑,沒必要再走下去 if(cur == n){//臨界條件 if(minPath > dst) minPath = dst; return; } else{ int i; for(i = 1; i <= n; i++){ if(edge[cur][i] != inf && edge[cur][i] != 0 && mark[i] == 0){ mark[i] = 1; dfs(i, dst+edge[cur][i]); mark[i] = 0; } } return; } } int main(){ while(cin >> n >> m && n != 0){ //初始化鄰接矩陣 int i, j; for(i = 1; i <= n; i++){ for(j = 1; j <= n; j++){ edge[i][j] = inf; } edge[i][i] = 0; } int a, b; while(m--){ cin >> a >> b; cin >> edge[a][b]; } //以dnf(1)為起點開始遞歸遍歷 memset(mark, 0, sizeof(mark)); minPath = inf; mark[1] = 1; dfs(1, 0); cout << minPath << endl; } return 0; }?
二、Dijkstra算法(解決單源最短路徑)
迪杰斯特拉算法是由荷蘭計算機科學家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。是從一個頂點到其余各頂點的最短路徑算法,解決的是有向圖中最短路徑問題。Dijkstra算法不能處理包含負邊(權重為負)的圖。
基本思想:每次找到離源點(如1號結點)最近的一個頂點,然后以該頂點為中心進行擴展,最終得到源點到其余所有點的最短路徑的一種貪婪算法。
基本步驟:1,設置標記數組book[]:將所有的頂點分為兩部分,已知最短路徑的頂點集合P和未知最短路徑的頂點集合Q,很顯然最開始集合P只有源點一個頂點。book[i]為1表示在集合P中;
2,設置最短路徑數組dst[]并不斷更新:初始狀態下,令dst[i] = edge[s][i](s為源點,edge為鄰接矩陣),很顯然此時dst[s]=0,book[s]=1。此時,在集合Q中可選擇一個離源點s最近的頂點u加入到P中。并依據以u為新的中心點,對每一條邊進行松弛操作(松弛是指由結點s-->j的途中可以經過點u,并令dst[j]=min{dst[j], dst[u]+edge[u][j]}),并令book[u]=1;
3,在集合Q中再次選擇一個離源點s最近的頂點v加入到P中。并依據v為新的中心點,對每一條邊進行松弛操作(即dst[j]=min{dst[j], dst[v]+edge[v][j]}),并令book[v]=1;
4,重復3,直至集合Q為空。
用Dijkstra算法找出以A為起點的單源最短路徑步驟如下
如果是稠密圖(邊比點多),則直接掃描所有未收錄頂點比較好,即第一種方法,每次O(V),總體算法復雜度T=O(V^2+E)
如果是稀疏圖(邊比點少),則使用優先隊列(最小堆)比較好,即第二種方法,每次O(logV),插入更新后的dist,O(logV)。總體算法復雜度T=O(VlogV+ElogE)
當然還有更加優秀的斐波那契堆,時間復雜度為?O(e+vlogv)
無權值(或者權值相等)的單源點最短路徑問題,Dijkstra算法退化成BFS廣度優先搜索。
那么,為什么BFS會比Dijkstra在這類問題上表現得更加好呢?
1. BFS使用FIFO的隊列來代替Dijkstra中的優先隊列(或者heap之類的)。
2. BFS不需要在每次選取最小結點時判斷其他結點是否有更優的路徑。
BFS的時間復雜度為O(v+e)
?
三、Floyd算法(解決多源最短路徑)
Dij算法只能得出源點到其余點的最短路徑,有一些是我們不需要的,如何得到任意兩點的最短路徑?
Floyd算法是一個經典的動態規劃算法。
算法思想:如果說兩個點之間的直接路徑不是最短路徑的話,必然有一個或者多個點供中轉,使其路徑最短。
?從任意節點i到任意節點j的最短路徑不外乎2種可能,1是直接從i到j,2是從i經過若干個節點k到j。所以,我們假設Dis(i,j)為節點u到節點v的最短路徑的距離,對于每一個節點k,我們檢查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,證明從i到k再到j的路徑比i直接到j的路徑短,我們便設置Dis(i,j) = Dis(i,k) + Dis(k,j),這樣一來,當我們遍歷完所有節點k,Dis(i,j)中記錄的便是i到j的最短路徑的距離。
其實核心思想是三角不等式:
如果我要求頂點A到頂點B之間的距離的話,我可以先找一個頂點C,求解頂點A到頂點C加上頂點C到頂點B的距離和,如果這個距離和小于頂點A直接到頂點B的距離的話,那么這個時候就要更新一下距離矩陣中的值,將頂點A到頂點B的距離更新為:頂點A到頂點C加上頂點C到頂點B的距離和。這就是Folyd的核心思想了。
算法描述:
a.從任意一條單邊路徑開始。所有兩點之間的距離是邊的權,如果兩點之間沒有邊相連,則權為無窮大。
b.對于每一對頂點 u 和 v,看看是否存在一個頂點 w 使得從 u 到 w 再到 v 比己知的路徑更短。如果是更新它。
那么如果要找到全局最優的解就要在選取中間頂點的過程中遍歷所有的節點才行,這樣就是三層for循環的結構了,得到的時間復雜度就是O(n*n*n),空間復雜度O(n^2),n為頂點的規模,其實在具體實際的應用中,這個算法的性能還是很不錯,可以正確處理有向圖或負權的最短路徑問題,同時也被用于計算有向圖的傳遞閉包。
用一個數組來存儲任意兩個點之間的距離,注意,這里可以是有向的,也可以是無向的。?
?
當任意兩點之間不允許經過第三個點時,這些城市之間最短路程就是初始路程。
分析如下:1,首先構建鄰接矩陣Floyd[n+1][n+1],假如現在只允許經過1號結點,求任意兩點間的最短路程,很顯然Floyd[i][j] = min{Floyd[i][j], Floyd[i][1]+Floyd[1][j]},代碼如下:
for(i = 1; i <= n; i++){for(j = 1; j <= n; j++){if(Floyd[i][j] > Floyd[i][1] + Floyd[1][j])Floyd[i][j] = Floyd[i][1] + Floyd[1][j];} }只需判斷Floyd[i][1]+Floyd[1][j]是否比Floyd[i][j]要小即可。Floyd[i][j]表示的是從i號頂點到j號頂點之間的路程。Floyd[i][1]+Floyd[1][j]表示的是從i號頂點先到1號頂點,再從1號頂點到j號頂點的路程之和。其中i是1~n循環,j也是1~n循環
在只允許經過1號頂點的情況下,任意兩點之間的最短路程更新為:?
?
通過上圖我們發現:在只通過1號頂點中轉的情況下,3號頂點到2號頂點(Floyd[3][2])、4號頂點到2號頂點(Floyd[4][2])以及4號頂點到3號頂點(Floyd[4][3])的路程都變短了。
2,接下來繼續求在只允許經過1和2號兩個頂點的情況下任意兩點之間的最短距離,在已經實現了從i號頂點到j號頂點只經過前1號點的最短路程的前提下,現在再插入第2號結點,來看看能不能更新更短路徑,我們需要在只允許經過1號頂點時任意兩點的最短路程的結果下,再判斷如果經過2號頂點是否可以使得i號頂點到j號頂點之間的路程變得更短。即判斷Floyd[i][2]+Floyd[2][j]是否比Floyd[i][j]要小。在只允許經過1和2號頂點的情況下,任意兩點之間的最短路程更新為:?
通過上圖得知,在相比只允許通過1號頂點進行中轉的情況下,這里允許通過1和2號頂點進行中轉,使得Floyd[1][3]和Floyd[4][3]的路程變得更短了。
3,很顯然,需要n次這樣的更新,表示依次插入了1號,2號......n號結點,最后求得的Floyd[n+1][n+1]是從i號頂點到j號頂點只經過前n號點的最短路程。
故核心代碼如下:
#define inf 99999999 for(k = 1; k <= n; k++){for(i = 1; i <= n; i++){for(j = 1; j <= n; j++){if(Floyd[i][k] < inf && Floyd[k][j] < inf && Floyd[i][j] > Floyd[i][k] + Floyd[k][j])Floyd[i][j] = Floyd[i][k] + Floyd[k][j];}} } #!/usr/bin/env python # -*- coding: utf-8 -*- """ # 算法思想: # 每個頂點都有可能使得兩個頂點之間的距離變短 # 當兩點之間不允許有第三個點時,這些城市之間的最短路徑就是初始路徑 """ # 城市地圖(字典的字典) # 字典的第1個鍵為起點城市,第2個鍵為目標城市其鍵值為兩個城市間的直接距離 # 將不相連點設為INF,方便更新兩點之間的最小值 INF = 99999 G = {1: {1: 0, 2: 2, 3: 6, 4: 4},2: {1: INF, 2: 0, 3: 3, 4: INF},3: {1: 7, 2: INF, 3: 0, 4: 1},4: {1: 5, 2: INF, 3: 12, 4: 0}}# Floyd-Warshall算法核心語句 # 分別在只允許經過某個點k的情況下,更新點和點之間的最短路徑 for k in G.keys(): # 不斷試圖往兩點i,j之間添加新的點k,更新最短距離for i in G.keys():for j in G[i].keys():if G[i][j] > G[i][k] + G[k][j]:G[i][j] = G[i][k] + G[k][j]if __name__ == '__main__':for i in G.keys():print(G[i].values())四、Bellman-Ford算法(解決負權邊,解決單源最短路徑)
上面這種思路不可行,下圖即為證。Dij無法解決負權圖。如果兩個頂點之間的距離為正數,那這個距離成為正權。反之,如果一個頂點到一個頂點的距離為負數,那這個距離就稱為負權。Bellman-Ford算法的時間復雜度是O(MN),但是我們依然可以對這個算法進行優化,在實際使用中,我們常常會發現不用循環到N-1次就能求出最短路徑,所以我們可以比較前后兩次松弛結果,若果兩次結果都一致,可說明松弛完成,不用再繼續循環了。
Bellman-Ford與Dijkstra的區別
Bellman-Ford和Dijkstra 相似,都是采用‘松弛’的方法來尋找最短的距離。
Bellman-Ford可以用于含有負權的圖中而Dijkstra不可以。
為什么Dijkstra不可以?其跟本的原因在于,在Dijkstra,一旦一個頂點用來松弛過以后,其最小值已經固定不會再參與到下一次的松弛中。因為Dijkstra中全部的距離都是正權,所以不可能出現A - B - C 之間的距離比 A - B - D - C 的距離短的情況,而Bellman-Ford則在每次循環中,則會將每個點都重新松弛一遍,所以可以處理負權。
主要思想:第一,初始化所有點。每一個點保存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。?
第二,進行循環,循環下標為從1到n-1(n等于圖中點的個數)。在循環內部,遍歷所有的邊,進行松弛計算。?
第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:?
d(v) > d (u) + w(u,v)?
則返回false,表示途中存在從源點可達的權為負的回路。
Bellman_Ford還可以檢測一個圖是否含有負權回路:如果在進行n-1輪松弛后仍然存在dst[e[i]] > dst[s[i]]+w[i]。
#!/usr/bin/env python # -*- coding: utf-8 -*- """ BellmanFord算法 """ G = {1: {1: 0, 2: -3, 5: 5},2: {2: 0, 3: 2},3: {3: 0, 4: 3},4: {4: 0, 5: 2},5: {5: 0}}def getEdges(G):""" 讀入圖G,返回其邊與端點的列表 """v1 = [] # 出發點v2 = [] # 對應的相鄰到達點w = [] # 頂點v1到頂點v2的邊的權值for i in G:for j in G[i]:if G[i][j] != 0:w.append(G[i][j])v1.append(i)v2.append(j)return v1, v2, wdef Bellman_Ford(G, v0, INF=999):v1, v2, w = getEdges(G)# 初始化源點與所有點之間的最短距離dis = dict((k, INF) for k in G.keys())dis[v0] = 0# 核心算法for k in range(len(G) - 1): # 循環 n-1輪check = 0 # 用于標記本輪松弛中dis是否發生更新for i in range(len(w)): # 對每條邊進行一次松弛操作if dis[v1[i]] + w[i] < dis[v2[i]]:dis[v2[i]] = dis[v1[i]] + w[i]check = 1if check == 0: break# 檢測負權回路# 如果在 n-1 次松弛之后,最短路徑依然發生變化,則該圖必然存在負權回路flag = 0for i in range(len(w)): # 對每條邊再嘗試進行一次松弛操作if dis[v1[i]] + w[i] < dis[v2[i]]:flag = 1breakif flag == 1:# raise CycleError()return Falsereturn disif __name__ == '__main__':v0 = 1dis = Bellman_Ford(G, v0)print('源點到圖中各點的最短距離(含負權點)', dis.values())?
五、SPSASPFA算法(Shortest Path Faster Algorithm單源最短路徑)
很多時候,給定的圖存在負權邊,這時類似Dijkstra等算法便沒有了用武之地,而Bellman-Ford算法的復雜度又過高,SPFA算法便派上用場了。有人稱spfa算法是最短路的萬能算法。
算法思想
設立一個隊列q用來保存待優化的結點,優化時每次取出隊首結點u,并且用u點當前的最短路徑估計值對離開u點所指向的結點v進行松弛操作,如果v點的最短路徑估計值有所調整,且v點不在當前的隊列中,就將v點放入隊尾。這樣不斷從隊列中取出結點來進行松弛操作,直至隊列空為止。?
松弛操作的原理是著名的定理:“三角形兩邊之和大于第三邊”,在信息學中我們叫它三角不等式。所謂對結點i,j進行松弛,就是判定是否dis[j]>dis[i]+w[i,j],如果該式成立則將dis[j]減小到dis[i]+w[i,j],否則不動。?
下面舉一個實例來說明SFFA算法是怎樣進行的:
和BFS的區別
SPFA在形式上和廣度優先搜索非常類似,不同的是BFS中一個點出了隊列就不可能重新進入隊列,但是SPFA中一個點可能在出隊列之后再次被放入隊列,也就是一個點改進過其它的點之后,過了一段時間可能本身被改進(重新入隊),于是再次用來改進其它的點,這樣反復迭代下去。
int main() {int n,m,s,t;//分別是節點數、邊的條數、起點、終點while(cin>>n>>m>>s>>t) {vector<vector<int>> edge(n+1,vector<int>(n+1,0));//鄰接矩陣vector<int> dis(n+1,INT_MAX);//從起點出發的最短路徑vector<int> book(n+1,0);//某結點在隊列中queue<int> q;for(int i=1;i<=n;i++)//初始化鄰接矩陣for(int j=1;j<=n;j++)if(i!=j) edge[i][j]=INT_MAX;int u,v,length;for(int i=0;i<m;i++) {//讀入每條邊,完善鄰接矩陣cin>>u>>v>>length;if(length<edge[u][v]) {//如果當前的邊長比已有的短,則更新鄰接矩陣edge[u][v]=length;edge[v][u]=length;}}dis[s]=0;book[s]=1;//把起點先放入隊列q.push(s);int top;while(!q.empty()) {//如果隊列非空top=q.front();//取出隊首元素q.pop();book[top]=0;//釋放隊首結點,因為這節點可能下次用來松弛其它節點,重新入隊for(int i=1;i<=n;i++) {if(edge[top][i]!=INT_MAX && dis[i]>dis[top]+edge[top][i]) {dis[i]= dis[top]+edge[top][i]; //更新最短路徑if(book[i]==0) { //如果擴展結點i不在隊列中,入隊book[i]=1;q.push(i);}}}}cout<<dis[t]<<endl;}return 0; }?
以上算法的比較
適用情況
dj和ford算法用于解決單源最短路徑,而floyd算法解決多源最短路徑。
dj適用稠密圖(鄰接矩陣),因為稠密圖問題與頂點關系密切;?
ford算法適用稀疏圖(鄰接表),因為稀疏圖問題與邊關系密切。?
floyd在稠密圖(鄰接矩陣)和稀疏圖(鄰接表)中都可以使用;
PS:dj算法雖然一般用鄰接矩陣實現,但也可以用鄰接表實現,只不過比較繁瑣。而ford算法只能用鄰接表實現。
dj算法不能解決含有負權邊的圖;?
而Floyd算法和Ford算法可以解決含有負權邊的問題,但都要求沒有總和小于0的負權環路。
SPFA算法可以解決負權邊的問題,而且復雜度比Ford低。形式上,它可以看做是dj算法和BFS算法的結合。
以上算法都是既能處理無向圖問題,也能處理有向圖問題。因為無向圖只是有向圖的特例,它們只是在鄰接矩陣或鄰接表的初始化時有所區別。
?
實際項目中用到的是帶啟發式的A*算法,將來再介紹。
?
總結
以上是生活随笔為你收集整理的图论3之图的最短路径算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决html5手机端滑动卡顿的现象
- 下一篇: 适用于Win和Mac的专业电脑数据恢复软