图算法:Bellman-Ford算法和SPFA优化

Bellman Ford 算法介绍

Bellman Ford算法解决的是一般情况下的单源最短路径问题,不同于Dijkstra算法,Bellman Ford算法允许边的权重为负数。给定带权重的有向图G =(V, E)和权重函数 w:E>R ,Bellman Ford算法返回一个布尔值,以表明是否存在一个从源结点可以到达的权重为负值的环路,如果存在,算法将告诉我们不存在解决方案,如果不存在,算法将给出最短路径和他们的权重。

1. 松弛操作

所谓松弛操作,就是对每一个结点 v ,估计源结点 s v 的最短路径。算法在松弛操作过程,对每个非源结点 v 记录它的前驱结点 pre[v] 。当从结点s到结点u之间的最短路径距离dis[u],加上结点u与结点v之间的边权重 edge(u,v).cost ,与当前的结点s到结点v的最短路径估计 dis[v] 比较,如果前者更小,那么对 dis[v] 更新,如此,松弛操作将产生一棵“最短路径树”。因为对每一个非源结点v来说,它到源结点的最短路径已经在这个过程被算出来了。

Question: 松弛为什么需要执行|V|-1外循环?

假定图G不包含从源结点s可以到达的权重的负值的环路,那么在算法的松弛操作执行 |V|1 次之后,对于所有从源结点s可以到的结点v,我们得到了v结点的最短路径。考虑从s可到达的结点v,设 P=(v0,v1,v2,v3,...,vk) 为任意一条最短路径, P 最多包含 |V|1 条边,因此 k<=|V|1 ,而松弛操作过程每次松弛了所有的 |E| 的边,这样重复 |V|1 就一定能够找出符合条件的最短路径 P

注意,每一次遍历,都可以从前一次遍历的基础上,找到此次遍历的部分点的单源最短路径。如:这是第i次遍历,那么,通过数学归纳法,若前面单源最短路径层次为1~(i-1)的点全部已经得到,而单源最短路径层次为i的点,必定可由单源最短路径层次为i-1的点集得到,从而在下一次遍历中充当前一次的点集,如此往复迭代,[v]-1次后,若无负权回路,则我们已经达到了所需的目的–得到每个点的单源最短路径。

2. 简单的C++实现

#include<iostream>

using namespace std;

#define MAX 0x3f3f3f3f
#define N 1010

int nodeNum, edgeNum, original; // 点, 边, 起点

struct Edge {
    int u, v;
    int cost;
}; 

Edge edge[N];
int dis[N], pre[N];

bool Bellman_Ford() {
    /* * 初始化 */ 
    for(int i = 1; i <= nodeNum; ++i)
        dis[i] = (i == orginal)? 0 : MAX;

    /* * 松弛操作 */ 
    for(int i = 1; i <= nodeNum - 1; ++i) {
        for(int j = 1; j <= edgeNum; ++j) {
            if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) {
                dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
                pre[edge[j].v] = edge[j].u;
            }
        }
    }
    /* * 判断是否有含负权的回路 */ 
    bool flag = true;
    for(int i = 1; i <= edgeNum; ++i) {
        if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost) {
            flag = false;
            break;
        }
    }
    return flag;
}
 /* * 打印出个各个点的最短路径 */
void print_shortestPath(int root) {
    while(root != pre[root]) {
        cout << root <<"->";
        root = pre[root];
    }
    if(root == pre[root])
        cout << root;
}

int main() {
    cin << nodeNum << edgeNum << original;
    pre[original] = original;

    for(int i = 1; i<= edgeNum; ++i) {
        cin >> edge[i].u >> edge[i].v >> edge[i].cost;
    }
    if(Bellman_Ford()) {
        for(int i = 1; i <= nodeNum; ++i) {
            cout << dis[i] << endl;
            cout << "shortest path: ";
            print_shortestPath(i); 
        }
    } else {
        cout << "There must be some negative circles" << endl;
    }
    return 0;
}

3. Bellman Ford算法的优化(SPFA,Shortest Path Faster Algorithm)

Shortest Path Faster Algorithm算法:

The Shortest Path Faster Algorithm (SPFA) is an improvement of the Bellman–Ford algorithm which computes single-source shortest paths in a weighted directed graph. The algorithm is believed to work well on random sparse graphs and is particularly suitable for graphs that contain negative-weight edges. However, the worst-case complexity of SPFA is the same as that of Bellman–Ford, so for graphs with nonnegative edge weights Dijkstra’s algorithm is preferred.The SPFA algorithm was published in 1994 by Fanding Duan. —— from WIKIPEDIA

伪代码
input G,v
for each u ∈ V(G)
     let dist[u] = ∞
let dist[v] = 0
let Q be an initially empty queue
push(Q,v)
while not empty(Q)
     let u = pop(Q)
     for each (u,w) ∈ E(G)
          if dist[w] > dist[u]+wt(u,w)
               dist[w] = dist[u]+wt(u,w)
               if w is not in Q
                    push(Q,w)

算法流程

算法大致流程是用一个队列来进行维护。 初始时将源加入队列。 每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。 直到队列为空时算法结束。
这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

SPFA——Shortest Path Faster Algorithm,它可以在O(kE)的时间复杂度内求出源点到其他所有点的最短路径,可以处理负边。SPFA的实现甚至比Dijkstra或者Bellman_Ford还要简单:设Dist代表S到I点的当前最短距离,Fa代表S到I的当前最短路径中I点之前的一个点的编号。开始时Dist全部为+∞,只有Dist[S]=0,Fa全部为0。

维护一个队列,里面存放所有需要进行迭代的点。初始时队列中只有一个点S。用一个布尔数组记录每个点是否处在队列中。

每次迭代,取出队头的点v,依次枚举从v出发的边v->u,设边的长度为len,判断Dist[v]+len是否小于Dist[u],若小于则改进Dist[u],将Fa[u]记为v,并且由于S到u的最短距离变小了,有可能u可以改进其它的点,所以若u不在队列中,就将它放入队尾。这样一直迭代下去直到队列变空,也就是S到所有的最短距离都确定下来,结束算法。若一个点入队次数超过n,则有负权环。

SPFA 在形式上和宽度优先搜索非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。设一个点用来作为迭代点对其它点进行改进的平均次数为k,有办法证明对于通常的情况,k在2左右。

SPFA算法(Shortest Path Faster Algorithm),也是求解单源最短路径问题的一种算法,用来解决:给定一个加权有向图G和源点s,对于图G中的任意一点v,求从s到v的最短路径。 SPFA算法是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算,他的基本算法和Bellman-Ford一样,并且用如下的方法改进: 1、第二步,不是枚举所有节点,而是通过队列来进行优化 设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。 2、同时除了通过判断队列是否为空来结束循环,还可以通过下面的方法: 判断有无负环:如果某个点进入队列的次数超过V次则存在负环(SPFA无法处理带负环的图)。—— From nocow.

C++ 实现
/* * 单源最短路算法SPFA,时间复杂度O(kE),k在一般情况下不大于2,对于每个顶点使用可以在O(VE)的时间内算出每对节点之间的最短路 * 使用了队列,对于任意在队列中的点连着的点进行松弛,同时将不在队列中的连着的点入队,直到队空则算法结束,最短路求出 * SPFA是Bellman-Ford的优化版,可以处理有负权边的情况 * 对于负环,我们可以证明每个点入队次数不会超过V,所以我们可以记录每个点的入队次数,如果超过V则表示其出现负环,算法结束 * 由于要对点的每一条边进行枚举,故采用邻接表时时间复杂度为O(kE),采用矩阵时时间复杂度为O(kV^2) */
#include<cstdio>
#include<vector>
#include<queue>
#define MAXV 10000
#define INF 1000000000 //此处建议不要过大或过小,过大易导致运算时溢出,过小可能会被判定为真正的距离

using std::vector;
using std::queue;

struct Edge{
    int v; //边权
    int to; //连接的点
};

vector<Edge> e[MAXV]; //由于一般情况下E<<V*V,故在此选用了vector动态数组存储,也可以使用链表存储
int dist[MAXV]; //存储到原点0的距离,可以开二维数组存储每对节点之间的距离
int cnt[MAXV]; //记录入队次数,超过V则退出
queue<int> buff; //队列,用于存储在SPFA算法中的需要松弛的节点
bool done[MAXV]; //用于判断该节点是否已经在队列中
int V; //节点数
int E; //边数

bool spfa(const int st){ //返回值:TRUE为找到最短路返回,FALSE表示出现负环退出
    for(int i=0;i<V;i++){ //初始化:将除了原点st的距离外的所有点到st的距离均赋上一个极大值
        if(i==st){
            dist[st]=0; //原点距离为0;
            continue;
        }
        dist[i]=INF; //非原点距离无穷大
    }
    buff.push(st); //原点入队
    done[st]=1; //标记原点已经入队
    cnt[st]=1; //修改入队次数为1
    while(!buff.empty()){ //队列非空,需要继续松弛
        int tmp=buff.front(); //取出队首元素
        for(int i=0;i<(int)e[tmp].size();i++){ //枚举该边连接的每一条边
            Edge *t=&e[tmp][i]; //由于vector的寻址速度较慢,故在此进行一次优化
            if(dist[tmp]+(*t).v<dist[(*t).to]){ //更改后距离更短,进行松弛操作
                dist[(*t).to]=dist[tmp]+(*t).v; //更改边权值
                if(!done[(*t).to]){ //没有入队,则将其入队
                    buff.push((*t).to); //将节点压入队列
                    done[(*t).to]=1; //标记节点已经入队
                    cnt[(*t).to]+=1; //节点入队次数自增
                    if(cnt[(*t).to]>V){ //已经超过V次,出现负环
                        while(!buff.empty())buff.pop(); //清空队列,释放内存
                        return false; //返回FALSE
                    }
                }
            }
        }
        buff.pop();//弹出队首节点
        done[tmp]=0;//将队首节点标记为未入队
    }
    return true; //返回TRUE
} //算法结束

int main(){ //主函数
    scanf("%d%d",&V,&E); //读入点数和边数
    for(int i=0,x,y,l;i<E;i++){
        scanf("%d%d%d",&x,&y,&l); //读入x,y,l表示从x->y有一条有向边长度为l
        Edge tmp; //设置一个临时变量,以便存入vector
        tmp.v=l; //设置边权
        tmp.to=y; //设置连接节点
        e[x].push_back(tmp); //将这条边压入x的表中
    }
    if(!spfa(0)){ //出现负环
        printf("出现负环,最短路不存在\n");
    }else{ //存在最短路
        printf("节点0到节点%d的最短距离为%d",V-1,dist[V-1]);
    }
    return 0;
}
    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/w_bu_neng_ku/article/details/78158009
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞