写给媳妇儿的算法(十三)——贝尔曼-福德算法

我们知道,狄克斯特拉算法可以用于寻找权值为正的有向无环图的最短路径。如果我们遇到图中某些边的权值为负,我们应该如何寻找最短路径呢?贝尔曼福德算法就是一种思路。

如果说狄克斯特拉算法是用一种“广度”的方式,每次寻找源点所能到达的距离最短的一个点,更新其邻居节点的距离(拓展)。那么贝尔曼福德算法就是用一种“深度”的方式,每次找到所有的路径,进行“松弛”操作(更新最短的路径)最终找到最短路径。

算法过程

松弛操作

贝尔曼福德算法是基于“松弛”操作来进行的,我们首先来看一下什么是松弛操作。其实表述起来也很简单,就是:

  1. 有两个点A、B都可以从源点经由某条路径到达,假设源点经由某条路径到A点的距离是M,源点经某条路径到达B点的距离是N;
  2. A点可直接到达B点,并且A点到B点的距离是L。
  3. 如果有 M(源点到A的距离) + L(A到B的距离) < N(源点到B的距离),那么就说明有一条新的从源点到B的路线(途径A点)的距离要小于之前源点到B点的距离。此时我们更新源点到B的距离为 M + L。
    这样下来,源点到B点的最小距离就被缩短了,也就是说我们找到了一条更近的能够从源点到达B点的路(经由A点)。这个路线的距离被缩短了,这个过程就是 “松弛” 的过程。
“松弛”过程.png

基于“深度”的松弛操作

对最短路径的一个认知

首先,我们要想清楚一件事,那就是:含有N个顶点的图,某两个顶点之间最短路径的深度不会超过N-1

这句话的意思是,如果我们找到的是一条最短的路径,那么这条最短的路径包含的边肯定小于N-1条;最短路径最多边的情况就是经历了所有的节点,最终到达终点,此时路径的变数为N-1条。

为什么是这样呢?因为如果不是这样的话,那么图中肯定存在一个 环路

存在环路的路径.png

我们看图,图中存在一条环路,B => C。如果我们从A出发最终到达D,那么我们的最短路径是什么呢?很容就能看到,最短路径是 A => B => D。如果我们经过一次B、C之间的环路,那么路径就会变为 A => B => C => B => D。此时路径长度就会增加B、C之间距离的2倍,就是2。也就是说,这种情况下,每每经过一次环路,路径的总长度就会增加2。

如果我们整个路径中没有环路,也就是没有两个点间互相到达的这种情况,那么我们的路径中,肯定就不会存在某个顶点重复出现的情况(刚才 A=>B=>C=>B=>D中存在环路,B就重复出现了),所以最多我们就是经历了所有的顶点:N个。这N个顶点可以确定N-1条边。所以:含有N个顶点的图,某两个顶点之间最短路径的深度不会超过N-1

基于深度进行松弛操作

为什么说是基于深度进行的松弛操作呢?我们依然使用狄克斯特拉算法时候的路径图来证明这一点。另外,我们还需要两个图表(与狄克斯特拉算法的相同):距离表、父节点表。

  1. 最初的时候,这个表中的值如下:
初始化的3个图表.png

我们得到了一张从Start开始,第一步只能到达A、B两点的初始化的距离表,也就是说,此时到达A、B的距离是已知的,因为可以由Start直接到达; 得到一个只有Start点作为父节点的两个点A、B的父地点表;还有一个全部路径的路径图。

  1. 我们遍历路径表中的 相邻两点之间的全部路径 一次,根据距离表更新每个点的到达距离。也就是对图进行一次松弛操作:
松弛操作.png

我们可以看到,初始化的时候,Start确定的是只能直接到达A、B两点。我们遍历所有的 两点之间 的路径(比如A、C之间的这条 A => C路径,起点为:A;终点为:C;权值为:2)。我们根据距离表,知道了起点到A的距离是5,到B的距离是3。由于A能到达C、D(存在路径:A => C 、A => D),所以,此时通过A到达C、D的距离就是(B点同理):

对于经过A直接到达的点:
C的距离 = 5(Start到A的距离)+ 2(A到C的距离,也就是 A => C 的权值)= 7
D的距离 = 5(Start到A的距离)+ 2(A到C的距离,也就是 A => C 的权值)= 7

对于经过B直接到达的点:
C的距离 = 3 (Start到B的距离)+ 1(B到C的距离,也就是 B=> C 的权值)= 4
E的距离 = 3 (Start到B的距离)+ 6(B到E的距离,也就是 B=> C 的权值)= 9

我们依次遍历所有相邻两点之间的路径,遍历到 A => C 的时候,我们得到Start到C的距离是 5 + 2 = 7,将其写入距离表中;但是之后遍历到 B => C 的时候,我们又得到了Start到C的距离是 3 + 1 = 4 < 7,所以我们更改距离表,到达C的距离就更新为了4。遍历完所有路径后(松弛之后),我们更新了图中的距离表,就是现在这个样子。同样,我们将记录进距离表的点的相应的父节点也写进父地点表。

我们可以对比第一张图来看,此时我们做的操作,其实就是基于深度(浅色标线)的一次松弛操作。

  1. 继续2中的操作过程,我们再一次遍历全部的相邻两点之间的路径,我们可以得到如下三张图:
再次松弛.png

同样的操作,我们继续遍历所有相邻两点之间的路径(包括之前已经记录过距离的点,每次都遍历全部,因为不知道之前遍历过的会不会被再次更新,再次松弛)。所以这次我们松弛的边分别是(这个图来说之前两部遍历过后,有一些边再松弛权重已经不会改变):

D => C、D => End,C => End、E => End

D => C : 查表得知到D的距离是7,D到C的距离是3,7 + 3 = 10 > 4(表中C的距离),所以C的距离不更新;
D => End: 查表知到D的距离是7,D到End的距离是4,7 + 4 = 11,表中无End,所以加入End = 11, 父地点为D;
C => End: 查表知到C的距离是4,C到End的距离是5,4 + 5 = 9 < 11(D=>End写入的),所以更新End的距离为 9, 父节点为C;
E => End: 查表知到E的距离是9, E到End的距离是1,9 + 1 = 10 > 9 (C => End更新过的),所以不更新距离表和父节点表。

到此更新完毕了。我们得到了最终的距离表和父地点表。这样就得到了结论,从Start到到End的最小距离是9,路径是:End => C(End的父节点) => B(C的父节点) => Start(B的父节点) 的反转 Start => B => C => End。

这张图比较简单,我们只是初始化了一次各个表格,3次遍历(初始化算一次)所有的相邻节点之间的路径就得到了最终的路径。但是根据上面我们 对最短路径的一个认知 可以知道,并不是所有的节点分布都如此,但是,最短路径的深度最多就是 N(节点个数) - 1,所以我们最多需要遍历 N - 1 次。

算法可以用于含有负权值边的图

狄克斯特拉算法实际是依赖广度来进行拓展。依赖广度来进行拓展就会造成已经完成任务的顶点就不能再改变其距离,最终是通过累加的方式,算出所有顶点到达源点的最小路径,只能逐渐增加总权值,负值的存在会减小总权值,可能会造成某条路径上的顶点从原来的不是当前距离表中的最小值变成最小值。

由于贝尔曼福德算法是通过N-1次遍历,每次都遍历所有的两个相邻顶点之间的路径。所以是有穷次的单纯遍历去“松弛”,相当于在深度上去松弛,每次都会再次遍历所有的路径进行松弛,简单粗暴,所以路径上某个顶点距离的变化都是动态的来变化,因为每次遍历只在一个深度,并不能确定下一个深度遍历过后,某个顶点的到达距离是会变大还是缩小。所以不管放大还是缩小都是可以的,也就是不管路径的权值是正还是负都是可以的。

去除负权环

上面的过程实际上还没有结束,我们还需要思考一个问题:如果图中存在负权环会怎么样?答:存在负权环的话会使最终某几个顶点的到达距离值不收敛于一个小的值,每次更新都不断的减小,无限制的减小。

图中负权环的部分.png

如图,如果图中某一部分如图中所示,是一个包含有负权值的一个环路,那么我们第一次遍历所有相邻接点之间的路径进行松弛的时候,各个顶点的距离会发生变化(假设遍历顺序从 C => A 这条路径开始):

遍历一次之后.png

我们可以看到,遍历一次之后,环中所有的顶点的距离值都变小了!如果图中的所有节点个数有N个,根据上面,我们会进行N-1次循环,那么在这N-1次循环中,这个环中的节点的距离每次都会减小,总的距离会无限减小,以至于最终找不到最短的路径(每次都减短,无限循环的话就无限减短,这肯定是不对的)。

所以我们要检查图中有没有负权环。检查的方法就是,在经历过之前N-1次循环之后,我们再次检查一遍所有的相邻节点之间的路径。如果不存在负权环,那么N-1次之后我们肯定找到了最短路径,那么我们此时图中的所有的相邻节点之间的路径都会满足(比如 C => A):

C的距离 + C到A的距离 >= A的距离(如果C => A 是最短路径的一部分取=,如果不是就取 >)

如果存在负权环,导致的是这个环中所有的顶点的到达距离会无限减小。我们N-1次循环是有限的,但是N-1次循环之后,再次进行检查时(多加一次第N次循环),距离还是可以减小,也就是(比如C => A):

C的距离 + C到A的距离 < A的距离(这个言外之意就是还能优化,我们知道经历N-1次循环之后已经找到最短路径了,这里还能再找,也就是无限减小最短路径的距离)

所以出现这种情况,我们就可以判定,这个图中存在负权环。如果存在负权环,那么我们经历N-1次循环之后我们得到的最短路径就不一定是最短的路径了。所以只有在经历过N-1次遍历松弛之后,再进行负权环的判定,如果没有负权环的存在,那么我们得到的最短路径,就是正确的。

算法实现

# -*- coding: utf-8 -*-
# @Time    : 2019-01-02 12:27
# @Author  : Jaedong
# @Version : 3.6
# @File    : Bellman-Ford_algorithm.py
# @Software: PyCharm


# 定义一个表示边的类
class Edge:
    u = ''      # 边的起点
    v = ''      # 边的终点
    weight = 0  # 边的权重

    def __init__(self, u, v, weight):
        self.u = u
        self.v = v
        self.weight = weight

# 贝尔曼福德算法的主体
def bellman_ford_algorithm(vertices, edges, start, end):
    distances = {}
    parents = {}

    # 初始化参数
    for v in vertices:
        if v == start:
            distances[v] = 0
        else:
            distances[v] = float('inf')

    # 遍历并且松弛每一条边
    for i in range(0, len(vertices) ):
        for edge in edges:
            if distances[edge.u] + edge.weight < distances[edge.v]:
                distances[edge.v] = distances[edge.u] + edge.weight
                parents[edge.v] = edge.u

    # 检查是否有负权环
    for edge in edges:
        if distances[edge.u] + edge.weight < distances[edge.v]:
            return  None, None

    # 更新了所有节点的到达最小距离,那么到达end的最小距离就是distances表中end所对应的值
    distance = distances[end]
    # 通过不断的回溯的方式,得到end到达start的路径,这样,就得到了start到达end的路径
    parent = parents[end]
    paths = [parent, end]
    while parent is not start:
        paths.insert(0, parents[parent])
        parent = parents[parent]

    return distance, paths


# 所有图中的顶点
vertex = ['Start', 'A', 'B', 'C', 'D', 'E', 'End']
# 所有图中的边
edges = [Edge('Start', 'A', 5), Edge('Start', 'B', 3),
         Edge('A', 'C', 2), Edge('A', 'D', 2),
         Edge('B', 'C', 1), Edge('B', 'E', 6),
         Edge('C', 'End', 5),
         Edge('D', 'C', 3), Edge('D', 'End', 4),
         Edge('E', 'End', 1)]

distance, paths = bellman_ford_algorithm(vertex, edges, 'Start', 'End')
print('\n从 Start 到 End 的最短的距离是:{}, 路径是: {}'.format(distance, ' ==> '.join(paths)))

结果是:

找到最短路径.png



上一篇:写给媳妇儿的算法(十二)——狄克斯特拉算法
下一篇:写给媳妇儿的算法(十四)——动态规划

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容