問題描述:給你一個頂點作源點,你想要知道,如何從源點到達其餘全部點的最短路徑。 php
OK,這個問題看起來沒什麼用。咱們通常想知道的是A點到B點的最短路徑,這個單源最短路徑問題告訴咱們A點到全部點的最短路徑,會不會計算過多了呢? html
有趣的是,解決A點到B點的最短路徑算法不會比單源最短路徑問題簡單,咱們所知的求A點到B點的最短路徑算法就是求A點到任何點的最短路徑。咱們除了這樣作,好像也沒什麼好辦法了。 java
基本原理: 算法
每次新擴展一個距離最短的點,更新與其相鄰的點的距離。當全部邊權都爲正時,因爲不會存在一個距離更短的沒擴展過的點,因此這個點的距離永遠不會再被改變,於是保證了算法的正確性。不過根據這個原理,用Dijkstra求最短路的圖不能有負權邊,由於擴展到負權邊的時候會產生更短的距離,有可能就破壞了已經更新的點距離不會改變的性質。 數組
適用條件與限制: ide
算法流程: 優化
在如下說明中,s爲源,w[u,v]爲點u和v之間的邊的長度,結果保存在dist[] 動畫
執行動畫過程以下圖: this
實例: spa
用Dijkstra算法找出以A爲起點的單源最短路徑步驟以下
代碼:
咱們在OJ上完成這個算法:http://acm.hdu.edu.cn/showproblem.php?pid=1874
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StreamTokenizer; public class Main { static int[][] v = new int[201][201]; static int[] dist = new int[201]; static boolean[] visit = new boolean[201]; public static void main(String[] args) throws IOException { StreamTokenizer in = new StreamTokenizer(new BufferedReader( new InputStreamReader(System.in))); int n, m, begin, end; while (in.nextToken() != StreamTokenizer.TT_EOF) { n = (int) in.nval; in.nextToken(); m = (int) in.nval; init(n); for (int i = 0; i < m; i++) { in.nextToken(); int a = (int) in.nval; in.nextToken(); int b = (int) in.nval; in.nextToken(); int d = (int) in.nval; if (d < v[a][b]) { v[a][b] = d; v[b][a] = d; } } in.nextToken(); begin = (int) in.nval; in.nextToken(); end = (int) in.nval; if (begin == end) { System.out.println("0"); continue; } dist[begin] = 0; visit[begin] = true; dijkstra(begin, n); if (!visit[end]) { System.out.println("-1"); } else { System.out.println(dist[end]); } } } public static void init(int n) { for (int i = 0; i < n; i++) { visit[i] = false; for (int j = 0; j < n; j++) { v[i][j] = 10000; } } } public static void dijkstra(int s, int n) { for (int i = 0; i < n; i++) { dist[i] = v[s][i]; } for (int i = 0; i < n - 1; i++) { int min = 10000; int index = 0; for (int j = 0; j < n; j++) { if (!visit[j] && dist[j] < min) { min = dist[j]; index = j; } } visit[index] = true; for (int j = 0; j < n; j++) { if (!visit[j] && dist[index] + v[index][j] < dist[j]) { dist[j] = dist[index] + v[index][j]; } } } } }能夠看出時間複雜度爲 O(v^2)。有沒有更快的方法呢?
因爲每次都要找到未訪問的點中距離最小的點,咱們使用優先隊列來解決這個問題,關於優先隊列請查看這篇Blog。如下是利用優先隊列實現的算法
public static void dijkstrapq(int s, int n) { class Item implements Comparable<Item> { public int idx; public int weight; public Item(int idx, int weight) { this.idx = idx; this.weight = weight; } @Override public int compareTo(Item item) { if (this.weight == item.weight) { return 0; } else if (this.weight < item.weight) { return -1; } else { return 1; } } } PriorityQueue<Item> pq = new PriorityQueue<Item>(); for (int i = 0; i < n; i++) { dist[i] = v[s][i]; if (i != s) { pq.offer(new Item(i, dist[i])); } } Item itm = null; while (!pq.isEmpty()) { itm = pq.poll(); int index = itm.idx; int weight = itm.weight; if (weight == 10000) { break; } visit[index] = true; for (int j = 0; j < n; j++) { if (!visit[j] && dist[index] + v[index][j] < dist[j]) { dist[j] = dist[index] + v[index][j]; pq.offer(new Item(j, dist[j])); } } } }若是是稠密圖(邊比點多),則直接掃描全部未收錄頂點比較好,即第一種方法,每次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)
Dijkstra很優秀,可是使用Dijkstra有一個最大的限制,就是不能有負權邊。而Bellman-Ford適用於權值能夠爲負、無權值爲負的迴路的圖。這比Dijkstra算法的使用範圍要廣。其基本思想爲:首先假設源點到全部點的距離爲無窮大,而後從任一頂點u出發,遍歷其它全部頂點vi,計算從源點到其它頂點vi的距離與從vi到u的距離的和,若是比原來距離小,則更新,遍歷完全部的頂點爲止,便可求得源點到全部頂點的最短距離。
Bellman-Ford算法能夠大體分爲三個部分
對有向帶權圖G = (V, E),從頂點s起始,利用Bellman-Ford算法求解各頂點最短距離,算法描述以下:
for(int k=1;k<=n-1;k++)//遍歷點的次數 { for(int i=1;i<=m;i++)//遍歷邊的次數 { if(dis[v[i]]>dis[u[i]]+w[i])//若是從u到v的距離可以經過w這條邊壓縮路徑 就要進行鬆弛操做 { dis[v[i]]=dis[u[i]]+w[i]; } } }
很明顯Bellman-Ford算法複雜度爲O(VE),比Dijkstra要慢,可是解決了負權值問題。
圖解:
固然不用老是鬆弛E次,可能遠小於E次時,全部邊都不能鬆弛了。因此加個check來優化,若是每一個邊都沒鬆弛,就break。
for(int k=1;k<=n-1;k++) { check=0;//用check檢查是否進行下一輪次的操做 for(int i=1;i<=m;i++) { if(dis[v[i]]>dis[u[i]]+w[i]) { dis[v[i]]=dis[u[i]]+w[i]; check=1; } } if(check==0)break; }
咱們一直說的是有向圖的鬆弛,若是是無向圖則要鬆弛兩次(由於A到B有邊,那麼B到A也有邊)
for(int k=1;k<=n-1;k++) { check=0; for(int i=1;i<=m;i++) { if(dis[v[i]]>dis[u[i]]+w[i]) { dis[v[i]]=dis[u[i]]+w[i]; check=1; } if(dis[u[i]]>dis[v[i]]+w[i]) { dis[u[i]]=dis[v[i]]+w[i]; check=1; } } if(check==0)break; }
代碼:
咱們在OJ上驗證這個算法:http://acm.hdu.edu.cn/showproblem.php?pid=2544
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StreamTokenizer; public class Main { static int[] begin = new int[121212]; static int[] end = new int[121212]; static int[] wight = new int[121212]; static int[] dist = new int[121212]; public static void main(String[] args) throws IOException { StreamTokenizer in = new StreamTokenizer(new BufferedReader( new InputStreamReader(System.in))); int n, m; while (in.nextToken() != StreamTokenizer.TT_EOF) { n = (int) in.nval; in.nextToken(); m = (int) in.nval; init(n); if (n == 0 || m == 0) { break; } for (int i = 1; i <= m; i++) { in.nextToken(); int a = (int) in.nval; in.nextToken(); int b = (int) in.nval; in.nextToken(); int d = (int) in.nval; begin[i] = a; end[i] = b; wight[i] = d; if (a == 1) { dist[b] = d; } if (b == 1) { dist[a] = d; } } bellmanFord(n, m); System.out.println(dist[n]); } } private static boolean bellmanFord(int n, int m) { dist[1] = 0; int check; for (int i = 1; i <= n - 1; i++) { check = 0; for (int j = 1; j <= m; j++) { int b = begin[j]; int e = end[j]; if (dist[b] + wight[j] < dist[e]) { check = 1; dist[e] = wight[j] + dist[b]; } if (dist[e] + wight[j] < dist[b]) { check = 1; dist[b] = wight[j] + dist[e]; } } if (check == 0) { break; } } return true; } public static void init(int n) { for (int i = 1; i <= n; i++) { dist[i] = 9999999; } } }OJ上的這個題目沒有負值環,Bellman-Ford是能夠檢查負值環的,就如上面所說,最後再遍歷一遍邊,若是還能鬆弛,說明有負值環。
for (int j = 1; j <= m; j++) { int b = begin[j]; int e = end[j]; if (dist[b] + wight[j] < dist[e]) { return false; } if (dist[e] + wight[j] < dist[b]) { return false; } }
SPFA算法維護一個隊列,裏面存放全部須要進行迭代的點。初始時隊列中只有一個點S。用一個布爾數組記錄每一個點是否處在隊列中。
SPFA算法能夠分爲大體3步
SPFA 在形式上和寬度優先搜索很是相似,不一樣的是寬度優先搜索中一個點出了隊列就不可能從新進入隊列,可是SPFA中一個點可能在出隊列以後再次被放入隊列,也就是一個點改進過其它的點以後,過了一段時間可能自己被改進,因而再次用來改進其它的點,這樣反覆迭代下去。
代碼:
咱們在OJ上驗證這個算法:http://acm.hdu.edu.cn/showproblem.php?pid=2544
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StreamTokenizer; import java.util.LinkedList; import java.util.Queue; public class Main { static int[][] v = new int[101][101]; static int[] dist = new int[101]; static int[] inside = new int[101]; static Queue<Integer> queue; public static void main(String[] args) throws IOException { StreamTokenizer in = new StreamTokenizer(new BufferedReader( new InputStreamReader(System.in))); int n, m; while (in.nextToken() != StreamTokenizer.TT_EOF) { n = (int) in.nval; in.nextToken(); m = (int) in.nval; init(n); if (n == 0 || m == 0) { break; } for (int i = 1; i <= m; i++) { in.nextToken(); int a = (int) in.nval; in.nextToken(); int b = (int) in.nval; in.nextToken(); int d = (int) in.nval; v[a][b] = d; v[b][a] = d; } SPFA(n); System.out.println(dist[n]); } } private static void SPFA(int n) { queue.add(1); inside[1] = 1; dist[1] = 0; while (!queue.isEmpty()) { int top = queue.poll(); inside[top] = 0; for (int i = 1; i <= n; i++) { if (v[top][i] < 9999999) { if (dist[top] + v[top][i] < dist[i]) { dist[i] = dist[top] + v[top][i]; if (inside[i] == 0) { queue.add(i); inside[i] = 1; } } } } } } public static void init(int n) { for (int i = 1; i <= n; i++) { dist[i] = 9999999; inside[i] = 0; for (int j = 1; j <= n; j++) { v[i][j] = 9999999; } } queue = new LinkedList<Integer>(); } }因爲上述代碼使用鄰接矩陣來存儲,因此在遍歷與某點相連的邊時,複雜度較高。若是將其改爲鄰接表來實現會更加明顯。
在平均狀況下,SPFA算法的指望時間複雜度爲O(E)。可是這一說法有爭議,在這裏就不討論了,總之SPFA是一種Bellman-Ford算法的優化。
那麼多源最短路徑問題該怎麼解決呢?
很明顯有一種方法就是,將單源最短路徑問題使用N次,那麼使用普通的Dijkstra算法的時間複雜度爲T=O(V^3+V*E),對於稀疏圖的效果比較好。
而第二種方法則是要介紹的Floyd算法,它的時間複雜度爲T=O(V^3),對於稠密圖來講效果更好。
對於最短路徑算法來講,其重點都是鬆弛。因爲如今是多源最短路徑問題,之前單源把dist[i]做爲源點S到i的最短路徑,如今源點不單一了,因此直接表示成e(i,j)表示i到j的最短路徑。
咱們已經知道鬆弛的緣由是,有了第三個點爲過渡點,使得距離變小了。
Floyd算法運用動態規劃的思想經過考慮最佳子路徑來獲得最佳路徑
而Floyd算法的步驟就分爲如下兩步
思想很是簡單,簡單來講就是遍歷全部的頂點,看看這個頂點是否能讓任意兩個頂點鬆弛。
核心代碼:
for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (v[i][k] + v[k][j] < v[i][j]) { v[i][j] = v[i][k] + v[k][j]; } } } }
代碼:
咱們在OJ上驗證這個算法:http://acm.hdu.edu.cn/showproblem.php?pid=2544
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StreamTokenizer; public class Main { static int[][] v = new int[101][101]; public static void main(String[] args) throws IOException { StreamTokenizer in = new StreamTokenizer(new BufferedReader( new InputStreamReader(System.in))); int n, m; while (in.nextToken() != StreamTokenizer.TT_EOF) { n = (int) in.nval; in.nextToken(); m = (int) in.nval; init(n); if (n == 0 || m == 0) { break; } for (int i = 1; i <= m; i++) { in.nextToken(); int a = (int) in.nval; in.nextToken(); int b = (int) in.nval; in.nextToken(); int d = (int) in.nval; v[a][b] = d; v[b][a] = d; } floyd(n); System.out.println(v[1][n]); } } private static void floyd(int n) { for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (v[i][k] + v[k][j] < v[i][j]) { v[i][j] = v[i][k] + v[k][j]; } } } } } public static void init(int n) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { v[i][j] = 9999999; } } } }算法時間複雜度很直觀O(V^3),由於要用鄰接矩陣來存儲圖,空間複雜度O(V^2)。
另外須要注意的是:Floyd算法也不能解決帶有「負權迴路」
經過以上4種最短路徑算法,咱們發現,最短路徑算法大概分爲3步
其中Bellman-Ford(鬆弛之後若是還能鬆弛則有負值環)與SPFA(每一個元素的入隊次數不能超過n)能檢測負值環。
咱們簡單的說一下4種算法的鬆弛過程:
時間複雜度:
Dijkstra:普通:O(V^2+E),最小堆優化:O(VlogV+ElogE),斐波那契堆優化:O(E+VlogV)
Bellman-Ford:O(VE)
SPFA:O(kE),有爭論,總之比Bellman-Ford更加快
Floyd:O(V^3)
1. http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html
2. http://www.nocow.cn/index.php/Dijkstra%E7%AE%97%E6%B3%95
3. http://blog.csdn.net/collonn/article/details/18155655
4. http://blog.csdn.net/mengxiang000000/article/details/50266373