前面讲了单源最短路径的Dijkstra算法和任意两点间最短路径的Floyd算法,今天我们来看一下求单源最短路径的另外两种常用的算法:bellman-ford算法和SPFA算法。至于为什么要把这两个放在一起呢,比较SPFA算法是对bellman-ford算法的改进和优化。
我们先来看一下bellman-ford算法:其实bellman-ford算法和Dijkstra算法是有相似之处的,之所以提出bellman-ford算法,是为了解决带有负权值的最短路径问题。bellman-ford算法的效率很低,他是通过对各边不断的进行松弛,从而达到更新源点到各点的最短距离的;对于一个有n个顶点的图来说,bellman-ford算法的松弛操作需要进行n-1遍,每次松弛都会伴随着最短距离的更新,如果n-1遍松弛之后还能更新最短距离,那么就说明图中存在负权值回路了(拿一个三角形的负权值回路,画一下就能明白了)。
bellman-ford算法的算法流程如下:
(1)初始化:将距离数组dis初始化为源点到该点的权值(也可以初始化为+∞),然后更新到源点dis为0;
(2)松弛更新操作:进行n-1遍松弛(n为图中顶点的个数),每次松弛都更新所有的边;
(3)判负环:若松弛操作之后某条边仍然可以更新,那么就说明图中存在负权值回路。
实现代码如下:
#include <iostream> #include <cstdio> #include <cstring> using namespace std; #define INF 999999999 #define MAX 1000 typedef struct node { int u,v,w; }EDGE; EDGE edge[MAX]; int dis[MAX],pre[MAX]; int n,m_edge; void add(int u,int v,int w) { m_edge++; edge[m_edge].u=u; edge[m_edge].v=v; edge[m_edge].w=w; } bool bellman_ford() { for(int i=0;i<=n;i++) dis[i]=INF; dis[1]=0; for(int i=0;i<n;i++) for(int j=1;j<=m_edge;j++) if(dis[edge[j].v]>dis[edge[j].u]+edge[j].w) { dis[edge[j].v]=dis[edge[j].u]+edge[j].w; pre[edge[j].v]=edge[j].u; } bool flag=true; for(int i=1;i<=m_edge;i++) if(dis[edge[i].v]>dis[edge[i].u]+edge[i].w) { flag=false; break; } return flag; } void print_path(int x) { while(x!=pre[x]) { printf("%d-->",x); x=pre[x]; } if(x==pre[x]) printf("%d\n",x); } int main() { int m; int u,v,w; m_edge=0; for(int i=0;i<m;i++) { scanf("%d%d%d",&u,&v,&w); add(u,v,w);//有向图 add(v,u,w); } if(bellman_ford()) for(int i=0;i<n;i++)//源点到每个点的最短路径 { printf("%d\n",dis[i]); printf("path:"); print_path(i); } else printf("存在负权值回路\n"); return 0; }
接下来我们看一下SPFA算法:
SPFA(Shortest Path Faster Algorithm)是对bellman-ford算法的一种队列优化。算法大致流程就是用一个队列来进行维护图中的n个顶点,初始时讲源点加入队列,每次从队列中取一个元素,并对所有与它相邻的点进行松弛更新,更新完后该点出队,在这个过程中,若某个点更新了,就把这个点入队,直到队列中的元素为0,算法结束。
SPFA算法可以在O(KE)的时间复杂度内求出源点到其他点的最短路径,可以处理负权边,某些情况甚至比Dijkstra算法更有效,但稳定性不如Dijkstra算法。
算法流程如下:
(1)初始化:将源点到每一点的距离dis初始化为+∞,将标记数组vis初始化为false,表示所有点都没有入队;更新源点的dis为0,同时将源点入队,改变vis为true;
(2)松弛更新操作:每次取队头元素tmp(更新vis[ tmp ]为false),枚举从tmp出发的每一条边,如果某条边能够松弛且该点没有在队列中,那么将该点入队;直至队列为空;
(3)判负环:如果图中某点入队的次数超过n,那么图中存在负环(SPFA无法处理带负环的问题)。
实现代码如下,和BFS有点相似:
邻接矩阵实现代码:
int n,m;//n代表顶点数,m表示边数 int w[MAX][MAX];//邻接矩阵表示图 int dis[MAX];//源点到各点的最短路 bool vis[MAX];//标记数组 void SPFA() { for(int i=0;i<n;i++) { dis[i]=INF; vis[i]=false; } queue<int> que; que.push(start); dis[start]=0; vis[start]=true; while(!que.empty()) { int tmp=que.front(); que.pop(); vis[tmp]=false; for(int i=0;i<n;i++) if(dis[i]>dis[tmp]+w[tmp][i]) { dis[i]=dis[tmp]+w[tmp][i]; if(!vis[i]) { que.push(i); vis[i]=true; } } } }
对于图中边数和顶点数比较大的情况,邻接矩阵显然不能满足我们的要求,这里用vector容器来装某一顶点相邻的边的信息,实现代码如下:
#include <cstdio> #include <iostream> #include <queue> #include <vector> using namespace std; #define INF 0x7fffffff #define MAX 100005 struct edge { int to,w; }; int dis[MAX]; bool vis[MAX]; int n,m,start,e; vector <edge> vec[MAX]; void SPFA() { for(int i=1;i<=n;i++) { dis[i]=INF; vis[i]=false; } dis[start]=0; queue<int> que; que.push(start); vis[start]=true; while(!que.empty()) { int tmp=que.front(); que.pop(); vis[tmp]=false; for(int i=0;i<vec[tmp].size();i++) { edge cnt=vec[tmp][i]; if(dis[ cnt.to ]>dis[tmp]+cnt.w) { dis[ cnt.to ]=dis[tmp]+cnt.w; if(!vis[ cnt.to ]) { que.push(cnt.to); vis[ cnt.to ]=true; } } } } } int main() { cin>>n>>m>>start>>e; for(int i=0;i<=n;i++) vec[i].clear(); for(int i=1;i<=m;i++) { edge cnt1,cnt2; int u,v,w; scanf("%d%d%d",&u,&v,&w); cnt1.to=u;cnt1.w=w; cnt2.to=v;cnt2.w=w; vec[u].push_back(cnt2); vec[v].push_back(cnt1); } SPFA(); cout<<dis[e]<<endl; return 0; }