如題,這篇博客就講一講最短路以及其它 亂七八糟 的處理路徑的問題html
至於鄰接表,鄰接矩陣,有向邊和無向邊等基礎概念之類的這裏就不過多闡述了,不會的話建議先在其餘dalao的博客或者書上面學習(請多諒解)node
首先講最短路,由於最短路比較基礎,並且在圖論中也應用較多,在學習了最短路只會就能夠繼續日後面學習了,若是您已經學習過了,能夠直接跳到後面的最長路和次短路中c++
最短路,在一個圖中,求一個地方到另外一個地方的最短路徑。聯繫到咱們以前學過的廣度優先搜索中,也能夠處理相似的問題,因此咱們先想想廣度優先搜索的一些思想——隊列。因此在接下來的最短路算法中,或多或少的會涉及到隊列算法
單源最短路徑,就是指在一個圖中,給你一個起點(起點固定),而後終點不是固定的,求起點到任意終點的最短路徑。這裏會涉及到3種算法,如下用$dis[]$表示起點到任意終點的最短距離數組
時間複雜度:O(nm)oop
給定一個圖,對於圖中的某一條邊(x,y,z),x和y表示兩個端點,z表示鏈接兩條邊的邊權,若是有全部邊都知足dis[y]≤dis[x]+z,則dis[]數組的值就是要求的最短路徑學習
這個算法的流程就是基於以上的式子進行操做的:優化
1.掃描全部的邊,若是有 d[y]>d[x]+z ,則 d[y]=d[x]+z (這也被叫作鬆弛操做) 2.重複以上的操做,知道全部邊沒法進行鬆弛操做
仍是比較好理解的,這裏就不掛上代碼了,由於講這個算法的目的是爲了下一個算法做鋪墊ui
時間複雜度:O(km) (k爲一個較小的常數)spa
SPFA算法其實就是用隊列優化事後的Ford的算法,因此沒事別用Ford算法 ,因此它的算法實現和Ford算法實際上是有類似之處的:
1.創建隊列,起初隊列中的節點只有起點 2.取出隊頭的點 x ,而後掃描 x 的全部出邊(x,y,z)進行鬆弛操做,若是 y 不在隊列中,將 y 入隊 3.重複以上操做,直到隊列爲空
------分割線,下面是代碼------
int head[MAXN],tot; struct edge{ int net,to,w; }e[MAXN]; void add(int x,int y,int z){ e[++tot].net=head[x]; e[tot].to=y; e[tot].w=z; head[x]=tot; } //以上是鏈式前向星的建邊 bool v[MAXN]; //是否入隊 int dis[MAXN],vis[MAXN]; //dis爲最短距離,vis爲入隊次數,若是入隊次數太多,說明該圖中有環 queue<int>q; //隊列 bool spfa(int s){ for(register int i=1;i<=n;i++) dis[i]=INF,v[i]=false; //初始化 d[s]=0,v[s]=true; vis[s]++; q.push(s); while(!q.empty()){ int x=q.front(); q.pop(); //取出隊頭 v[x]=false; if(vis[x]>n) return false; //超過了n次,就說明有環 for(register int i=head[x];i;i=e[i].net){ //掃描x的出邊 int y=e[i].to,z=e[i].w; if(d[y]>d[x]+z){ //鬆弛操做 d[y]=d[x]+z; if(v[y]==false){ //是否入隊 v[y]=true; vis[y]++; q.push(y); } } } } return true; }
相信你們都據說過流傳於OI界的一句話「關於SPFA,它死了」,是由於有的出題人故意出數據卡SPFA,因此SPFA的時間複雜度會退化爲Ford,因此在下面又會介紹一種超級香的算法
SPFA已死,Dijkstra當立!!!
這裏先講DIjkstra的算法流程:
1.初始化dis[]爲極大值,起點爲0 2.找出一個沒有被標記過的且dis[]值最小的節點x,而後標記點x 3.掃描x的出邊,進行鬆弛操做 4.重複以上步驟,直到全部點都被標記
這裏不難看出Dijkstra是基於貪心思想的一種最短路算法,咱們經過一個已經肯定了的最短路$dis[x]$,而後不斷找到全局最小值進行標記和擴展,最終實現算法,其實對於以上的步驟,也能夠進行一個堆優化(優先隊列優化),因此下面我會給出兩個程序段
未優化 時間複雜度:O(n^2)
int dis[MAXN]; bool v[MAXN]; void Dijkstra(int s){ for(register int i=1;i<=n;i++) dis[i]=INF,v[i]=false; d[s]=0; //初始化 for(register int i=1;i<n;i++){ int x=0; for(register int j=1;j<=n;j++){ if(v[j]==false&&(x==0||d[j]<d[x])) x=j; } //找到最小的x v[x]=true; for(register int y=1;y<=n;y++){ d[y]=min(d[y],d[x]+a[x][y]); } //鬆弛操做 } } ··· ··· for(register int i=1;i<=n;i++){ for(register int j=1;j<=n;j++){ a[i][j]=INF; } a[i][i]=0; } for(register int i=1;i<=m;i++){ int x,y,z; a[x][y]=min(a[x][y],z); //取min是爲了判斷重邊 } //創建鄰接矩陣
堆優化 時間複雜度:O(m log n)
int head[MAXN],tot; struct edge{ int net,to,w; }e[MAXN]; void add(int x,int y,int z){ e[++tot].net=head[x]; e[tot].to=y; e[tot].w=z; head[x]=tot; } //鄰接表建邊 int d[MAXN]; bool v[MAXN]; priority_queue<pair<int,int> >q; //這裏是建大根堆,利用相反數實現小根堆 //first爲距離,second爲編號 //按first從小到大排序 //或者你本身手寫重載運算符 void Dijkstra(int s){ for(register int i=1;i<=n;i++) d[i]=INF,v[i]=false; d[s]=0; q.push(make_pair(0,s)); while(!q.empty()){ int x=q.top().second; q.pop(); if(v[x]==true) continue; v[x]=true; for(register int i=head[x];i;i=e[i].net){ int y=e[i].to,z=e[i].w; if(d[y]>d[x]+z){ d[y]=d[x]+z; q.push(make_pair(-d[y],y)); //很是靈魂的取相反數 } } } }
關於Dijkstra,它是真的很香,由於確實跑得很快,對於單源最短路的算法就介紹到這裏了,可是對於這些算法的各自特色,我會留到最後來說
目前涉及到的還只有FLoyd算法,固然還有一個Johnson的全源最短路算法,由於用的很少,這裏就不過多介紹
時間複雜度:O(n^3)
對於Floyd的實現,其實很是的簡單,它有一點像動態規劃的方式,經過枚舉全部中間點進行鬆弛操做,大概就是在直接路徑和間接路徑中取一個最小的,這裏就直接掛上代碼了
for(register int i=1;i<=n;i++){ for(register int j=1;j<=n;j++){ d[i][j]=INF; } d[i][i]=0; }//鄰接矩陣存儲,d[i][j]表示i到j的距離 for(register int k=1;k<=n;k++){ //第一層枚舉中間點 for(register int i=1;i<=n;i++){ //第二層枚舉起點 for(register int j=1;j<=n;j++){ //第三層枚舉終點 if(i!=j&&j!=k) d[i][j]=min(d[i][j],d[i][k]+d[k][j]); //動態轉移方程,在間接路徑和直接路徑中取最小值 } } }
以上就是對於最短路的算法介紹,這裏會對各類算法進行對比和總結,而後給出一些我我的認爲好一點的例題
首先是Ford算法,不用說,能不用就別用,由於SPFA算法在大部分時候都比Ford算法優越,最多就和Ford算法同樣
而後說SPFA,SPFA其實能夠處理負邊權和負環的狀況,這是它的特色,而SPFA在不被卡的狀況下實際上是比Dijkstra更加快的(可是SPFA基本上都會被卡的死死的)
過了就是DIjkstra,這個算法其實算是能夠優先選擇,可是遇到環和負邊權的狀況,它是徹底不能處理的,這個時候就要回去考慮SPFA了
對於FLoyd,若是不是多源最短路就能夠不考慮,由於二維數組的空間不會太大,而且n^3的時間複雜度估計沒人會接受吧,可是Floyd(Floyd的變種)有一些其它的應用,這裏不會涉及
先上兩道通用的模板題:
第二道其實徹底能夠不考慮,可是仍是要放一下,這樣大家才能本身親身感覺一下上面各種算法的區別,建議你們各類算法都試一試(SPFA真的死得特別慘)
而後就是其它的一些單獨的算法了:
Dijkstra:
P1529 [USACO2.4]回家 Bessie Come Home
這幾道題中,郵遞員送信會涉及到一點反向圖的知識,能夠去看個人另外一篇博客(啊。無恥)。回家那道題難在一些字符串的處理上。剩下兩道題就比較模板了,考驗你們對算法的本質的一些認識
SPFA:
我是真的沒有找到幾道必須用SPFA作的題,因此你們見諒啊,可是全部能用Dijkstra的均可以用SPFA,可是通常會被卡。。。這道題難在處理點權和邊權的關係上面
Floyd:
P1522 [USACO2.4]牛的旅行 Cow Tours
若是你能本身A掉上面的題,證實你對Floyd的理解已經很深很透徹了,因此在思惟難度上是比較高的
最短路的綜合練習:
這三道題就是用來告訴你如何記錄最短路的路徑的,爲以後的次短路的算法做一下鋪墊吧,順便加深理解。這裏就不放代碼了,若是不會的話能夠去看看個人博客或者其餘dalao的題解
最長路,顧名思義嘛,最短路就是道路最短,那就最長路就是道路最長了咯
最長路的求法也有兩種,一種是SPFA,一種是拓撲排序,拓撲排序跑得比SPFA快不少,這裏也要說一下,雖然SPFA容易被卡,可是但願那些認爲SPFA沒用的人也去學一學,這是頗有必要的(儘管我知道用SPFA的人不少)
首先講SPFA,咱們知道SPFA算法能夠處理負邊權的問題,若是你上太小學,那麼你確定知道,一個負數越小,那它的絕對值確定更大。這樣咱們就能夠把最長路問題轉換爲最短路問題了
相比讀者確定已經想到了,在存邊的時候,咱們只須要把邊權取一個相反數,而後正常地求最短路,在最後的答案中取一個相反數就能夠了,是否是很簡單?
而後是拓撲排序,不知道或是不瞭解拓撲排序的能夠看一下這篇博客(繼續無恥),同桌的拓撲排序
瞭解拓撲排序以後,咱們其實能夠知道使用拓撲排序的話是有限制的,它只能處理有向無環圖,無向圖這些都不能處理,可是仍是要去學。使用拓撲排序的話,須要用到一些DP的思想,這個地方不太好講解思路,直接在代碼裏面看實現方法
這裏就直接用一個例題來說解了
#include <bits/stdc++.h> using namespace std; int n,m,u,v,w,tot; int dis[510010],vis[510010],head[510010]; struct node { int to,net,val; } e[510010]; inline void add(int u,int v,int w) { e[++tot].to=v; e[tot].net=head[u]; e[tot].val=w; head[u]=tot; } //鏈式前向星建邊 inline void spfa() { queue<int> q; for(register int i=1;i<=n;i++) dis[i]=20050206; dis[1]=0; vis[1]=1; q.push(1); while(!q.empty()) { int x=q.front(); q.pop(); vis[x]=0; for(register int i=head[x];i;i=e[i].net) { int v=e[i].to; if(dis[v]>dis[x]+e[i].val) { dis[v]=dis[x]+e[i].val; if(!vis[v]) { vis[v]=1; q.push(v); } } } } }//正常跑最短路 int main() { scanf("%d%d",&n,&m); for(register int i=1;i<=m;i++) { scanf("%d%d%d",&u,&v,&w); add(u,v,-w);//很是靈魂地存一個相反數 } spfa(); if(dis[n]==20050206) puts("-1"); //到不了就-1 else printf("%d",-dis[n]);//記得存回來 return 0; }
#include<bits/stdc++.h> using namespace std; const int MAXN=2*5*1e4; int n,m; struct edge{ int net,to,w; }e[MAXN]; int head[MAXN],tot; void add(int x,int y,int z){ e[++tot].net=head[x]; e[tot].to=y; e[tot].w=z; head[x]=tot; } //鏈式前向星建邊 bool v[MAXN]; //用來標記是否能夠從1走到這個點 //由於是1到n,因此若是不能從1開始走 //說明不知足條件,沒有這條最長路 int ru[MAXN]; int ans[MAXN]; queue<int>q; void toop(){ for(register int i=1;i<=n;i++){ if(ru[i]==0) q.push(i); }//入度爲0的進隊 while(!q.empty()){ int x=q.front(); q.pop();//出隊 for(register int i=head[x];i;i=e[i].net){ int y=e[i].to,z=e[i].w; ru[y]--;//入度-- if(v[x]==true){ ans[y]=max(ans[y],ans[x]+z); v[y]=true; }//若是這個節點能從1走到,說明它的邊能夠走 //更新最長路 if(ru[y]==0) q.push(y);//進隊 } } } int main(){ scanf("%d%d",&n,&m); for(register int i=1;i<=m;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); ru[v]++; }//建邊,入度++ v[1]=true;//1確定本身能走 ans[n]=-1;//初始值爲-1,方便輸出 toop();//拓撲排序求最長路 cout<<ans[n]; return 0; }
最長路的其餘題:
有了最短路和最長路,那麼確定就有次短路,仍是很好理解的,就是第二短路(除了最短路的最短路)
這裏的話,我就只介紹一種方法了,還有一個A star算法 這貌似均可以用來作K短路了,我想都不敢想(好吧,單純就是我不會,若是我學會了我會回來更的)
簡明扼要的來講,咱們求次短路,確定和最短路脫不了干係,因此怎麼說要先把最短路跑出來,這樣纔能有一個拿來比較的東西
次短路,它確定比最短路要長(廢話),考慮一種很是極端的狀況,次短路確定不會是最短路(廢話),那麼次短路確定至少有一條邊不在最短路上,明白這個很重要,固然它也多是徹底沒有交集的兩條邊
瞭解以後,咱們來想一想到底怎麼實現這個次短路。由上面的推斷,咱們確定須要去記錄最短路的路徑和通過的節點,若是你沒法理解這個東西,能夠去上面找一找瑪麗卡和最短路計數兩題
咱們能夠嘗試把最短路上的任意一條邊刪掉,而後從新跑最短路,這樣就能夠保證了我以後跑的全部最短路都比第一次的最短路要長,而後經過比較就能夠求出次短路了,咱們經過一道例題來具體理解一下
這道題仍是比較模板,其它次短路的題我並無接觸過多少,因此仍是讀者本身去領悟和多刷題(見諒)
拿到這道題後,確定先把建邊這些不那麼重要的東西先處理掉,記得用double和一些精度處理,全部的邊和存儲答案都用double。而後按上面講的思路實現一遍
跑最短路 -> 記錄路徑 -> 枚舉刪邊,再跑最短路 -> 處理答案
但其實題目中還告訴了一些條件,就是關於一些無解的判斷
這實際上是很好理解的,若是存在多條最短路徑,那我在枚舉刪除第一條最短路上的邊的時候,是徹底不影響其它最短路的,那麼咱們求出來的仍是一條最短路,過掉
若是不存在第二短路徑,說明起點和終點之間只存在一條簡單路徑,而這條路徑就是最短路,若是刪去邊以後就沒法到達終點了,特判一下就ok
那麼思路就這麼講完了,咱們直接用代碼來加深理解一下
#include<bits/stdc++.h> using namespace std; const int MAXN=1e7+50; const double INF=200500305; int n,m; int x[MAXN],y[MAXN]; struct node{ int net,to,from; double w; }e[MAXN]; int head[MAXN],tot; void add(int u,int v,double w){ e[++tot].net=head[u]; e[tot].to=v; e[tot].from=u; //這裏的from和to表示這一條邊的兩個端點 //在後面的程序中用來比較求次短路 e[tot].w=w; head[u]=tot; } //鏈式前向星建邊 double d[MAXN]; int bian[MAXN]; //記錄最短路 bool v[MAXN]; inline bool ok(int i,int j){ if(min(e[i].to,e[i].from)==min(e[j].to,e[j].from)&&max(e[i].to,e[i].from)==max(e[j].to,e[j].from))return 0; return 1; }//這一坨長長的東西用來判斷是否是我此次要刪掉的邊 void dij(int s,int p){ //p用來表示刪除哪一條邊 priority_queue<pair<double,int> >q; for(register int i=1;i<=n;i++) d[i]=INF,v[i]=false; d[s]=0; //初始化 q.push(make_pair(0,s)); while(!q.empty()){ int x=q.top().second; q.pop(); if(v[x]==true) continue; v[x]=true; for(register int i=head[x];i;i=e[i].net) if(p==-1||ok(i,p)){ //若是是第一次跑最短路就記錄路徑,若是是該邊被刪去就不跑 int y=e[i].to; double z=e[i].w; if(d[y]>d[x]+z){ d[y]=d[x]+z; if(p==-1)bian[y]=i; //第一次跑最短路記錄路徑 q.push(make_pair(-d[y],y)); } } } } double Min(double x,double y){ if(x<=y) return x; return y; } //c++自帶的min不支持double類型的比較 int main(){ scanf("%d%d",&n,&m); for(register int i=1;i<=n;i++) scanf("%d%d",&x[i],&y[i]); for(register int i=1;i<=m;i++){ int u,v; double w; scanf("%d%d",&u,&v); w=(double)sqrt((x[u]-x[v])*(x[u]-x[v])+(y[u]-y[v])*(y[u]-y[v])); add(u,v,w); add(v,u,w); }//建雙向變 dij(1,-1); //第一次跑最短路不刪邊 int t=n; //用t來代替n,遍歷最短路的邊 double ans=INF; while(t!=1){ int i=bian[t]; dij(1,i); ans=min(ans,d[n]); //取一個更小的答案表示次短路 t=e[bian[t]].from; //遍歷最短路的路徑 } printf("%.2lf",ans); //輸出答案 return 0; }
博客園的話,我不太會用Markdown,因此我把洛谷博客也掛在這裏
感謝一下ZJY,同桌和RHL三位大佬提供的一些幫助啊
這篇博客就寫到這裏了,若是我誤人子弟了,能夠在評論區指出錯誤或者在QQ上告訴我,我會盡早改正,這麼長的文章,謝謝閱讀