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),v∈V
证明:在之前的文章中我们已经证明了,松弛操作使得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=s→v1→v2→...→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,vi−1)+w(vi,vi−1)
接下来,用归纳法证明
当循环次数为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[vi−1]=δ(s,vi−1)。
在第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<∣V∣−1,因为p是一条无重复的路径,因此p上的节点总数不会超过图的顶点数
∣
V
∣
|V|
∣V∣。
证毕。