寫(xiě)給媳婦兒的算法(十三)——貝爾曼-福德算法

我們知道,狄克斯特拉算法可以用于尋找權(quán)值為正的有向無(wú)環(huán)圖的最短路徑坏逢。如果我們遇到圖中某些邊的權(quán)值為負(fù),我們應(yīng)該如何尋找最短路徑呢?貝爾曼福德算法就是一種思路。

如果說(shuō)狄克斯特拉算法是用一種“廣度”的方式惹谐,每次尋找源點(diǎn)所能到達(dá)的距離最短的一個(gè)點(diǎn),更新其鄰居節(jié)點(diǎn)的距離(拓展)驼卖。那么貝爾曼福德算法就是用一種“深度”的方式氨肌,每次找到所有的路徑,進(jìn)行“松弛”操作(更新最短的路徑)最終找到最短路徑酌畜。

算法過(guò)程

松弛操作

貝爾曼福德算法是基于“松弛”操作來(lái)進(jìn)行的擅这,我們首先來(lái)看一下什么是松弛操作郭卫。其實(shí)表述起來(lái)也很簡(jiǎn)單,就是:

  1. 有兩個(gè)點(diǎn)A、B都可以從源點(diǎn)經(jīng)由某條路徑到達(dá)结笨,假設(shè)源點(diǎn)經(jīng)由某條路徑到A點(diǎn)的距離是M宰衙,源點(diǎn)經(jīng)某條路徑到達(dá)B點(diǎn)的距離是N旅薄;
  2. A點(diǎn)可直接到達(dá)B點(diǎn)搁进,并且A點(diǎn)到B點(diǎn)的距離是L。
  3. 如果有 M(源點(diǎn)到A的距離) + L(A到B的距離) < N(源點(diǎn)到B的距離)缎罢,那么就說(shuō)明有一條新的從源點(diǎn)到B的路線(途徑A點(diǎn))的距離要小于之前源點(diǎn)到B點(diǎn)的距離伊群。此時(shí)我們更新源點(diǎn)到B的距離為 M + L。
    這樣下來(lái)策精,源點(diǎn)到B點(diǎn)的最小距離就被縮短了舰始,也就是說(shuō)我們找到了一條更近的能夠從源點(diǎn)到達(dá)B點(diǎn)的路(經(jīng)由A點(diǎn))。這個(gè)路線的距離被縮短了蛮寂,這個(gè)過(guò)程就是 “松弛” 的過(guò)程蔽午。
“松弛”過(guò)程.png

基于“深度”的松弛操作

對(duì)最短路徑的一個(gè)認(rèn)知

首先,我們要想清楚一件事酬蹋,那就是:含有N個(gè)頂點(diǎn)的圖及老,某兩個(gè)頂點(diǎn)之間最短路徑的深度不會(huì)超過(guò)N-1抽莱。

這句話的意思是,如果我們找到的是一條最短的路徑骄恶,那么這條最短的路徑包含的邊肯定小于N-1條食铐;最短路徑最多邊的情況就是經(jīng)歷了所有的節(jié)點(diǎn),最終到達(dá)終點(diǎn)僧鲁,此時(shí)路徑的變數(shù)為N-1條虐呻。

為什么是這樣呢?因?yàn)槿绻皇沁@樣的話寞秃,那么圖中肯定存在一個(gè) 環(huán)路

存在環(huán)路的路徑.png

我們看圖斟叼,圖中存在一條環(huán)路,B => C春寿。如果我們從A出發(fā)最終到達(dá)D朗涩,那么我們的最短路徑是什么呢?很容就能看到绑改,最短路徑是 A => B => D谢床。如果我們經(jīng)過(guò)一次B、C之間的環(huán)路厘线,那么路徑就會(huì)變?yōu)?A => B => C => B => D识腿。此時(shí)路徑長(zhǎng)度就會(huì)增加B、C之間距離的2倍造壮,就是2渡讼。也就是說(shuō),這種情況下费薄,每每經(jīng)過(guò)一次環(huán)路硝全,路徑的總長(zhǎng)度就會(huì)增加2栖雾。

如果我們整個(gè)路徑中沒(méi)有環(huán)路楞抡,也就是沒(méi)有兩個(gè)點(diǎn)間互相到達(dá)的這種情況,那么我們的路徑中析藕,肯定就不會(huì)存在某個(gè)頂點(diǎn)重復(fù)出現(xiàn)的情況(剛才 A=>B=>C=>B=>D中存在環(huán)路召廷,B就重復(fù)出現(xiàn)了),所以最多我們就是經(jīng)歷了所有的頂點(diǎn):N個(gè)账胧。這N個(gè)頂點(diǎn)可以確定N-1條邊竞慢。所以:含有N個(gè)頂點(diǎn)的圖,某兩個(gè)頂點(diǎn)之間最短路徑的深度不會(huì)超過(guò)N-1治泥。

基于深度進(jìn)行松弛操作

為什么說(shuō)是基于深度進(jìn)行的松弛操作呢筹煮?我們依然使用狄克斯特拉算法時(shí)候的路徑圖來(lái)證明這一點(diǎn)。另外居夹,我們還需要兩個(gè)圖表(與狄克斯特拉算法的相同):距離表败潦、父節(jié)點(diǎn)表本冲。

  1. 最初的時(shí)候,這個(gè)表中的值如下:
初始化的3個(gè)圖表.png

我們得到了一張從Start開(kāi)始劫扒,第一步只能到達(dá)A檬洞、B兩點(diǎn)的初始化的距離表,也就是說(shuō)沟饥,此時(shí)到達(dá)A添怔、B的距離是已知的,因?yàn)榭梢杂蒘tart直接到達(dá)贤旷; 得到一個(gè)只有Start點(diǎn)作為父節(jié)點(diǎn)的兩個(gè)點(diǎn)A广料、B的父地點(diǎn)表;還有一個(gè)全部路徑的路徑圖幼驶。

  1. 我們遍歷路徑表中的 相鄰兩點(diǎn)之間的全部路徑 一次性昭,根據(jù)距離表更新每個(gè)點(diǎn)的到達(dá)距離。也就是對(duì)圖進(jìn)行一次松弛操作:
松弛操作.png

我們可以看到县遣,初始化的時(shí)候糜颠,Start確定的是只能直接到達(dá)A、B兩點(diǎn)萧求。我們遍歷所有的 兩點(diǎn)之間 的路徑(比如A其兴、C之間的這條 A => C路徑,起點(diǎn)為:A夸政;終點(diǎn)為:C元旬;權(quán)值為:2)。我們根據(jù)距離表守问,知道了起點(diǎn)到A的距離是5匀归,到B的距離是3。由于A能到達(dá)C耗帕、D(存在路徑:A => C 穆端、A => D),所以仿便,此時(shí)通過(guò)A到達(dá)C体啰、D的距離就是(B點(diǎn)同理):

對(duì)于經(jīng)過(guò)A直接到達(dá)的點(diǎn):
C的距離 = 5(Start到A的距離)+ 2(A到C的距離,也就是 A => C 的權(quán)值)= 7
D的距離 = 5(Start到A的距離)+ 2(A到C的距離嗽仪,也就是 A => C 的權(quán)值)= 7

對(duì)于經(jīng)過(guò)B直接到達(dá)的點(diǎn):
C的距離 = 3 (Start到B的距離)+ 1(B到C的距離荒勇,也就是 B=> C 的權(quán)值)= 4
E的距離 = 3 (Start到B的距離)+ 6(B到E的距離,也就是 B=> C 的權(quán)值)= 9

我們依次遍歷所有相鄰兩點(diǎn)之間的路徑闻坚,遍歷到 A => C 的時(shí)候沽翔,我們得到Start到C的距離是 5 + 2 = 7,將其寫(xiě)入距離表中窿凤;但是之后遍歷到 B => C 的時(shí)候仅偎,我們又得到了Start到C的距離是 3 + 1 = 4 < 7西潘,所以我們更改距離表,到達(dá)C的距離就更新為了4哨颂。遍歷完所有路徑后(松弛之后)喷市,我們更新了圖中的距離表,就是現(xiàn)在這個(gè)樣子威恼。同樣品姓,我們將記錄進(jìn)距離表的點(diǎn)的相應(yīng)的父節(jié)點(diǎn)也寫(xiě)進(jìn)父地點(diǎn)表。

我們可以對(duì)比第一張圖來(lái)看箫措,此時(shí)我們做的操作腹备,其實(shí)就是基于深度(淺色標(biāo)線)的一次松弛操作。

  1. 繼續(xù)2中的操作過(guò)程斤蔓,我們?cè)僖淮伪闅v全部的相鄰兩點(diǎn)之間的路徑植酥,我們可以得到如下三張圖:
再次松弛.png

同樣的操作,我們繼續(xù)遍歷所有相鄰兩點(diǎn)之間的路徑(包括之前已經(jīng)記錄過(guò)距離的點(diǎn)弦牡,每次都遍歷全部友驮,因?yàn)椴恢乐氨闅v過(guò)的會(huì)不會(huì)被再次更新,再次松弛)驾锰。所以這次我們松弛的邊分別是(這個(gè)圖來(lái)說(shuō)之前兩部遍歷過(guò)后卸留,有一些邊再松弛權(quán)重已經(jīng)不會(huì)改變):

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,表中無(wú)End姓言,所以加入End = 11瞬项, 父地點(diǎn)為D;
C => End: 查表知到C的距離是4何荚,C到End的距離是5,4 + 5 = 9 < 11(D=>End寫(xiě)入的)猪杭,所以更新End的距離為 9餐塘, 父節(jié)點(diǎn)為C;
E => End: 查表知到E的距離是9皂吮, E到End的距離是1戒傻,9 + 1 = 10 > 9 (C => End更新過(guò)的)税手,所以不更新距離表和父節(jié)點(diǎn)表。

到此更新完畢了需纳。我們得到了最終的距離表和父地點(diǎn)表芦倒。這樣就得到了結(jié)論,從Start到到End的最小距離是9不翩,路徑是:End => C(End的父節(jié)點(diǎn)) => B(C的父節(jié)點(diǎn)) => Start(B的父節(jié)點(diǎn)) 的反轉(zhuǎn) Start => B => C => End兵扬。

這張圖比較簡(jiǎn)單,我們只是初始化了一次各個(gè)表格口蝠,3次遍歷(初始化算一次)所有的相鄰節(jié)點(diǎn)之間的路徑就得到了最終的路徑器钟。但是根據(jù)上面我們 對(duì)最短路徑的一個(gè)認(rèn)知 可以知道,并不是所有的節(jié)點(diǎn)分布都如此妙蔗,但是傲霸,最短路徑的深度最多就是 N(節(jié)點(diǎn)個(gè)數(shù)) - 1,所以我們最多需要遍歷 N - 1 次眉反。

算法可以用于含有負(fù)權(quán)值邊的圖

狄克斯特拉算法實(shí)際是依賴廣度來(lái)進(jìn)行拓展昙啄。依賴廣度來(lái)進(jìn)行拓展就會(huì)造成已經(jīng)完成任務(wù)的頂點(diǎn)就不能再改變其距離,最終是通過(guò)累加的方式寸五,算出所有頂點(diǎn)到達(dá)源點(diǎn)的最小路徑跟衅,只能逐漸增加總權(quán)值,負(fù)值的存在會(huì)減小總權(quán)值播歼,可能會(huì)造成某條路徑上的頂點(diǎn)從原來(lái)的不是當(dāng)前距離表中的最小值變成最小值伶跷。

由于貝爾曼福德算法是通過(guò)N-1次遍歷,每次都遍歷所有的兩個(gè)相鄰頂點(diǎn)之間的路徑秘狞。所以是有窮次的單純遍歷去“松弛”叭莫,相當(dāng)于在深度上去松弛,每次都會(huì)再次遍歷所有的路徑進(jìn)行松弛烁试,簡(jiǎn)單粗暴雇初,所以路徑上某個(gè)頂點(diǎn)距離的變化都是動(dòng)態(tài)的來(lái)變化,因?yàn)槊看伪闅v只在一個(gè)深度减响,并不能確定下一個(gè)深度遍歷過(guò)后靖诗,某個(gè)頂點(diǎn)的到達(dá)距離是會(huì)變大還是縮小。所以不管放大還是縮小都是可以的支示,也就是不管路徑的權(quán)值是正還是負(fù)都是可以的刊橘。

去除負(fù)權(quán)環(huán)

上面的過(guò)程實(shí)際上還沒(méi)有結(jié)束,我們還需要思考一個(gè)問(wèn)題:如果圖中存在負(fù)權(quán)環(huán)會(huì)怎么樣颂鸿?答:存在負(fù)權(quán)環(huán)的話會(huì)使最終某幾個(gè)頂點(diǎn)的到達(dá)距離值不收斂于一個(gè)小的值促绵,每次更新都不斷的減小,無(wú)限制的減小。

圖中負(fù)權(quán)環(huán)的部分.png

如圖败晴,如果圖中某一部分如圖中所示浓冒,是一個(gè)包含有負(fù)權(quán)值的一個(gè)環(huán)路,那么我們第一次遍歷所有相鄰接點(diǎn)之間的路徑進(jìn)行松弛的時(shí)候尖坤,各個(gè)頂點(diǎn)的距離會(huì)發(fā)生變化(假設(shè)遍歷順序從 C => A 這條路徑開(kāi)始):

遍歷一次之后.png

我們可以看到稳懒,遍歷一次之后,環(huán)中所有的頂點(diǎn)的距離值都變小了慢味!如果圖中的所有節(jié)點(diǎn)個(gè)數(shù)有N個(gè)场梆,根據(jù)上面,我們會(huì)進(jìn)行N-1次循環(huán)贮缕,那么在這N-1次循環(huán)中辙谜,這個(gè)環(huán)中的節(jié)點(diǎn)的距離每次都會(huì)減小,總的距離會(huì)無(wú)限減小感昼,以至于最終找不到最短的路徑(每次都減短装哆,無(wú)限循環(huán)的話就無(wú)限減短,這肯定是不對(duì)的)定嗓。

所以我們要檢查圖中有沒(méi)有負(fù)權(quán)環(huán)蜕琴。檢查的方法就是,在經(jīng)歷過(guò)之前N-1次循環(huán)之后宵溅,我們?cè)俅螜z查一遍所有的相鄰節(jié)點(diǎn)之間的路徑凌简。如果不存在負(fù)權(quán)環(huán),那么N-1次之后我們肯定找到了最短路徑恃逻,那么我們此時(shí)圖中的所有的相鄰節(jié)點(diǎn)之間的路徑都會(huì)滿足(比如 C => A):

C的距離 + C到A的距離 >= A的距離(如果C => A 是最短路徑的一部分取=雏搂,如果不是就取 >)

如果存在負(fù)權(quán)環(huán),導(dǎo)致的是這個(gè)環(huán)中所有的頂點(diǎn)的到達(dá)距離會(huì)無(wú)限減小寇损。我們N-1次循環(huán)是有限的凸郑,但是N-1次循環(huán)之后,再次進(jìn)行檢查時(shí)(多加一次第N次循環(huán))矛市,距離還是可以減小芙沥,也就是(比如C => A):

C的距離 + C到A的距離 < A的距離(這個(gè)言外之意就是還能優(yōu)化,我們知道經(jīng)歷N-1次循環(huán)之后已經(jīng)找到最短路徑了浊吏,這里還能再找而昨,也就是無(wú)限減小最短路徑的距離)

所以出現(xiàn)這種情況,我們就可以判定找田,這個(gè)圖中存在負(fù)權(quán)環(huán)歌憨。如果存在負(fù)權(quán)環(huán),那么我們經(jīng)歷N-1次循環(huán)之后我們得到的最短路徑就不一定是最短的路徑了午阵。所以只有在經(jīng)歷過(guò)N-1次遍歷松弛之后躺孝,再進(jìn)行負(fù)權(quán)環(huán)的判定享扔,如果沒(méi)有負(fù)權(quán)環(huán)的存在底桂,那么我們得到的最短路徑植袍,就是正確的。

算法實(shí)現(xiàn)

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


# 定義一個(gè)表示邊的類(lèi)
class Edge:
    u = ''      # 邊的起點(diǎn)
    v = ''      # 邊的終點(diǎn)
    weight = 0  # 邊的權(quán)重

    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 = {}

    # 初始化參數(shù)
    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

    # 檢查是否有負(fù)權(quán)環(huán)
    for edge in edges:
        if distances[edge.u] + edge.weight < distances[edge.v]:
            return  None, None

    # 更新了所有節(jié)點(diǎn)的到達(dá)最小距離籽懦,那么到達(dá)end的最小距離就是distances表中end所對(duì)應(yīng)的值
    distance = distances[end]
    # 通過(guò)不斷的回溯的方式于个,得到end到達(dá)start的路徑,這樣暮顺,就得到了start到達(dá)end的路徑
    parent = parents[end]
    paths = [parent, end]
    while parent is not start:
        paths.insert(0, parents[parent])
        parent = parents[parent]

    return distance, paths


# 所有圖中的頂點(diǎn)
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)))

結(jié)果是:

找到最短路徑.png



上一篇:寫(xiě)給媳婦兒的算法(十二)——狄克斯特拉算法
下一篇:寫(xiě)給媳婦兒的算法(十四)——?jiǎng)討B(tài)規(guī)劃

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末厅篓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捶码,更是在濱河造成了極大的恐慌羽氮,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惫恼,死亡現(xiàn)場(chǎng)離奇詭異档押,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)祈纯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)令宿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人腕窥,你說(shuō)我怎么就攤上這事粒没。” “怎么了簇爆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵癞松,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我入蛆,道長(zhǎng)响蓉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任安寺,我火速辦了婚禮厕妖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘挑庶。我一直安慰自己言秸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布迎捺。 她就那樣靜靜地躺著举畸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凳枝。 梳的紋絲不亂的頭發(fā)上抄沮,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天跋核,我揣著相機(jī)與錄音,去河邊找鬼叛买。 笑死砂代,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的率挣。 我是一名探鬼主播刻伊,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼椒功!你這毒婦竟也來(lái)了捶箱?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤动漾,失蹤者是張志新(化名)和其女友劉穎丁屎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體旱眯,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晨川,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了键思。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片础爬。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吼鳞,靈堂內(nèi)的尸體忽然破棺而出看蚜,到底是詐尸還是另有隱情,我是刑警寧澤赔桌,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布供炎,位于F島的核電站,受9級(jí)特大地震影響疾党,放射性物質(zhì)發(fā)生泄漏音诫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一雪位、第九天 我趴在偏房一處隱蔽的房頂上張望竭钝。 院中可真熱鬧,春花似錦雹洗、人聲如沸香罐。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)庇茫。三九已至,卻和暖如春螃成,著一層夾襖步出監(jiān)牢的瞬間旦签,已是汗流浹背查坪。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宁炫,地道東北人偿曙。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像淋淀,于是被迫代替她去往敵國(guó)和親遥昧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子覆醇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容