最短路径问题--SPFA算法
1、最短路径问题介绍
问题解释:
从图中的某个顶点出发到达另外一个顶点的所经过的边的权重和最小的一条路径,称为最短路径
解决问题的算法:
- 迪杰斯特拉算法(Dijkstra算法)
- 弗洛伊德算法(Floyd算法)
- SPFA算法
2、SPFA算法介绍
SPFA算法是求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达 O(VE)。但算法可以进行若干种优化,提高了效率。
算法的思路:
我们用数组dis记录每个结点的最短路径估计值,用邻接表或邻接矩阵来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止
我们要知道带有负环的图是没有最短路径的,所以我们在执行算法的时候,要判断图是否带有负环,方法有两种:
- 开始算法前,调用拓扑排序进行判断(一般不采用,浪费时间)
- 如果某个点进入队列的次数超过N次则存在负环(N为图的顶点数)
3、SPFA算法手动操作过程
下面我们采用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数组,得到如下结果:
此时只有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数组各个顶点对应的值,如下图:
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<queue> 5 #include<vector> 6 using namespace std; 7 #define INF 0x3f3f3f3f 8 #define LINF 0x3f3f3f3f3f3f3f3f 9 const int maxn=1e5+1; 10 typedef long long type_weight; 11 struct Edge 12 { 13 //根据题意定义weight的类型 14 //可能为long long 型,也可能为int型 15 //用typedef定义type_weight,方便修改类型 16 int vex; 17 type_weight weight; 18 Edge(int v=0,type_weight w=0):vex(v),weight(w){} 19 }; 20 vector<Edge>E[maxn]; 21 22 //向E[u]中加入(v,weight) : 边u与边v相连,其权值为weight 23 void addedge(int u,int v,type_weight weight) 24 { 25 E[u].push_back(Edge(v,weight)); 26 } 27 //visited[i] : 判断结点i是否在队列中 28 //dist[] : 存储最短距离,最好定义成long long 型 29 //cnt[] : 判断是否存在负环 30 bool visited[maxn]; 31 long long dist[maxn]; 32 int cnt[maxn]; 33 //求从start结点到其他结点的最短距离 34 //共n个结点 35 bool SPFA(int start,int n) 36 { 37 memset(visited,0,sizeof(visited)); 38 //初始化dist[]为LINF,即长整型的最大值(无穷大) 39 for(int i=1;i<=n;i++) 40 dist[i]=LINF; 41 dist[start]=0; 42 visited[start]=true; 43 44 queue<int >que; 45 while(!que.empty()) 46 que.pop(); 47 que.push(start); 48 49 while(!que.empty()) 50 { 51 int u=que.front(); 52 que.pop(); 53 54 //遍历以u为弧尾的所有结点 55 //E[u][i].v : 以u为弧尾的结点 56 //E[u][i].weight : 结点u与结点E[u][i].v之间的权重 57 for(int i=0;i<E[u].size();i++) 58 { 59 int v=E[u][i].vex; 60 61 //判断边u是否能松弛边v 62 if(dist[v] > dist[u]+E[u][i].weight) 63 { 64 dist[v]=dist[u]+E[u][i].weight; 65 if(!visited[v]) 66 { 67 que.push(v); 68 visited[v]=true; 69 //如果某个结点进入队列的此数超过n-1次,则此图存在负环 70 if(++cnt[v] > n) 71 return false; 72 } 73 } 74 } 75 } 76 return true; 77 }