单源最短路径:bellman-ford算法和SPFA算法

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


你可能感兴趣的:(单源最短路径:bellman-ford算法和SPFA算法)