如题,这篇博客就讲一讲最短路以及其它 乱七八糟 的处理路径的问题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上告诉我,我会尽早改正,这么长的文章,谢谢阅读