最短路径问题
1.最短路径介绍
最短路径问题是图论研究中的一个经典算法问题, 旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。
算法具体的形式包括:
①确定起点的最短路径问题即已知起始结点,求最短路径的问题。
②确定终点的最短路径问题与确定起点的问题相反,该问题是已知终点结点,求最短路径的问题。
在无向图中该问题与确定起点的问题完全等同,
在有向图中该问题等同于把所有路径方向反转的确定起点的问题。
③确定起点终点的最短路径问题即已知起点和终点,求两结点之间的最短路径。
④全局最短路径问题求图中所有的最短路径。
用于解决最短路径问题的算法被称做“最短路径算法”, 有时被简称作“路径算法”。
最常用的路径算法有:
Bellman-Ford算法
SPFA算法
Dijkstra算法
Floyd-Warshall算法
2、常见算法
①Dijkstra算法
是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。
基本思想是:
将顶点集合V分成两个集合,一-类是生长点的集合S,包括源点和已经确定最短路径的顶点;另一类是非生长点的集合V(s), 包括所有尚未确定最短路径的顶点,并使用一个待定路径表,存储当前从源点v到每个非生长点vi的最短路径。初始时,S只包含源点v,对v∈V(s),待定路径表为从源点v到vi的有向边。然后在待定路径表中找到当前最短路径v,…
Vk, 将v加入集合S中,对vi∈V(s),将路径v, … vk, vi与待定路径表中从源点v到vi的最短路径相比较,取路径长度较小者为当前最短路径。重复上述过程,直到集合V中全部顶点加入到集合S中。
对图6-20(b)所示有向网执行Dijkstra 算法,求得从顶点vo到其余各顶点的最短路径,以及算法执行过程中数组dist 和path的变化状况,如表6-3所示。
代码:
#include<stdio.h>
#include<stdlib.h>
#define max1 10000000 //原词条这里的值太大,导致溢出,后面比较大小时会出错
int a[1000][1000];
int d[1000];//d表示源节点到该节点的最小距离
int p[1000];//p标记访问过的节点
int i, j, k;
int m;//m代表边数
int n;//n代表点数
int main()
{
scanf("%d%d",&n,&m);
int min1;
int x,y,z;
for(i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
a[x][y]=z;
a[y][x]=z;
}
for( i=1; i<=n; i++)
d[i]=max1;
d[1]=0;
for(i=1;i<=n;i++)
{
min1 = max1;
//下面这个for循环的功能类似冒泡排序,目的是找到未访问节点中d[j]值最小的那个节点,
//作为下一个访问节点,用k标记
for(j=1;j<=n;j++)
if(!p[j]&&d[j]<min1)
{
min1=d[j];
k=j;
}
//p[k]=d[k]; // 这是原来的代码,用下一 条代码替代。初始时,执行到这里k=1,而d[1]=0
//从而p[1]等于0,这样的话,上面的循环在之后的每次执行之后,k还是等于1。
p[k] = 1; //置1表示第k个节点已经访问过了
for(j=1;j<=n;j++)
if(a[k][j]!=0&&!p[j]&&d[j]>d[k]+a[k][j])
d[j]=d[k]+a[k][j];
}
//最终输出从源节点到其他每个节点的最小距离
for(i=1;i<n;i++)
printf("%d->",d[i]);
printf("%d\n",d[n]);
return 0;
}
堆优化的迪杰斯特拉算法
#include<bits/stdc++.h>
#define M(x,y) make_pair(x,y)
using namespace std;
int fr[100010],to[200010],nex[200010],v[200010],tl,d[100010];
bool b[100010];
void add(int x,int y,int w){
to[++tl]=y;
v[tl]=w;
nex[tl]=fr[x];
fr[x]=tl;
}
priority_queue< pair<int,int> > q;
int main(){
int n,m,x,y,z,s;
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
for(int i=1;i<=n;i++) d[i]=1e10;
d[s]=0;
q.push(M(0,s));
while(!q.empty()){
int x=q.top().second;
q.pop();
if(b[x]) continue;
b[x]=1;
for(int i=fr[x];i;i=nex[i]){
int y=to[i],l=v[i];
if(d[y]>d[x]+l){
d[y]=d[x]+l;
q.push(M(-d[y],y));//懒得重载运算符
}
}
}
for(int i=1;i<=n;i++) printf("%d ",d[i]);
return 0;
}
②Bellman-Ford算法
贝尔曼-福特算法(Bellman-Ford)是求解单源最短路径问题的一种算法。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。
Floyd算法用于求每一对顶点之间的最短路径问题,问题描述如下:给定带权有向图G=(V, E),对任意顶点vi和vj (i≠j) ,求顶点Vi到顶点Vj的最短路径。
Floyd算法的基本思想是:
假设从v;到v;的弧(若从v;到v;的弧不存在,则将其弧的
权值看成∞)是最短路径,然后进行n次试探。首先比较vi, Vj和vi; vo, v;的路径长度,取长度较短者作为从vi到vj中间顶点的编号不大于0的最短路径。在路径上再增加一个顶点vI,将vi,…vI,…vj和已经得到的从vi到vj中间顶点的编号不大于0的最短路径相比较,取长度较短者作为中间顶点的编号不大于1的最短路径。
依此类推,在一般情况下,若vi,…,vk和Vk,…vj分别是从vi到vk和从Vk到vj中间顶点的编号不大于k-1的最短路径,则将vi,…,vk,…,vj和已经得到的从vi到vj中间顶点的编号不大于k-1的最短路径相比较,取长度较短者为从vi到vj中间顶点的编号不大于k的最短路径。经过n次比较后,最后求得的必是从Vi到vj的最短路径。
伪代码表示
procedure BellmanFord(list vertices, list edges, vertex source)
// 该实现读入边和节点的列表,并向两个数组(distance和predecessor)中写入最短路径信息
// 步骤1:初始化图
for each vertex v in vertices:
if v is source then distance[v] := 0
else distance[v] := infinity
predecessor[v] := null
// 步骤2:重复对每一条边进行松弛操作
for i from 1 to size(vertices)-1:
for each edge (u, v) with weight w in edges:
if distance[u] + w < distance[v]:
distance[v] := distance[u] + w
predecessor[v] := u
// 步骤3:检查负权环
for each edge (u, v) with weight w in edges:
if distance[u] + w < distance[v]:
error "图包含了负权环"
③*SPFA算法
SPFA(最短路径快速算法,英语:Shortest Path Faster Algorithm)是一个用于求解有向带权图单源最短路径的改良的贝尔曼-福特算法。这一算法被认为在随机的稀疏图上表现出色,并且极其适合带有负边权的图。然而SPFA在最坏情况的时间复杂度与贝尔曼-福特算法相同,因此在非负边权的图中仍然最好使用迪杰斯特拉算法(Dijkstra)。
SPFA算法的基本思路与贝尔曼-福特算法相同,即每个节点都被用作用于松弛其相邻节点的备选节点。相较于贝尔曼-福特算法,SPFA算法的提升在于它并不盲目尝试所有节点,而是维护一个备选节点队列,并且仅有节点被松弛后才会放入队列中。整个流程不断重复直至没有节点可以被松弛。
下面我们采用SPFA算法对下图求v1到各个顶点的最短路径:
初始化:
首先我们先初始化数组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数组,得到如下结果:
此时只有v4对应的值被更新了,而且v4不在队列中,则把它加入到队列中:{v5,v6,v4}
第三次循环
此时,队首元素为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数组各个顶点对应的值,如下图:
int SPFA(int s) {
queue<int> q;
bool inq[maxn] = {false};
for(int i = 1; i <= N; i++) dis[i] = 2147483647;
dis[s] = 0;
q.push(s); inq[s] = true;
while(!q.empty()) {
int x = q.front(); q.pop();
inq[x] = false;
for(int i = front[x]; i !=0 ; i = e[i].next) {
int k = e[i].v;
if(dis[k] > dis[x] + e[i].w) {
dis[k] = dis[x] + e[i].w;
if(!inq[k]) {
inq[k] = true;
q.push(k);
}
}
}
}
for(int i = 1; i <= N; i++) cout << dis[i] << ' ';
cout << endl;
return 0;
}
松弛
每次松弛操作实际上是对相邻节点的访问,第n次松弛操作保证了所有深度为n的路径最短。由于图的最短路径最长不会经过超过|V|-1条边,所以可知贝尔曼-福特算法所得为最短路径。
负边权操作
与迪杰斯特拉算法不同的是,迪杰斯特拉算法的基本操作“拓展”是在深度上寻路,而“松弛”操作则是在广度上寻路,这就确定了贝尔曼-福特算法可以对负边进行操作而不会影响结果。
正权图使用dijkstra算法,负权图使用SPFA算法
负权环判定
因为负权环可以无限制的降低总花费,所以如果发现第
次操作仍可降低花销,就一定存在负权环。
④Floyd - Warshall(弗洛伊德算法)
模板
视频资源:
https://www.bilibili.com/video/BV1LE411R7CS?from=search&seid=1868101711902580696
参考博客:
https://blog.csdn.net/qq_35644234/article/details/61614581