前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单源最短路径之Bellman-Ford算法

单源最短路径之Bellman-Ford算法

作者头像
每天学Java
发布2020-06-01 10:44:12
1.8K0
发布2020-06-01 10:44:12
举报
文章被收录于专栏:每天学Java

之前文章对于Dijkstra算法进行了讲解和实现,其实现的原理在于采用贪心算法,遍历N(结点数)次,每次找到局部最优的路径的结点u, 判断该节点可达的顶点v的权重是否大于结点u权重+u->v的权重,如果大于则替换顶点v的权重(也叫松弛操作)。因为Dijkstra算法无法 正确计算负权路径的最短路径(详情可看上一节),所以有了Bellman-Ford算法来解决这一问题。

贝尔曼-福特算法

贝尔曼-福特算法(Bellman-Ford)是由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的,求解单源最短路径问题的一种算法。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行V-1次松弛操作(V是顶点数量),得到所有可能的最短路径。其优于Dijkstra算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。

贝尔曼-福特算法与迪科斯彻算法类似,都以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。在两个算法中,计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。然而,迪科斯彻算法以贪心法选取未被处理的具有最小权值的节点,然后对其出边进行松弛操作;而贝尔曼-福特算法简单地对所有边进行松弛操作,共V-1次,其中V是图的顶点数量。在重复的计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。这样的策略使得贝尔曼-福特算法比迪科斯彻算法适用于更多种类的输入

实现过程

从上面介绍我们可以知道,Bellman算法对每一条边采用松弛操作,对于单源最短路径实现来说, 源点到某一顶点所经过的边最多为V-1条(可以看成树的两个结点),如下,三个顶点ABC,A->C最多走过2两条边即A->B->C

代码语言:javascript
复制
A->B
A->C
B->C

在我们将源点最短路径设置为0时,每次松弛操作,至少会松弛一条边,而最多共有N-1条边,所以我们需要遍历N-1次。每一次遍历的松弛操作和Dijkstra算法类似,判断结点u权重是否大于 v->u的权重+v的权重。

与Dijkstra算法使用最短边向其他顶点扩展方案不同,在Bellman-Ford算法中松弛操作是针对边,其目的是对每一条边进行松弛, 这样总能使得边达到最小,如下图解,A为源点

代码语言:javascript
复制
A->C 2

D->C 1

A->B -1

B->D 1

上面共计 4个顶点,源点A到达任意顶点B,C,D最高走V-1=3条边,也就是松弛3轮, 初始化时源点A到任意顶点设置为无穷大,s[]表示最短距离,S[A]=0。

那么第一轮:

A -> C 边权2 可以推导==> (S[C] = ∞) > (S[A] + W[AB] = 0+2)条件符合,所以进行松弛操作 S[C] = 2

D -> C 边权1 可以推导==> (S[C] = 2) > (S[D] + W[DC] = ∞+1)条件不符合,无法进行松弛操作

A -> B 边权-1 可以推导==> (S[B] = ∞) > (S[A] + W[AB] = 0+(-1))条件符合,所以进行松弛操作 S[B] = -1

B -> D 边权1 可以推导==> (S[D] = ∞) > (S[B] + W[BD] = -1+1)条件符合,所以进行松弛操作 S[D] = 0

然后进行第二轮(基于第一轮):

A -> C 边权2 可以推导==> (S[C] = 2) > (S[A] + W[AB] = 0+2)条件不符合,无法进行松弛操作

D -> C 边权1 可以推导==> (S[C] = 2) > (S[D] + W[DC] = 0+1)条件符合,所以进行松弛操作 S[C] = 1

A -> B 边权-1 可以推导==> (S[B] = -1) > (S[A] + W[AB] = 0+(-1))条件不符合,无法进行松弛操作

B -> D 边权1 可以推导==> (S[D] = 0) > (S[B] + W[BD] = -1+1)条件不符合,无法进行松弛操作

第三轮逻辑同上。

我们会发现其实第二轮的时候,已经实现最短路径了,第三轮属于没有用的遍历。

负环

负环,又叫负权回路,负权环,指的是一个图中存在一个环,里面包含的边的边权总和<0。在存在负环的图中,是求不出最短路径的, 因为每次要在这个环上遍历,最短路径就会无限次的变小。如下

代码语言:javascript
复制
A->B 1
A->C 1
C->B -1
B->D 1
D->C 1

那么BCD就会存在负环,按照上面N-1次遍历后,我们再遍历,最短路径仍会变小。

实现代码(C++)
代码语言:javascript
复制
//
//  main.cpp
//  Bellman-Ford
//
//  Created by 陈龙.
//  Copyright © 2019 陈龙. All rights reserved.
//

#include <iostream>

using namespace std;

//顶点和边和源点
int N,E,S;

 struct Graph{
    //起点,终点
    int u,v;
    int w;
//    Graph(int _u,int _v,int _w):u(_u),v(_v),w(_w){};
};



int main(int argc, const char * argv[]) {
    // insert code here...
    cin>>N>>E>>S;
    Graph g[N];
    int dis[N];
    int u,v,w;
    for (int i=0; i<E; i++) {
        cin>>u>>v>>w;
        g[i].u = u;
        g[i].v=v;
        g[i].w=w;
    }
    //初始化
    dis[S] = 0;
    for(int i=0;i<E;i++){
        if (i!=S) {
           dis[i] = INT_MAX;
        }
    }

    //两顶点最多N-1条边
    for (int i=0; i<N-1; i++) {
        for (int j=0; j<E; j++) {
            if (dis[g[j].v]> g[j].w + dis[g[j].u]) {
                dis[g[j].v] =g[j].w + dis[g[j].u];
            }
        }
    }
    // 判断是否有负环路
    for(int i=0; i<E; ++i){
        if(dis[g[i].v] > dis[g[i].u] + g[i].w){
               cout<<"有负环路"<<endl;
               break;
           }
        }
    
    for(int i=0;i<N;i++){
        cout<<dis[i]<<endl;
    }

    return 0;
}

    /*
     eg1:
     6 7 0
     0 1 -1
     0 2 2
     1 3 2
     2 3 -4
     2 4 -3
     4 5 5
     3 5 2
     eg2:
     3 3 0
     0 1 1
     0 2 2
     1 2 -1
     eg3
     3 4 1
     0 1 1
     2 0 1
     1 2 -1
     2 1 -2
     */
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 每天学Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 贝尔曼-福特算法
  • 实现过程
  • 负环
  • 实现代码(C++)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档