浅说——最短路径
四种方法:
Dijkstra,Bellman-ford,SPFA,Floyd
—————————————————————————————————————————————————
思想(懒得看可以不看,就是加离源点最近的边,懂?):
G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合
(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将其加入到集合S中,直到全部顶点都加入到S中,算法就结束了)
第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。
在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。
此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
形象一点:
我们把点分为两类,一类是已确定最短路径的点,称为“白点”,另一类是未确定最短路径的点,称为“蓝点”。
如果我们要求出一个点的最短路径,就是把这个点由蓝点变为白点。从起点到蓝点的最短路径上的中转点在这个时刻只能是白点。
Dijkstra的算法思想,就是一开始将起点到起点的距离标记为0,而后进行n次循环,每次找出一个到起点距离dis[u]最短的点u,将它从蓝点变为白点。
随后枚举所有的蓝点vi,如果以此白点为中转到达蓝点vi的路径dis[u]+w[u][vi]更短的话,这将它作为vi的“更短路径”dis[vi](此时还不确定是不是vi的最短路径)。
就这样,我们每找到一个白点,就尝试着用它修改其他所有的蓝点。中转点先于终点变成白点,故每一个终点一定能够被它的最后一个中转点所修改,而求得最短路径。
Dijkstra步骤:
(1)初始时,S只包含源点,即S=v,距离为0。U包含除v外的其他顶点,U中顶点u距离为边上的权;
(2)从U中选取一个距离v最小的顶点k,把k加入S中(该选定的距离就是v到k的最短路径长度);
(3)以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值为经过顶点k的值(松弛操作,important!!);
(4)重复步骤(2)和(3)直到所有顶点都包含在S中。
下面我求下图,从顶点v1到其他各个顶点的最短路径
首先第一步,我们先声明一个dis数组,该数组初始化的值为:
我们的顶点集T的初始化为:T={v1}
既然是求 v1顶点到其余各个顶点的最短路程,那就先找一个离 1 号顶点最近的顶点。
通过数组 dis 可知当前离v1顶点最近是 v3顶点。
当选择了 2 号顶点后,dis[2](下标从0开始)的值就已经从“估计值”变为了“确定值”,即 v1顶点到 v3顶点的最短路程就是当前 dis[2]值。将V3加入到T中。
为什么呢?因为目前离 v1顶点最近的是 v3顶点,并且这个图所有的边都是正数,那么肯定不可能通过第三个顶点中转,使得 v1顶点到 v3顶点的路程进一步缩短了。因为 v1顶点到其它顶点的路程肯定没有 v1到 v3顶点短.
OK,既然确定了一个顶点的最短路径,下面我们就要根据这个新入的顶点V3会有出度,
发现以v3 为弧尾的有: < v3,v4 >,那么我们看看路径:v1–v3–v4的长度是否比v1–v4短,其实这个已经是很明显的了,因为dis[3]代表的就是v1–v4的长度为无穷大,而v1–v3–v4的长度为:10+50=60,所以更新dis[3]的值,得到如下结果:
因此 dis[3]要更新为 60。这个过程有个专业术语(好牛逼的样子)叫做“松弛”。即 v1顶点到 v4顶点的路程即 dis[3],通过 < v3,v4> 这条边松弛成功。这便是 Dijkstra 算法的主要思想:通过“边”来松弛v1顶点到其余各个顶点的路程。
然后,我们又从除dis[2]和dis[0]外的其他值中寻找最小值,发现dis[4]的值最小,通过之前是解释的原理,可以知道v1到v5的最短距离就是dis[4]的值,然后,我们把v5加入到集合T中。
又然后,考虑v5的出度是否会影响我们的数组dis的值,v5有两条出度:< v5,v4>和 < v5,v6>,
再然后我们发现:v1–v5–v4的长度为:50,而dis[3]的值为60,所以我们要更新dis[3]的值.
v1-v5-v6的长度为:90,而dis[5]为100,所以我们需要更新dis[5]的值。更新后的dis数组如下图:
额然后,继续从dis中选择未确定的顶点的值中选择一个最小的值,发现dis[3]的值是最小的,所以把v4加入到集合T中,此时集合T={v1,v3,v5,v4}
嗯然后,考虑v4的出度是否会影响我们的数组dis的值,v4有一条出度:< v4,v6>,然后我们发现:v1–v5–v4–v6的长度为:60,而dis[5]的值为90,所以我们要更新dis[5]的值,更新后的dis数组如下图:
对然后,我们使用同样原理,分别确定了v6和v2的最短路径,最后dis的数组的值如下:
因此,从图中,我们可以发现v1-v2的值为:∞,代表没有路径从v1到达v2。所以我们得到的最后的结果为:
两种实现方法:
1.邻接矩阵+找最小边(简单缓慢)
2.邻接表+优先队列(复杂快速)
关键:
松弛操作
if(d[v]>d[u]+e[u][v].w) //如果源点经过u点距离v更短,更新d[v] d[v]=d[u]+e[u][v].w
稠密图的邻接矩阵:
for(int i=0;i<n;i++)//一共寻找n个点的最小dis { int x,m=inf; for(int y=0;y<n;y++) if(!vis[y]&&dis[y]<=m)//刷新最短dis m=dis[y],x=y; vis[x]=1; for(int y=0;y<n;y++) if(dis[y]>dis[x]+w[x][y])//刷新个点到原点距离 dis[y]=dis[x]+w[x][y]; }
邻接链表+优先队列:
memset(dis,127,sizeof(dis)); dis[1]=0; q.push(make_pair(dis[1],1)); while(!q.empty()) { int u=q.top().second;q.pop(); if(vis[u]) continue; vis[u]=1; for(int k=head[u];k!=-1;k=e[k].next) { if(dis[e[k].v]>dis[u]+e[k].w) { dis[e[k].v]=dis[u]+e[k].w; q.push(make_pair(dis[e[k].v],e[k].v)); }
路径输出:
方法一:从终点出发,不断顺着dis[y]==dis[x]+w[x][y]的边从y回到x,直到回到起点(太慢了),但更好的方法是方法二。
方法二:在更新时维护father指针 for(int y=0;y<n;y++) if(dis[y]>dis[x]+w[x][y]) father[y]=x;(即可一直递归)
邻接矩阵.练习:1261:【例9.5】城市交通路网
#pragma GCC optimize(2)//开O2,不用管,建议慎用(NOIP不准用) #include<cstdio> #include<string> #include<iostream> #include<cstring> #include<stdlib.h> using namespace std; const int maxn=101; const int inf=0x7f7f7f7f; int n; int map[maxn][maxn]; bool vis[maxn]; int dis[maxn]; int pre[maxn]; int f=1; int read() { int x=0,f=1;char c=getchar(); while(!isdigit(c)){ if(c=='-') f=-1; c=getchar(); } while(isdigit(c)) { x=x*10+c-'0'; c=getchar(); } return x*f; } void print(int x)//经典的输出函数 { if(x==0)return; else { print(pre[x]); printf("%d ",x+1); } } void work() { n=read(); memset(dis,inf,sizeof(dis)); for (int i=0;i<n;i++) for (int j=0;j<n;j++) { map[i][j]=read(); if(map[i][j]==0) map[i][j]=inf; else if(i==0) dis[j]=map[i][j]; } dis[0]=0; //printf("minlong=%d\n",dis[n-1]); //printf("%d",dis[4]); for(int i=0;i<n;i++) { int minn=inf,x=0; for (int j=0;j<n;j++) { if(!vis[j]&&minn>dis[j]) minn=dis[j],x=j; } vis[x]=1; //printf("%d\n",x); for (int j=0;j<n;j++) if(map[x][j]+dis[x]<dis[j]) { pre[j]=x; //我竟然卡了输出 dis[j]=map[x][j]+dis[x]; } } printf("minlong=%d\n",dis[n-1]); printf("1 "); print(n-1); } int main() { work(); return 0; }
邻接链表+优先队列.练习:RQNOJ 星门龙跃
#include<cstdio> #include<cstring> #include<queue> #define inf 0x3f3f3f3f using namespace std; typedef pair<int,int> pii; priority_queue<pii,vector<pii>,greater<pii> >q; struct edge{ int x,z,next; }e[300005]; int n,m,f[30005],vis[30005],tot=1,head[30005]; void adde(int u,int x,int z)//建树 { e[tot].x=x; e[tot].z=z; e[tot].next=head[u]; head[u]=tot++; } int main() { int a,b,c; memset(head,-1,sizeof(head)); memset(f,inf,sizeof(f)); scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { scanf("%d%d%d",&a,&b,&c); adde(a,b,c); adde(b,a,c); } f[1]=0; q.push(make_pair(f[1],1)); while(!q.empty()) { int u=q.top().second; q.pop(); if(vis[u]) continue; vis[u]=1; for(int k=head[u];k!=-1;k=e[k].next) { int t=e[k].x; if(f[u]+e[k].z<f[t]) { f[t]=f[u]+e[k].z; q.push(make_pair(f[t],t)); } } } printf("%d",f[n]); return 0; }
——————————————————————————————————————————————————————
Floyd(暴力松弛):
算法的特点:
弗洛伊德算法是解决任意两点间的最短路径的一种算法,可以正确处理有向图或有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包。
算法的思路:
so easy,三层循环,第一层循环中间点k,第二第三层循环起点终点i、j,算法的思想很容易理解:如果点i到点k的距离加上点k到点j的距离小于原先点i到点j的距离,那么就用这个更短的路径长度来更新原先点i到点j的距离。(其实就是松弛)
动态规划原理
设d(i,j)为从i到j的只以(1…k)集合中的节点为中间点的最短路的长度;
1、若经过k,d(i,j)=d(i,k)+d(k,j)
2、若不经过k(可能经过1~k-1中的点),d(i,j)=d(i,j)
则d(i,j)=min(d(i,k)+d(k,j), d(i,j))(循环刷新最短路径)
我们求下图的每个点对之间的最短路径的过程如下:
第一步,我们先初始化两个矩阵,得到下图两个矩阵:
第二步,以v1为中阶,更新两个矩阵:
发现,a[1][0]+a[0][6] < a[1][6] 和a[6][0]+a[0][1] < a[6][1],所以我们只需要矩阵D和矩阵P,结果如下:
通过矩阵P,我发现v2–v7的最短路径是:v2–v1–v7
第三步:以v2作为中介,来更新我们的两个矩阵,使用同样的原理,扫描整个矩阵,得到如下图的结果:
OK,到这里我们也就应该明白Floyd算法是如何工作的了,他每次都会选择一个中介点,然后,遍历整个矩阵,查找需要更新的值,下面还剩下五步,就不继续演示下去了,理解了方法,我们就可以写代码了。
详情:
for(k=0;k<n;k++)//k代表中间点必须放最外层 for(i=0;i<n;i++) for(j=0;j<n;j++) if(d[i][j]>d[i][k]+d[k][j]) d[i][j]= d[i][k]+d[k][j];
因为Floyd为O(N3),所以慎用,只在求任意两点(求多次)的情况下使用。
练习:自己练吧qaq.
—————————————————————————————————————————————————
Bellman-ford:
同样是用来计算从一个点到其他所有点的最短路径的算法,也是一种单源最短路径算法。 能够处理存在负边权的情况,但无法处理存在负权回路的情况。
算法步骤:
1、初始化:将除源点外的所有顶点最短距离估计值d[v]=inf,d[s]=0;
2、迭代求解:反复对边集E中每条边进行松弛操作,使得顶点集V中每个顶点v的最短距离估计值逐步逼近其最短距离;
3、检验负权回路:如果有存在点v,使得d[v]>d[u]+w[u][v],则有负权回路,返回false;
4、返回true,源点到v的最短距离保存在d[v]中。
算法分析&思想讲解:
Bellman-Ford算法的思想很简单。一开始认为起点是白点(dis[1]=0),每一次都枚举所有的边,必然会有一些边,连接着白点和蓝点。因此每次都能用所有的白点去修改所有的蓝点,每次循环也必然会有至少一个蓝点变成白点。
负权回路:
虽然Bellman-Ford算法可以求出存在负边权情况下的最短路径,却无法解决存在负权回路的情况。
负权回路是指边权之和为负数的一条回路,上图中②-④-⑤-③-②这条回路的边权之和为-3。
在有负权回路的情况下,从1到6的最短路径是多少?答案是无穷小,因为我们可以绕这条负权回路走无数圈,每走一圈路径值就减去3,最终达到无穷小。
所以说存在负权回路的图无法求出最短路径,Bellman-Ford算法可以在有负权回路的情况下输出错误提示。
如果在Bellman-Ford算法的n-1重循环完成后,还是存在某条边使得:dis[u]+w<dis[v],
则存在负权回路: For每条边(u,v) If (dis[u]+w<dis[v]) return False
伪代码:
Bool bellman-ford(G,w,s) { for each vertex in V(G)d[v]=inf;d[s]=0; for(i=1;i<v;i++)//执行v-1次操作 for each egde(u,v) in E(G)//对每条边尝试松弛 if(d[v]>d[u]+w[u][v])d[v]=d[u]+w[u][v]; for each edge(u,v) in E(G)//v-1次松弛结束若还可以松弛,则有负环 if(d[v]>d[u]+w[u][v])return false; return true; }
——————————————————————————————————————————————————
SPFA:
我劝大家能用Bellman-ford,就用SPFA,毕竟SPFA是对Bellman的优化(也不复杂,Dijkstra的堆优化就复杂多了)
思想:
初始时将起点加入队列。每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队。直到队列为空时算法结束。利用了每个点不会更新次数太多的特点发明的此算法。
SPFA 在形式上和广度优先搜索非常类似。
不同的是广度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去。
算法时间复杂度:O(kE),E是边数。K是常数,平均值为2。
下面我们采用SPFA算法对下图求v1到各个顶点的最短路径,通过手动的方式来模拟SPFA每个步骤的过程
初始化:
首先我们先初始化数组dis如下图所示:(除了起点赋值为0外,其他顶点的对应的dis的值都赋予无穷大,这样有利于后续的松弛)
此时,我们还要把v1如队列:{v1}
现在进入循环,直到队列为空才退出循环。第一次循环:
首先,队首元素出队列,即是v1出队列,然后,对以v1为弧尾的边对应的弧头顶点进行松弛操作,可以发现v1到v3,v5,v6三个顶点的最短路径变短了,更新dis数组的值,得到如下结果:
我们发现v3,v5,v6都被松弛了,而且不在队列中,所以要他们都加入到队列中:{v3,v5,v6}
第二次循环
此时,队首元素为v3,v3出队列,然后,对以v3为弧尾的边对应的弧头顶点进行松弛操作,可以发现v1到v4的边,经过v3松弛变短了,所以更新dis数组,得到如下结果:
第三次循环
此时,队首元素为v5,v5出队列,然后,对以v5为弧尾的边对应的弧头顶点进行松弛操作,发现v1到v4和v6的最短路径,经过v5的松弛都变短了,更新dis的数组,得到如下结果:
我们发现v4、v6对应的值都被更新了,但是他们都在队列中了,所以不用对队列做任何操作。队列值为:{v6,v4}、
第四次循环
此时,队首元素为v6,v6出队列,然后,对以v6为弧尾的边对应的弧头顶点进行松弛操作,发现v6出度为0,所以我们不用对dis数组做任何操作,其结果和上图一样,队列同样不用做任何操作,它的值为:{v4}
第五次循环
此时,队首元素为v4,v4出队列,然后,对以v4为弧尾的边对应的弧头顶点进行松弛操作,可以发现v1到v6的最短路径,经过v4松弛变短了,所以更新dis数组,得到如下结果:
因为我修改了v6对应的值,而且v6也不在队列中,所以我们把v6加入队列,{v6}
第六次循环
此时,队首元素为v6,v6出队列,然后,对以v6为弧尾的边对应的弧头顶点进行松弛操作,发现v6出度为0,所以我们不用对dis数组做任何操作,其结果和上图一样,队列同样不用做任何操作。所以此时队列为空。
OK,队列循环结果,此时我们也得到了v1到各个顶点的最短路径的值了,它就是dis数组各个顶点对应的值,如下图:
伪代码:
q.push(s);vis[s]=1; void spfa() { while(!q.empty()) u=q.front();q.pop();vis[u]=0;//出队标记 for each v in adj(u) { if(dis[v]>d[u]+w[u][v]) { dis[v]=d[u]+w[u][v]; if(!vis[v]){q.push(v);vis[v]=1;} } } }
例题:RQONJ星门龙跃
#include<cstdio> #include<cstring> #include<queue> #define inf 0x3f3f3f3f #define maxm 30001 using namespace std; queue<int> q; struct edge{ int x,z,next; }e[150001]; int n,m,f[maxm],vis[maxm],tot=1,head[maxm]; void adde(int u,int x,int z) { e[tot].x=x; e[tot].z=z; e[tot].next=head[u]; head[u]=tot++; e[tot].x=u; e[tot].z=z; e[tot].next=head[x]; head[x]=tot++; } int main() { int a,b,c,p; memset(head,-1,sizeof(head)); memset(f,inf,sizeof(f)); memset(vis,0,sizeof(vis)); scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { scanf("%d%d%d",&a,&b,&c); adde(a,b,c); } f[1]=0; q.push(1); vis[1]=1; while(!q.empty()) { int u=q.front(); q.pop(); vis[u]=0; for(int k=head[u];k!=-1;k=e[k].next) { p=e[k].x; if(f[p]>f[u]+e[k].z) { f[p]=f[u]+e[k].z; if(!vis[p]) { q.push(p); vis[p]=1; } } } } printf("%d",f[n]); return 0; }
三种最短路算法
1、dijkstra(单源点最短路):贪心的思想,每次从集合U中找一个离源点最近的点加入到集合S中,并以新加入的点作为中间点松弛U中的点到源点的距离。直到U为空算法结束。也就是一个一个的求出最短路径
使用优先队列优化。队列中存储的是U中点的子集。
不能处理负权存在的情况。
复杂度远小于O(M*N),因为贪心所以速度三者中最快,用二项堆优化到 O(ElogV) 。
2、Bellman-Ford (单源点最短路):对所有边,进行k遍松弛操作,就会计算出与源点最多由k条边相连的点的最短路。 也就是不断刷新最短路
因为最短路一定不含环,所以最多包含n-1条边,那么我们进行n-1遍松弛操作就可以计算出所有点的最短路。
每次计算时,那些已经算出来最短路的点不用重复计算,可使用队列优化(SPFA)。
可含负边权。
复杂度为远小于O(M*N)
3、Floyd(多源点最短路):点i到j的最短路有两种情况,
1:i直接到j。
2:i经过k到j。
所以对于每个中间点k,枚举它的起点和终点,进行松弛操作,最终将得到所有点的最短路。
邻接矩阵存储,可含负边权。
复杂度O(N^3)。
谢谢大家,有错请再评论区指出