前面讲了单源最短路径的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;
}