[算法分析笔记] 最短路径(下)Ballman-Ford算法

1. 问题定义

上一篇文章,介绍了Dijkstra算法求最短路径,但使用Dijkstra算法时必须保证所有边的权值非负。
这篇文章就让我们来看一下,如果存在负值的边,该如何改进算法以适应需求。
这里我们要介绍一个新的算法–Ballman-Ford算法,在有负值的边存在的情况下,如果检测到有一个负循环路径,则报告负循环路径存在。

2. 代码解析

2.1 python算法代码

还是老规矩先上代码

class graph:
    def __init__(self, V=[], W=[]):
        self.V = V
        self.W = W
        
    def weight(self, vertix_1, vertix_2):
        #print(vertix_1,vertix_2)
        if self.W.get(vertix_1,'NaN') != 'NaN':
            if self.W[vertix_1].get(vertix_2,'NaN') != 'NaN':
                return self.W[vertix_1][vertix_2]
        return float('-inf')

    def Adj(self, u):
        adj_u = []
        for key in self.W[u]:             
                adj_u.append(key)
        return adj_u

def Bellman_Ford(G,s):
    # Initialization
    d = {}
    for v in G.V:
        d[v] = float('inf')
    d[s] = 0
    #Loop to relax each edge
    #1st Loop
    for i in range(1,len(G.V)-1):
        change = False
        print('-----loop %d-----'%(i))
        #2nd Loop
        for u in G.W:
        	#3rd Loop
            for v in G.W[u]:
                if d[v] > d[u] + G.weight(u,v):
                    d[v] = d[u] + G.weight(u,v)
                    change = True
                print(d)
        #if no change on d in last loop, then stop
        if change == False:
            break
    #Check the existance of negative loop
    for u in G.W:
        for v in G.W[u]: 
            if d[v] > d[u] + G.weight(u,v):
                print('存在一条权值是负的循环通路!')
                return {}
    return d

2.2 代码解析

数据结构这里就不多说了,和上一篇文章《最短路径(上)- Dijkstra算法》是一模一样的。

那么算法函数输入两个参数,G是图的结构,他的数据结构是之前定义的graph类。第二个参数是起始顶点,该顶点必须是G中定义的一个顶点。
函数返回值d是一个字典,他的每一个key是图中的顶点,每个key对应的值在算法结束时就是s顶点到各其他顶点的最短路径。如果检测到存在负值的循环通路,则报错并返回空字典。

第一部分 初始化

这里的初始化和Dijkstra算法一模一样,将出发顶点s的d[s]设为0, 其他顶点的值设为无穷大。

第二部分 松弛操作

先让我们来看第二重循环里的代码:

#2nd Loop
for u in G.W:
	#3rd Loop
    for v in G.W[u]:
        if d[v] > d[u] + G.weight(u,v):
            d[v] = d[u] + G.weight(u,v)

第二重循环和第三重循环放在一起看,是对图中存在的每一条边逐个进行松弛操作,检测d[v]是否大于d[u]+weight(u,v),换而言之目前d[v]中记录的由s到v的路径的权值如果大于d[u]+weight(u,v),则说明由s经过d到v的路径更有,故而更新d[v]值。

而最外层的循环数等于该图包括的顶点数减1,经过那么多次对图中每条边的松弛操作,就可以得到s到每个顶点的最短路径。

这里值得注意的是为了提高运算效率,我们设置了一个change标志,如果在某一轮循环中,没有任何d的key值被改变,那么我们可以提前退出循环,因为即使继续循环,根据算法d值也不可能再发生变化。

第三部分 检测负循环路径

这一部分是用来检测是否存在负的循环通路,如果存在则报错且返回空列表,如果没有检测到则返回字典数据d。
这里依旧是个双重循环,和上一部分代码相同,这个双重循环可以看做是在取出图中的每一条边,然后判断d[v]是否大于d[u]+weight(u,v),如果存在这样的d[v]那么就是说s经过u到v的路径比前一段算法得到的最短路径更短,发生这种情况的唯一可能就是存在负值循环通路。

至此,代码解析完毕。接下来让我们看一个实例。

2.3 算法实例

让我们以下图为例来构建一个graph对象。
在这里插入图片描述

V = ['A','B','C','D','E']
W = {'A':{'B':-1,'C':4}, 'B':{'C':3,'D':2,'E':2},'D':{'C':5,'B':1},'E':{'D':-3}}
G = graph(V,W)


shortest_path=Bellman_Ford(G,'A')
print('最短路径为:')
print(d)
Output:
-----loop 0-----
{'A': 0, 'B': -1, 'C': inf, 'D': inf, 'E': inf}
{'A': 0, 'B': -1, 'C': 4, 'D': inf, 'E': inf}
{'A': 0, 'B': -1, 'C': 2, 'D': inf, 'E': inf}
{'A': 0, 'B': -1, 'C': 2, 'D': 1, 'E': inf}
{'A': 0, 'B': -1, 'C': 2, 'D': 1, 'E': 1}
{'A': 0, 'B': -1, 'C': 2, 'D': -2, 'E': 1}
-----loop 1-----
最短路径为 :
{'A': 0, 'B': -1, 'C': 2, 'D': -2, 'E': 1}

此出,我们手工的来推演一下算法的运行过程:
根据graph的数据定义可知,在循环中边的松弛过程是按照如下顺序进行的:
(A,B) = -1, (A,C)=4, (B,C)=3, (B,D)=2, (B,E)=2, (D,C)=5, (D,B)=1, (E,D)=-3

在第一轮循环开始前d值中除了d[A]=0,其余皆为无穷大(inf)。首先被松弛的边是(A,B),我们检测d[B]>d[A]+weight(A,B)=-1,显然成立,于是d[B]=-1。
接下来松弛边(A,C),检测d[C]>d[A]+weight(A,C)=4,也成立,于是d[C]=4。
以此类推,直到第一轮外部循环结束。在进行完第二轮循环后会发现,d里的key的值没有任何变化,说明我们已经获得了最短路径的值,直接退出循环。

在验证环节,我们逐条边的检测是否存在d[v]>d[u]+weight(u,v)的情况,没有发现负值循环路径,至此算法结束,返回字典d。

3. 算法证明

首先,我们证明如果不存在负权值的边,那么Bellman-Ford算法终止时, d [ v ] = δ ( s , v ) , v ∈ V d[v]=\delta(s,v), v\in V d[v]=δ(s,v),vV

证明:在之前的文章中我们已经证明了,松弛操作使得d[v]单调递减的,且是安全的,即d[v]不会小于 δ ( s , v ) \delta(s,v) δ(s,v) δ ( s , v ) \delta(s,v) δ(s,v)是s到v的最短路径。
则存在一条路径 p = s → v 1 → v 2 → . . . → v k p=s\rarr v_1\rarr v_2\rarr ...\rarr v_k p=sv1v2...vk是一条最短路径,且p上不存在权值为0的边,那么对于p上的一个顶点 v i v_i vi, 由上一篇文章里的引理可得,有: δ ( s , v i ) = δ ( s , v i − 1 ) + w ( v i , v i − 1 ) \delta(s,v_i)=\delta(s,v_{i-1})+w(v_i,v_{i-1}) δ(s,vi)=δ(s,vi1)+w(vi,vi1)

接下来,用归纳法证明
当循环次数为0时,存在 d [ s ] = 0 = δ ( s , s ) d[s]=0=\delta(s,s) d[s]=0=δ(s,s) ,显然成立。
假设循环次数为j时,存在 d [ v j ] = δ ( s , v j ) , j < i d[v_j] = \delta(s,v_j), j<i d[vj]=δ(s,vj),j<i
由之前的证明经过i-1次循环后,得到 d [ v i − 1 ] = δ ( s , v i − 1 ) d[v_{i-1}]=\delta(s,v_{i-1}) d[vi1]=δ(s,vi1)
在第i次循环时,对每一条边做松弛操作后,由前一篇文章中得引理可知: d [ v i ] = δ ( s , v i ) d[v_i]=\delta(s,v_i) d[vi]=δ(s,vi),且循环次数 k < ∣ V ∣ − 1 k<|V|-1 k<V1,因为p是一条无重复的路径,因此p上的节点总数不会超过图的顶点数 ∣ V ∣ |V| V
证毕。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程小白的逆袭日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值