最近花了大約一個月左右的時間集中刷了一些圖論的題目。雖然收穫了許多可是仍是付出了不少做業沒有作的代價0.0。在這裏先把本身所作的關於最短路的基礎算法來作一個總結,至少把學到的東西記錄下來。ios
先說明一下在這裏咱們暫且認爲n爲圖中頂點個數,m爲圖中邊的個數,INF爲極大值(能夠是題目計算過程當中不會的獲得的一個大數字)。算法
而後說一下最近學到了什麼吧。最初學習的是dij--O(n^2)的算法。這個在之前數據結構的時候就已經學習過了如今增強了一下,熟練掌握了算法思想而後會裸敲。接着固然就是dij--O(mlogn)的算法。瞭解算法思想而且明白了是如何優化的,而後會裸敲。接着是floyd算法這個最簡單了只有幾行代碼。可是它題目中每每會結合一些動態規劃來考察你,複雜度爲O(n^3),還有就是關於bellmam-ford與SPFA是關於帶負權邊圖上的最短路。前者的複雜度爲O(nm)後者爲O(kn)k爲常數通常狀況下小於2。數組
1、Dijkstra算法數據結構
首先講一下關於dij算法的思想。首先dij算法使用的前提是圖中不存在負權邊。
函數
咱們分三步來說述dij:性能
(1)參數與返回值學習
dij算法是單元最短路因此咱們須要告訴dij函數你的源點(s)是哪個結點,而後函數執行完後dis數組中存放的就是s到圖中全部結點的最短距離,若是不連通的話會返回極大值。優化
(2)初始化spa
在初始化過程當中咱們要定義vis數組--用來記錄已經訪問過的結點,而且清零。而後給dis數組賦初值INF(s點爲0),表示初始狀況下源點到除本身以外的全部結點都爲無窮大。code
(3)算法主體
咱們執行n次循環,每次從dis數組中選出一個值最小的結點-標記此結點-對這個結點所鏈接的每一條邊進行鬆弛
if(mindis+Map[min][j]<dis[j] && Map[min][j]!=INF && vis[j]==0)
dis[j] = mindis+Map[min][j];
而後咱們就能夠給出算法的全部代碼:
1 /**************************************** 2 Dijkstra O(n^2) 單元最短路算法 3 鄰接矩陣 4 By 小豪 5 *****************************************/ 6 #include <iostream> 7 #include <cstdio> 8 #include <string.h> 9 #define INF 0x3f3f3f3f 10 #define LEN 1010 11 using namespace std; 12 13 int Map[LEN][LEN], dis[LEN], n, m; 14 15 void Dijkstra(int s) 16 { 17 int vis[LEN] = {0}; 18 for(int i=1; i<=n; i++) 19 dis[i] = INF; 20 dis[s] = 0; 21 for(int i=0; i<n ;i++) 22 { 23 int min, mindis = INF; 24 for(int j=1; j<=n; j++) 25 if(dis[j]<mindis && vis[j] == 0) 26 { 27 mindis = dis[j]; 28 min = j; 29 } 30 vis[min] = 1; 31 for(int j=1; j<=n; j++) 32 if(mindis+Map[min][j]<dis[j] && Map[min][j]!=INF && vis[j]==0) 33 dis[j] = mindis+Map[min][j]; 34 } 35 } 36 37 38 int main() 39 { 40 // freopen("in.txt", "r", stdin); 41 return 0; 42 }
其實上述dij在實際競賽中時不經常使用的,由於他的複雜度過高,不能符合比賽中大多數題目對於時間效率的要求。咱們實際使用的dij使用優先隊列優化的Dij時間複雜度爲O(mlogn)。仔細想一想咱們會發如今最壞狀況下也就是對於一個徹底圖m=n(n-1)那麼這個版本的的dij複雜度不是退化成O(n^2logn)不只較之前沒有下降反而上升了,這還叫優化?等等,這只是理論上分析而已。在實際使用中因爲他的入隊條件每每得不到知足,因此實際的使用效率會大大的好於O(n^2)的版本,因此你大可放心使用。
下面來說述一下究竟是怎麼對原來的算法進行優化的?前面我分了三塊講述的dij咱們能夠很清晰的看見影響複雜度的是第三塊,第三塊又分爲兩部分--
(1)找出dis最小的點
對於這一塊直接使用優先隊列就能夠了,對於每次選出最小值的操做只須要logn的時間複雜度。
(2)對其鏈接的全部邊進行鬆弛
對於這一塊也很容易咱們能夠不用鄰接矩陣而用鄰接表存儲。這樣很容易證實當左右頂點都訪問事後正好每一條邊都被鬆弛了一次。
綜上所述複雜度O(mlogn)由此產生。
下面咱們給出代碼:
1 /**************************************** 2 Dijkstra O(mlogn) 單元最短路算法 3 By 小豪 4 *****************************************/ 5 #include <iostream> 6 #include <cstdio> 7 #include <cstring> 8 #include <cstdlib> 9 #include <algorithm> 10 #include <utility> 11 #include <vector> 12 #include <queue> 13 #include <stack> 14 #define INF 500001 15 #define LEN 50100 16 using namespace std; 17 18 typedef pair<int, int> pii; 19 vector<pii> Map[LEN]; 20 int dis[LEN]; 21 22 void init(){for(int i=0; i<LEN; i++)Map[i].clear();} 23 24 void Dijkstra(int vex){ 25 priority_queue<pii, vector<pii>, greater<pii> > q; 26 int vis[LEN] = {0}; 27 for(int i=0; i<LEN; i++) dis[i] = (i==vex?0:INF); 28 q.push(make_pair(dis[vex], vex)); 29 while(!q.empty()){ 30 pii nv = q.top(); q.pop(); 31 int x = nv.second; 32 if(vis[x]) continue; 33 vis[x] = 1; 34 for(vector<pii>::iterator it = Map[x].begin(); it!=Map[x].end(); ++it){ 35 int y = it->first, v = it->second; 36 if(dis[y]>dis[x]+v){ 37 dis[y] = dis[x]+v; 38 q.push(make_pair(dis[y], y)); 39 } 40 } 41 } 42 } 43 44 int main() 45 { 46 // freopen("in.txt", "r", stdin); 47 return 0; 48 }
關於代碼的說明:
這裏我習慣用vector<pair<int,int> >來存儲圖。pair的第一個值表示指向的結點,第二個值表示邊的權值。
初始化操做和參數返回值和原來沒有區別,只是後來改爲相似於BFS的形式每次取出一個節點,若是該節點已經被訪問過則丟棄,不然鬆弛全部該結點鏈接的邊,再把鬆弛好的dis,與結點入隊(新更新的值能夠用來更新其餘的結點)。直到隊列爲空算法結束。
2、Floyd算法
對於floyd算法比較簡單,也比較實用,它的特色就是代碼特別短。在比賽的時候背出來就能夠了。
這裏先給出個人代碼:
1 /**************************************** 2 Floyd O(n^3) 最短路算法 3 By 小豪 4 *****************************************/ 5 #include <iostream> 6 #include <cstdio> 7 #include <cstring> 8 #include <cstdlib> 9 #include <cmath> 10 #include <algorithm> 11 #define LEN 1010 12 #define INF 500001 13 using namespace std; 14 15 int Map[LEN][LEN], dis[LEN][LEN]; 16 int n, m; 17 18 void init() 19 { 20 for(int i=0; i<LEN; i++){ 21 for(int j=0; j<LEN; j++){ 22 Map[i][j] = INF; 23 if(i==j)Map[i][j] = 0; 24 } 25 } 26 } 27 28 void floyd() 29 { 30 for(int i=1; i<=n; i++){ 31 for(int j=1; j<=n; j++){ 32 dis[i][j] = Map[i][j]; 33 } 34 } 35 for(int k=1; k<=n; k++){ 36 for(int i=1; i<=n; i++){ 37 for(int j=1; j<=n; j++){ 38 dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]); 39 } 40 } 41 } 42 } 43 44 int main() 45 { 46 // freopen("in.txt", "r", stdin); 47 return 0; 48 }
算法的主體部分是三層for循環k表示i-j通過前k個結點所得到的最短路徑,每一次比較原來的dis[i][j]是否是比通過k結點也就是dis[i][k]+dis[k][j]大,若果是則更新。
floyd算法的主題思想是動態規劃。在實際運用中咱們經常能夠改變dis[i][j]狀態的含義來計算出題目所須要的東西。這一類floyd變形的題目仍是很常見,固然在狀態記錄信息不足時咱們還能夠增長一維用於記錄其餘信息(這是解動態規劃題經常使用的方法),這裏我就再也不詳細敘述了。
3、bellman-ford與SPFA算法(帶負權的最短路問題)
在圖論問題中咱們還會遇到帶負權圖的單源最短路問題,這是dij算法就沒有用武之地了,然而floyd算法的複雜度有太高(也有一些大材小用)。這是咱們就須要用到下面兩個算法:
1.bellman-ford算法
bellman-ford算法的不只思想很簡單,寫起來也很簡單,就兩重循環對全部邊鬆弛n次:
if(dis[y]>dis[x]+w[j])dis[y] = dis[x]+w[j];
這樣在沒有負權環的狀況下咱們能夠求出圖中的最短路。說明:算法中咱們只存圖的邊便可。
代碼以下:(部分代碼)
1 for(int i=0; i<n-1; i++){ 2 for(int j=0; j<m; j++){ 3 int x = u[j], y = v[j]; 4 if(dis[y]>dis[x]+w[j])dis[y] = dis[x]+w[j]; 5 } 6 }
雖然寫起來簡單,可是算法複雜度實在過高了。而實際使用中咱們推薦使用更加優秀的SPFA算法。
2.SPFA算法
SPFA算法是西南交通大學段凡丁於1994年發表的。算法也十分容易實現,並且效率很不錯。因此有很大的實用性,SPFA算法的思想是從廣度優先搜索演變而來的。咱們知道在對於一個不帶權圖上求最短路的時候咱們經常會用用到BFS算法藉助於隊列先進先出的性質,先到達的結點所經歷的步數必定是最短路。因爲每一個結點只入隊一次,複雜度爲O(n)。那麼藉助於這個思想是否是能夠對於帶權圖也求出最短路呢。
SPFA就完成了這一點對於每次出隊的節點對於全部這個結點鏈接的邊執行鬆弛操做,如果dis數組被更新了則將結點從新入隊。(由於更新好的結點有可能會影響到其餘結點的最短路)這裏就能夠看出來每一個結點可能屢次入隊,這裏用k表示平均入隊次數,因此複雜度爲O(kn)在實際使用中k在2左右。可見spfa性能很卓越。
下面給出個人代碼:
1 /**************************************** 2 SPFA O(kn) 單源最短路算法 3 By 小豪 4 *****************************************/ 5 #include <iostream> 6 #include <cstdio> 7 #include <cstring> 8 #include <cstdlib> 9 #include <algorithm> 10 #include <queue> 11 #include <stack> 12 #include <vector> 13 #define LEN 1010 14 #define INF 0x3f3f3f3f 15 #define pb(a) push_back(a) 16 #define mp(a, b) make_pair(a, b) 17 18 using namespace std; 19 20 typedef pair<int, int> pii; 21 int n, dis[LEN]; 22 vector<pii> Map[LEN]; 23 24 //返回false有負環true最短路在dis數組中 25 bool SPFA(int s) 26 { 27 queue<int> q; 28 int vis[LEN] = {0}, cnt[LEN] = {0}; 29 for(int i=0 ;i<n; i++)dis[i] = INF; 30 dis[s] = 0; 31 q.push(s); 32 vis[s] = 1; 33 cnt[s]++; 34 while(!q.empty()){ 35 int nv = q.front(); q.pop(); 36 for(int i=0; i<Map[nv].size(); i++){ 37 int x = Map[nv][i].first, y = Map[nv][i].second; 38 if(dis[x] > dis[nv]+y){ 39 dis[x] = dis[nv] + y; 40 if(!vis[x]){ 41 q.push(x); 42 vis[x] = 1; 43 cnt[x] ++; 44 if(cnt[x]>n) return false; 45 } 46 } 47 } 48 vis[nv] = 0; 49 } 50 return true; 51 } 52 53 int main() 54 { 55 // freopen("in.txt", "r", stdin); 56 return 0; 57 }
對於代碼的說明:
代碼使用的存儲結構與前面dij使用的鄰接表是同樣的,這裏就再也不講一遍了。咱們會發現這裏多出來一個cnt數組。這個數組是幹什麼用的呢?
前面說過SPFA算法計算最短路圖中是不能有負環的,那麼如何判斷負環,這裏cnt數組就產生了做用,cnt是用來記錄結點入隊次數,能夠證實當結點入隊超過n次時說明圖中存在負環。也有一些題目會要求你判斷圖中存不存在負環,這時候就可使用SPFA算法。另外一點和BFS區別就是在對於一個節點操做完後,咱們須要去除結點標記。由於SPFA不像BFS每一個結點只入隊一次,而是須要屢次入隊,因此vis數組是用來標記當前節點是否存在隊列中。