如題,這篇部落格就講一講最短路以及其它 亂七八糟 的處理路徑的問題
至於鄰接表,鄰接矩陣,有向邊和無向邊等基礎概念之類的這裡就不過多闡述了,不會的話建議先在其他dalao的部落格或者書上面學習(請多諒解)
最短路
首先講最短路,因為最短路比較基礎,而且在圖論中也應用較多,在學習了最短路只會就可以繼續往後面學習了,如果您已經學習過了,可以直接跳到後面的最長路和次短路中
最短路,在一個圖中,求一個地方到另一個地方的最短路徑。聯絡到我們之前學過的廣度優先搜尋中,也可以處理類似的問題,所以我們先想一想廣度優先搜尋的一些思想——佇列。所以在接下來的最短路演算法中,或多或少的會涉及到佇列
單源最短路徑
單源最短路徑,就是指在一個圖中,給你一個起點(起點固定),然後終點不是固定的,求起點到任意終點的最短路徑。這裡會涉及到3種演算法,以下用$dis[]$表示起點到任意終點的最短距離
1. Bellman-Ford演算法
時間複雜度:O(nm)
給定一個圖,對於圖中的某一條邊(x,y,z),x和y表示兩個端點,z表示連線兩條邊的邊權,如果有所有邊都滿足dis[y]≤dis[x]+z,則dis[]陣列的值就是要求的最短路徑
這個演算法的流程就是基於以上的式子進行操作的:
1.掃描所有的邊,如果有 d[y]>d[x]+z ,則 d[y]=d[x]+z (這也被叫做鬆弛操作)
2.重複以上的操作,知道所有邊無法進行鬆弛操作
還是比較好理解的,這裡就不掛上程式碼了,因為講這個演算法的目的是為了下一個演算法作鋪墊
2. SPFA演算法
時間複雜度:O(km) (k為一個較小的常數)
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,所以在下面又會介紹一種超級香的演算法
3. Dijkstra演算法
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的全源最短路演算法,因為用的不多,這裡就不過多介紹
Floyd演算法
時間複雜度: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的思想,這個地方不太好講解思路,直接在程式碼裡面看實現方法
這裡就直接用一個例題來講解了
SPFA
#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上告訴我,我會盡早改正,這麼長的文章,謝謝閱讀