引入
原文链接:https://www.freecodecamp.org/news/dijkstras-shortest-path-algorithm-visual-introduction/
如果你对图有一定的了解,则可以开始这篇让人惊异的算法,分为六个部分:
1)目的与使用场景;
2)历史;
3)算法基础;
4)算法局限;
5)示意;以及
6)实现。
1 目的与使用场景
基于Dijkstra算法,你可以找到图中任意节点之间的最短路径。特别地,你可以找到给定的,称为源点 (Source node) 的节点到图中其他节点的最短路径,并获取一个最短路径树。
该算法已广泛使用在GPS设备中,用以寻找到当前位置到目的地的最短路径,其也在工业,尤其是需要建模网络的领域中普遍使用。
2 历史
该算法由Edsger W. Dijkstra教授提出,一位杰出的荷兰计算机科学家与软件工程师。早在1959年,他便在名为《A note on two problems in connexion with graphs》的文章中解释了这个新算法。
在2001年的一次访谈中,Dijkstra教授揭露了他是如何以及为何设计这个算法:
从鹿特丹到格罗宁根最短的路线是什么?这样的一个最短路径算法只用了20分钟就从我的脑袋里冒了出来。一天我和小娇妻在阿姆斯特丹购物,“真是太累了。”于是我们去了露天咖啡厅来了几口,我想啊,如何才能走最短的路了?事实上,这个文章被发表已是三年后,出版还是很赞的。它自身很赞的一个原因是当时我既没有笔也没有纸,这使得我不得不避免一些无用功。最终,该算法成为我成名的基石之一,真是惊掉了我的下巴。
3 Dijkstra算法的基础
1)Dijkstra算法从源点开始分析图形以找到该节点与图形中所有其他节点之间的最短路径;
2)该算法跟踪当前已知的从每个节点到源点的最短距离,如果找到更短的路径,它会更新这些值;
3)一旦节点到源点的最短路径被找到,其将被标记为“已访问”并加到路径中;以及
4)终止条件是所有的节点都被添加到路径中。这样我们便拥有了源点到其他所有节点的最短路径。
4 算法的局限
Dijkstra算法仅在图拥有正权重边时生效。这是因为在最短路径树的构建中必须添加边的权重才能找到最短路径。
具体地,一旦节点被标记为“已访问”,到该节点的当前路径被标记为到达该节点的最短路径。而负权重的存在将改变这一现状,即使得在这一步发生后总权重减少。
5 示意
假设有以下图,令0号节点为源点,两个节点之间的权重表示其之间的距离。
1)初始化:已有值为距离的列表:
⋆
\star
⋆ 源点到自己的距离为0;
⋆
\star
⋆ 源点到其他点的距离由于未测定,因此均标记未无穷。
初始化未访问节点列表:
由于0为源点,则将其设置为已访问。等效地,我们将其从未访问节点列表中删除,并为图中的相应节点添加红色边框:
2)检查源点0到相邻节点的距离。如你所见,有1号和2号:
注:这并不意味着需要立即将这两个邻接点加入最短路径。在此之前,需要检查是否已有最短路径可以到达。
我们需要使用0与1和2之间的权重来更新距离:
在更新距离后,我们需要:
⋆
\star
⋆ 基于当前距离知识来选择离源点最近的节点;
⋆
\star
⋆ 标记它并将其加入路径。
此时,1是离0最近的节点:
在列表中,我们用红色方块标记它,表示它已被“访问”并且我们找到了到该节点的最短路径:
从未访问节点列表中划掉它:
3)分析新的相邻节点以找到到达它们的最短路径:仅分析与已经是最短路径一部分的节点相邻的节点。
节点3和2将作为相邻节点分析,因为它们分别与加入最短路径中的1、0节点相连:
由于我们已经在列表中记录了从源点到2的距离,因此我们这次不需要更新它。而是只需要更新源点到新的相邻节点3的距离:
这个距离是7,为什么呢?为了找到3到0的最短距离,我们添加到达该节点,同时组成最短路径的所有边的权重:于3而言,我们将0->1->3的权重相加,则总距离为7。
有了到相邻节点的距离,我们必须选择将哪个节点添加到路径中,即必须选择距离源点最短 (当前已知) 的未访问节点。
从距离列表,能够立即选择出节点2:
图形化的方式标记出来:
在距离列表中添加一个红色小方块并将其从未访问节点列表中划掉来将其标记为已访问:
4)重复以上操作来找到源点到新的邻域点的最短距离:你会发现我们有两条可能路径0-->1-->3
和0-->2-->3
,来看看如何找到最短路径。
点3在距离列表中的最短距离是7,其是我们通过路径0-->1-->3
的路径所得到的权重结果。
但是现在我们有了另一个选择,即如果走0-->2-->3
,我们需要消耗14:
显然,已有的距离更短,所以我们选择0-->1-->3
。注意我们只会在新的路径更短时更新距离。因此,我们将该节点加入到最短路径中:
再次标记:
5)再次重复:此时新的邻域节点为4和5,我们更新这些节点到源点的距离,以找到最短路径:
⋆
\star
⋆ 对于4,由路径0-->1-->3-->4
知距离为17
⋆
\star
⋆ 对于5,由路径0-->1-->3-->5
知距离为22
注:我们只能考虑在最短路径 (标记为红色) 上扩展,而不考虑尚未添加到最短路径的边的路径,例如2-->3
。
现在我们需要选择将哪个未访问节点标记为已访问。在这种情况下,节点4将被标记,因为它在距离列表中的距离最短:
标记4:
6)新的邻域节点为5和6:需要分析我们可以遵循的每条可能的路径,以便从已经标记为已访问并添加到路径中的节点到达它们。
对于5:
⋆
\star
⋆ 第一个选择是0-->1-->3-->5
,此时的距离为22,它已经被记录了
⋆
\star
⋆ 第二个选择是0-->1-->3-->4-->5
,此时的距离为23。
显然第一个选择更优。
对于6,其可用路径是0-->1-->3-->4-->6
,距离为19。
因此标记6为已访问:
新的路径为:
7)目前还有节点5未访问,来看看如何把它加到最短路径里。
对于节点5,目前由三个选项:
⋆
\star
⋆ 0-->1-->3-->5
,距离22
⋆
\star
⋆ 0-->1-->3-->4-->5
,距离23
⋆
\star
⋆ 0-->1-->3-->4-->6-->5
,距离25
所以0-->1-->3-->5
被选择:
完成标记:
最终,我们有了源点0到其他所有节点的最短路径:
6 实现
class Graph:
def __get_parent(self, dist, no_visited):
"""
找到父节点
"""
minimum = float("Inf")
min_index = -1
for i in range(len(dist)):
if dist[i] < minimum and i in no_visited:
minimum = dist[i]
min_index = i
return min_index
def __print_path(self, parent, j):
"""
输出单个节点的路径
"""
if parent[j] == -1:
print(j, end=" ")
return
self.__print_path(parent, parent[j])
print(j, end=" ")
def printSolution(self, dist, parent, src):
"""
输出所有路径
"""
print("节点 \t\t\t代价\t\t\t\t 路径")
for i in range(0, len(dist)):
print("\n%d --> %d \t\t%d \t\t\t\t" % (src, i, dist[i]), end=" ")
self.__print_path(parent, i)
def dijkstra(self, graph, src):
# 矩阵的行
row = len(graph)
# 初始化距离向量
dist = [float("Inf")] * row
# 初始化父节点向量
parent = [-1] * row
# 设置源点到自己的距离为0
dist[src] = 0
# 初始化未查询列表
no_visited = list(range(row))
while no_visited:
# 找到父节点
u = self.__get_parent(dist, no_visited)
# 去除最小元素
no_visited.remove(u)
for i in range(row):
if graph[u][i] and i in no_visited:
if dist[u] + graph[u][i] < dist[i]:
dist[i] = dist[u] + graph[u][i]
parent[i] = u
# 输出
self.printSolution(dist, parent, src)
g = Graph()
graph = [[0., 2., 6., 0., 0., 0., 0.],
[2., 0., 0., 5., 0., 0., 0.],
[6., 0., 0., 8., 0., 0., 0.],
[0., 5., 8., 0., 10., 15., 0.],
[0., 0., 0., 10., 0., 6., 2.],
[0., 0., 0., 15., 6., 0., 6.],
[0., 0., 0., 0., 2., 6., 0.]]
g.dijkstra(graph, 6)