目录
1 算法步骤
Johnson算法分为三步:
1. 先使用bellman-ford算法,计算单源到其他点的最短路径;
2. 利用计算结果去重赋权reweight,使得没有负数权重;
3. 循环调用dijikstra算法计算所有点的权重。
4. 所有点的权重再换算回来,得到结果。
复杂度为
O
(
n
m
+
n
2
l
o
g
n
)
O(nm + n^2 log n)
O(nm+n2logn)(计算机学科的对数以2为底),从理论上讲这是目前世界上性能最高的全源负权最短距离算法。
重赋权的计算也不难。步骤分为以下几步:
1. 图增加一个新的节点H,使图变成增广图
G
′
G'
G′,新节点H与图中每一个节点都相连,并且距离为0;
2. 使用bellman-ford算法计算H与其他点的单源最短路径。
3. 计算出来后,用以下公式进行重赋权
W
2
(
x
,
y
)
=
W
1
(
x
,
y
)
+
h
(
x
)
−
h
(
y
)
W_2(x, y) = W_1(x, y) + h(x) - h(y)
W2(x,y)=W1(x,y)+h(x)−h(y)
4. 然后移除新的顶点H,因为这是个虚拟节点,所以在计算完成之后,要将图进行恢复。
2 重赋权为什么非负
为了解释重赋权公式,我画一张图,图中
h
(
x
)
h(x)
h(x)代表虚拟点h到x的最短路径,
h
(
y
)
h(y)
h(y)代表虚拟点h到y的最短路径,
w
(
x
,
y
)
w(x,y)
w(x,y)代表x指向y的边的权重:
因为是最短路径,所以必定有以下公式:
h
(
x
)
+
w
(
x
,
y
)
≥
h
(
y
)
∴
w
(
x
,
y
)
+
h
(
x
)
−
h
(
y
)
≥
0
h(x)+w(x,y)\ge h(y)\\ \therefore w(x,y) + h(x) - h(y) \ge 0
h(x)+w(x,y)≥h(y)∴w(x,y)+h(x)−h(y)≥0
所以重赋权之后,一定不会出现负权重。
3 为什么可以重赋权
但是还有一个问题,为什么可以这样重赋权?我们必须证明一点,重赋权之后还是最短路径。假设
u
0
u_0
u0到
v
k
v_k
vk的最短路径为
p
p
p。在p上,除了
v
0
v_0
v0和
v
k
v_k
vk外还有
v
1
,
v
2
,
…
,
v
k
−
1
v_1,v_2,\dots,v_{k-1}
v1,v2,…,vk−1这些点。所以最短路径总权重,也就是最短距离
d
(
v
0
,
v
k
)
d(v_0,v_k)
d(v0,vk)就是:
d
(
v
0
,
v
k
)
=
∑
i
=
0
k
−
1
w
(
v
i
,
v
i
+
1
)
d(v_0,v_k)=\sum_{i=0}^{k-1}w(v_i,v_{i+1})
d(v0,vk)=i=0∑k−1w(vi,vi+1)
我们进行重赋权,重赋权公式为:
w
′
(
x
,
y
)
=
w
(
x
,
y
)
+
h
(
x
)
−
h
(
y
)
w'(x, y) = w(x, y) + h(x) - h(y)
w′(x,y)=w(x,y)+h(x)−h(y)
那么重赋权之后的最短路径总权重为:
d
′
(
v
0
,
v
k
)
=
∑
i
=
0
k
−
1
w
′
(
v
i
,
v
i
+
1
)
=
∑
i
=
0
k
−
1
w
(
v
i
,
v
i
+
1
)
+
h
(
v
i
)
−
h
(
v
i
+
1
)
d'(v_0,v_k)=\sum_{i=0}^{k-1}w'(v_i,v_{i+1})=\sum_{i=0}^{k-1}w(v_i,v_{i+1}) + h(v_i) - h(v_{i+1})
d′(v0,vk)=i=0∑k−1w′(vi,vi+1)=i=0∑k−1w(vi,vi+1)+h(vi)−h(vi+1)
展开之后,发现这是个伸缩数列,中间的
h
(
v
1
)
…
h
(
v
k
−
1
)
h(v_1) \dots h(v_{k-1})
h(v1)…h(vk−1)全部是一正一负,相加之后被消去了,只剩下了首尾:
d
′
(
v
0
,
v
k
)
=
w
(
v
0
,
v
1
)
+
h
(
v
0
)
−
h
(
v
1
)
+
w
(
v
1
,
v
2
)
+
h
(
v
1
)
−
h
(
v
2
)
+
⋯
+
w
(
v
k
−
2
,
v
k
−
1
)
+
h
(
v
k
−
2
)
−
h
(
v
k
−
1
)
+
w
(
v
k
−
1
,
v
k
)
+
h
(
v
k
−
1
)
−
h
(
v
k
)
=
h
(
v
0
)
−
h
(
v
k
)
+
∑
i
=
0
k
−
1
w
(
v
i
,
v
i
+
1
)
=
d
(
v
0
,
v
k
)
+
h
(
v
0
)
−
h
(
v
k
)
d'(v_0,v_k)\\ =w(v_0,v_1)+h(v_0)-h(v_1)\\ +w(v_1,v_2)+h(v_1)-h(v_2)\\ +\cdots\\ +w(v_{k-2},v_{k-1})+h(v_{k-2})-h(v_{k-1})\\ +w(v_{k-1},v_k)+h(v_{k-1})-h(v_k)\\ =h(v_0)-h(v_k)+\sum_{i=0}^{k-1}w(v_i,v_{i+1})\\ =d(v_0, v_k) + h(v_0) - h(v_k)
d′(v0,vk)=w(v0,v1)+h(v0)−h(v1)+w(v1,v2)+h(v1)−h(v2)+⋯+w(vk−2,vk−1)+h(vk−2)−h(vk−1)+w(vk−1,vk)+h(vk−1)−h(vk)=h(v0)−h(vk)+i=0∑k−1w(vi,vi+1)=d(v0,vk)+h(v0)−h(vk)
所以证明了,最短路径上,重赋权之后的权重和是原权重和的重赋权。巧妙地利用伸缩数列,是Johnson算法的精妙之处。而其他博文是很少讲到这里的,感觉有点意思的,有意思的,可以收藏本文,也可以顺势关注一波。
4 小例子
创建虚拟节点H
重赋权:
5 python实现
Python代码如下:
def johnson(self):
print("before reweight", self.to_dot())
# 新加一个虚拟节点
self.__vertices.append('H')
n = len(self.__vertices)
for i in range(0, n - 1):
self.__edges.append(WeightedEdge(n - 1, i, 0))
h = self.bellman_ford(n - 1)[0]
print("before reweight", self.to_dot())
# 重建权重
for e in self.__edges:
e.weight = e.weight + h[e.from_index] - h[e.to_index]
# 删除后,使用
self.__vertices.pop()
self.__edges = self.__edges[0:-n]
print("after reweight", self.to_dot())
distance = [None for _ in range(len(self.__vertices))]
for s in range(len(self.__vertices)):
dijkstra = self.dijkstra(s)
print(dijkstra)
for i, e in enumerate(dijkstra):
dijkstra[i] = e - h[s] + h[i]
distance[s] = dijkstra
# 恢复权重
for e in self.__edges:
e.weight = e.weight - h[e.from_index] + h[e.to_index]
return distance
测试数据:
def create_graph(self):
vertices = ['A', 'B', 'C', 'S']
edges = [
WeightedEdge(0, 1, 2),
WeightedEdge(2, 0, -2),
WeightedEdge(2, 1, 1),
WeightedEdge(3, 0, 1),
WeightedEdge(3, 2, 2)
]
graph = WeightedGraph()
graph.vertices = vertices
graph.edges = edges
return graph
测试结果:
[[0, 2, inf, inf], [inf, 0, inf, inf], [-2, 0, 0, inf], [1, 3, inf, 0]]