关于最短路的随笔
今天做了一個最短路的練習,前面幾道都還比較水,最后一道不久以前做過,而且還糾結過很長一段時間,方法記下了,所以做出來了。可是回頭看看自己的代碼,發現似乎全部都是照搬的白書的代碼。想要重新看看白書加深一下了解,卻發現,有關最短路的好多東西都還沒有了解過,比如說圖的鄰接表的使用以及優先隊列的優化都還不曾了解。再往前一看,卻發現最小生成樹的方法居然也不記得了。所以又重新看了看書,加深一下了解,下面把有關最短路的問題先簡單整理一下,待以后慢慢添加。
首先是最小生成樹,他指的是權值最小的沒有環的圖。而解最小生成樹就有一個最經典的方法,那就是Kruskal。下面是偽代碼
先將所有的邊按照權值的從小到大排序 首先樹為空 初始化連通分量,讓每個節點自成一個獨立的連通分量 for(對于每一條邊e) {如果e的左右端點不在同一個連通分量{邊e加入到樹中合并邊e的左右頂點} }??? 上面的方法求出來的樹就是要求的最小生成樹。由于for循環里面是按照順序拿出的每條邊,而邊又是按照從小到大的順序排序了的,所以加起來一定是權值最小的。但是個人感覺上面代碼寫的相當抽象,什么叫一個連通分量,怎么找兩個點在不在同一個連通分量,又怎么合并???
?
這里就用到了并查集的知識。正好并查集就是對集合的操作,而這個集合正好就可以表示上面的連通分量的概念,至于查找和連通正好又是并查集的最基本的操作。(所以說感覺好像只要是用Kruskal就一定得用并查集一樣)。不多說,
并查集的查找是否在同一集合
int find(int x) {return x == p[x] ? x : p[x] = find(p[x]);}
合并(x,y):
int a = find(x);
int b = find(y);
p[a] = b;
到這里,最小生成樹問題就順利解決了。
?
??? 然后是一般的最短路問題,首先回顧一下最簡單的flyod,它的思想就是暴力枚舉a到b的最短距離肯定可以劃分為a到{......}再到b的最短距離的和(當然集合也可以為空),這個隨便就可以想明白的。當然他的缺點和優點也是顯而易見的、
代碼:
void flyod() {for(int k=0;k<N;k++){for(int i=0;i<N;i++){for(int j=0;j<N;j++){if(d[i][j] > d[i][k] + d[k][j]){d[i][j] = d[i][k] + d[k][j];}}}} }?
然后看一看Dijkstra。它是求單源最短路最唱使用的方法之一。它的思想就是然每一步都是走的當前最短的距離。先看一圖:點擊此處
從圖中可以看到每一步都是找到的路徑最短的路所走的。先看代碼:
1 void dijkstra(int s) 2 { 3 for(int i=0;i<=N;i++) d[i] = INF; 4 d[s] = 0; 5 for(int i=0;i<N;i++) 6 { 7 int m = INF; 8 for(int j = 0;j<N;j++)if(!vis[j] && d[j]<m)m=d[s=j]; 9 vis[s] = 1; 10 for(int j=0;j<N;j++)if(d[j] > d[s]+Map[s][j])d[j]=d[s]+Map[s][j]; 11 } 12 }??? 可以看到代碼中首先將所有的d值賦值為無窮大,只有起點(源點)是0,這也就保證了下面的for循環中第一個找的的滿足d[j]<m的點一定是起點,然后就從它求出它到其他所有點的最小權值。即上面的Map[a][b]表示邊a到b的權值(Map[a][b]=INF相當于a到b的邊不存在)。
??? 在下一次for(i=1)時,又首先找到一個d值最小的點(也就是到起點s最近的點),再求一次,又更新最短距離,這樣的話每次都是從選擇的從起點出發的新的最小權值的點,因此也就求出來了從起點到其他所有點的最短路徑。又因為每一個外部循環,都會有一個頂點被標記,所以外部循環就至少N次,也只需要N次就夠了(繼續循環沒有意義了)。
?
然后就看了一下鄰接表的優化。
??? 首先明確的就是鄰接表只對稀疏圖(也就是邊的數目遠遠小于頂點的數目)作用比較明顯,因為這時就可以不用管那些不存在的邊,我覺的我還是的好好學學鄰接表的使用,除了下了一兩道hash題目外,好像再也沒有用過鄰接表了。它的復雜度就由O(n^2),可以減少到O(mlogn),m是邊的數目。
1 int n,m; 2 int first[MAXN],next[MAXM],u[MAXM],v[MAXM],w[MAXM]; 3 void read_graph() 4 { 5 scanf("%d%d", &n, &m); 6 for(int i=0;i<n;i++) first[i] = -1; 7 for(int e=0;e<m;e++) 8 { 9 scanf("%d%d%d", &u[e],&v[e],&w[e]); 10 next[e] = first[u[e]]; 11 first[u[e]] = e; 12 } 13 }?
??? 既然上面使用了鄰接表來存邊,那么要如何實現mlogn的算法呢,這里就再講講優先隊列的實現。
??? 簡而言之,優先隊列就是存放在隊列里的元素不是按照他們的存進順序排列的,而是按照我們自定義的元素優先級的大小排列的,優先級大的元素會被首先取出來。
??? 這樣的話,那我們就可以在存放每一條邊的時候,按照他們每一個點的d[]值作為優先級比較放進隊列中,這樣的話每次取出d值最小的點,也就相當于上面dijkstra代碼里面的
for(int j = 0;j<N;j++)if(!vis[j] && d[j]<m)m=d[s=j];這一行語句,所以也就不用每次都循環n次來查找了。但是有個問題就是如果僅僅是將d值放進優先隊列的話,在取出來,我們也不會知道它是屬于哪一個頂點的值。
所以這里就又新添加了一個STL的東西,叫做pair,用它便可以將兩個值捆綁在一起,在取出一個元素的時候,也就把它的d值和頂點編號一并取了出來(當然用結構體也是相當方便的)。
看代碼: 1 struct cmp//定義優先隊列的優先級比較 2 { 3 bool operator() (Pair a, Pair b) 4 { 5 return a.first < b.first; 6 } 7 }; 8 9 bool done[MAXN]; 10 typedef pair<int, int> Pair;//用于捆綁d值和序號頂點序號 11 void dijkstra(int s) 12 { 13 mem(done); 14 for(int i=0;i<=N;i++) d[i] = INF;//初始時將suoyoud值設置為+∞ 15 priority_queue<Pair, vector<Pair>, cmp>q;//定義一個優先隊列 16 q.push(make_pair(d[s]=0, s));//將起點放入隊列中,且只有起點的d值為0 17 while(!q.empty())//依次從優先隊列中取出優先級最大的元素(也就是d值最小的點)直到為空 18 { 19 Pair top = q.top(); q.pop(); 20 int x = top.second; 21 if(done[x]) continue;//如果此頂點已經算過了,不在討論 22 done[x] = true; 23 for(int e = first[x]; e != -1; e = next[e])//枚舉此個頂點的所有邊 24 { 25 if(d[v[e]] > d[x] + w[e]) 26 { 27 d[v[e]] = d[x] + w[e]; 28 q.push(make_pair(d[v[e]], v[e]));//將新的d值變小的點放進優先隊列 29 } 30 } 31 } 32 }
?
Bellman-Ford算法:
??? 由于之前的算法都是針對于只含有正權的邊的最短路,如果存在負權,那就該使用Bellman-Ford了。首先需要明確的是,如果存在負權的話,有可能最短路都會不存在(如果n個點形成了一個負權回路的話,那么每一個點再繞一個環回來后那么“最短路”又會縮小),所以Bellman-Ford就給我們提供了一個判斷是否存在負權回路的方法。時間復雜度O(nm)
見代碼:
1 bool Bellman_Ford(int s)//判斷是否存在最短路,如果存在,則d值保留起點s的單源最短路 2 { 3 for(int i = 1; i <= N; i ++) d[i] = INF; 4 d[s] = 0; 5 for(int i=1;i<N;i++)//由于由起點出發只需要N-1次就可以確定起點到其他所有點的最短路 6 { 7 for(int e = 0;e < M; e ++) //枚舉每條邊 8 { 9 int x = u[e], y = v[e]; 10 if(d[x] < INF) d[y] = MIN(d[y], d[x] + w[e]);//松弛 11 } 12 } 13 for(int e = 0; e < M; e ++) if(d[u[e]] < INF)//再一次枚舉所有邊 14 { 15 int x = u[e], y = v[e]; 16 if(d[y] > d[x] + w[e]) return false;//如果還有頂點可以松弛,存在負權回路,不存在最短路 17 } 18 return true; 19 }有了上面dijkstra的思路,我們不難理解他的正確性,這里邊不給出解釋。
???? 同樣,Bellman-Ford也可以用隊列來優化,由于不再需要像dijkstra一樣每次取出d值最小的頂點,所以我們也就不需要使用優先隊列,而使用一般的隊列便可以實現,下面是白書上的一段代碼:
1 queue<int>q; 2 bool inq[MAXN]; 3 for(int i = 0; i < n; i ++) d[i] = (i == s ? 0 : INF); 4 memset(inq, 0, sizeof(inq)); // “在隊列中”的標志 5 q.push(s); 6 while(!q.empty()) 7 { 8 int x = q.front(); q.pop(); 9 inq[x] = false; // 清除“在隊列中”的標志 10 for(int e = first[x]; e != -1; e = next[e]) if(d[v[e]] > d[x] + w[e]) 11 { 12 d[v[e]] = d[x] +w[e]; 13 if(!inq[d[e]]) // 如果已經在隊列中,就不要重復添加了 14 { 15 inq[v[e]] = true; 16 q.push(v[e]); 17 } 18 } 19 }我想如果明白了鄰接表的使用和Bellman-Ford的思想,理解上面的代碼應該問題就不大了。
copy的題目鏈接,慢慢刷
最短路: http://acm.hdu.edu.cn/showproblem.php?pid=1596(中等) http://acm.hdu.edu.cn/showproblem.php?pid=2433(稍難) http://acm.hdu.edu.cn/showproblem.php?pid=2112(基礎) http://acm.hdu.edu.cn/showproblem.php?pid=1874(基礎) http://acm.hdu.edu.cn/showproblem.php?pid=2544(基礎) http://acm.hdu.edu.cn/showproblem.php?pid=2680(中等) http://acm.hdu.edu.cn/showproblem.php?pid=2647(稍難) http://acm.hdu.edu.cn/showproblem.php?pid=1690(中等)轉載于:https://www.cnblogs.com/gj-Acit/p/3254311.html
總結
- 上一篇: javascript实现kruskal算
- 下一篇: 鲁卡斯数列表