classGraph:def__init__(self, graph:dict={}):
self.graph = graph # A dictionary for the adjacency listdefadd_edge(self, node1, node2, weight):if node1 notin self.graph:# Check if the node is already added
self.graph[node1]={}# If not, create the node
self.graph[node1][node2]= weight # Else, add a connection to its neighbor技术支持
使用该类,我们可以像这样从零开始迭代构建图:
G = Graph()# Add A and its neighbors
G.add_edge("A","B",3)
G.add_edge("A","C",3)# Add B and its neighbors
G.add_edge("B","A",3)
G.add_edge("B","D",3.5)
G.add_edge("B","E",2.8)...技术支持
或者我们可以直接传入一个邻接表字典,这更快捷:
# Use the dictionary we defined earlier
G = Graph(graph=graph)
G.graph
技术支持
classGraph:def__init__(self, graph:dict={}):
self.graph = graph
# The add_edge method omitted for spacedefshortest_distances(self, source:str):# Initialize the values of all nodes with infinity
distances ={node:float("inf")for node in self.graph}
distances[source]=0# Set the source value to 0...技术支持
当方法编写完成后,会返回这个字典,其中包含从源点到所有节点的最短距离。我们就可以像这样找到从 B 到 F 的距离:
# Sneak-peek - find the shortest paths from B
distances = G.shortest_distances("B")
to_F = distances["F"]print(f"The shortest distance from B to F is {to_F}")技术支持
classGraph:def__init__(self, graph:dict={}):
self.graph = graph
defshortest_distances(self, source:str):# Initialize the values of all nodes with infinity
distances ={node:float("inf")for node in self.graph}
distances[source]=0# Set the source value to 0# Initialize a priority queue
pq =[(0, source)]
heapify(pq)# Create a set to hold visited nodes
visited =set()技术支持
pq 中每个元素的优先级就是它的当前取值。我们还初始化一个空集合来记录已访问的节点。
4. 用 while 循环实现 Dijkstra 算法
现在,我们使用 while 循环开始遍历所有未访问的节点:
classGraph:def__init__(self, graph:dict={}):
self.graph = graph
defshortest_distances(self, source:str):# Initialize the values of all nodes with infinity
distances ={node:float("inf")for node in self.graph}
distances[source]=0# Set the source value to 0# Initialize a priority queue
pq =[(0, source)]
heapify(pq)# Create a set to hold visited nodes
visited =set()while pq:# While the priority queue isn't empty
current_distance, current_node = heappop(
pq
)# Get the node with the min distanceif current_node in visited:continue# Skip already visited nodes
visited.add(current_node)# Else, add the node to visited set技术支持
while pq:# While the priority queue isn't empty
current_distance, current_node = heappop(pq)if current_node in visited:continue
visited.add(current_node)for neighbor, weight in self.graph[current_node].items():# Calculate the distance from current_node to the neighbor
tentative_distance = current_distance + weight
if tentative_distance < distances[neighbor]:
distances[neighbor]= tentative_distance
heappush(pq,(tentative_distance, neighbor))return distances
技术支持
G = Graph(graph)
distances = G.shortest_distances("B")print(distances,"\n")
to_F = distances["F"]print(f"The shortest distance from B to F is {to_F}")技术支持
{'A':3,'B':0,'C':5.6,'D':3.5,'E':2.8,'F':9.1,'G':9.8}
The shortest distance from B to F is9.1技术支持
因此,从 B 到 F 的距离是 9.1。但等等:这条路径是哪一条?
5. 构造最短路径
到目前为止,我们只有最短距离而非路径,使得我们的图看起来像这样:
要从计算得到的最短距离构造最短路径,我们可以从目标点开始回溯。对于连接到 F 的每条边,分别相减其权重:
9.1–3.5 = 5.6
9.1–2.5 = 6.5
然后查看这些结果是否与 F 的邻居的取值相匹配。只有 5.6 与 C 的值匹配,这意味着 C 在最短路径上。
我们对 C 的邻居 A 和 E 再重复一次:
5.6–3 = 2.6
5.6–2.8 = 2.8
由于 2.8 与 E 的值相匹配,它也在最短路径上。E 也是 B 的邻居,因此 B 与 F 之间的最短路径是 BECF。
让我们把这一过程的实现添加到我们的类中。代码本身与上述解释略有不同,因为它会找出 B 与所有其他节点之间的最短路径,而不仅仅是 F。
为什么要学习 Dijkstra 算法?
Dijkstra 是在网络中寻找两点之间最短路径的首选算法,应用广泛。它是计算机科学与图论中的基础。理解并学会实现它,将为更高级的图算法与应用奠定基础。
它还是学习一种重要问题求解思路的途径——贪心算法,即在每一步都基于当前信息做出最优选择。
这一技能可迁移到其他优化算法。Dijkstra 算法并非所有场景下都最高效,但在解决“最短距离”问题时,它是一个很好的基线。
示例包括:
即使您对上述领域并不感兴趣,用 Python 实现 Dijkstra 算法也能让您练习以下重要概念:
与图相关的关键概念
要在 Python 中实现 Dijkstra 算法,我们需要回顾图论中的几个基本概念。首先是图本身:
图是由节点(顶点)与边构成的集合。节点表示网络中的实体或点,边表示它们之间的连接或关系。
图可以是加权或非加权。非加权图中,所有边的权重相同(通常设为 1)。在加权图中(正如其名),每条边都有一个权重,常表示成本、距离或时间。
本教程将使用加权图,因为 Dijkstra 算法需要加权图。
图中的路径指由边连接的一系列节点,从特定起点到终点。在本教程中我们关心的是最短路径,即总权重(距离、成本等)最小的那条路径。
用可视化方式解释 Dijkstra 算法
在接下来的教程中,我们将使用刚才看到的最后一个图:
让我们用 Dijkstra 算法在至少七条可能路径中,找出点 B 与 F 之间的最短路径。起初我们先用可视化方式完成此任务,随后再用代码实现。
首先,如下初始化算法:
然后,我们迭代执行以下步骤:
在我们的图中,B 有三个邻居——A、D 和 E。让我们从根节点开始依次访问它们,并根据它们的边权更新其值(迭代 1):
在迭代 2 中,我们再次选择值最小的节点,这次是 E。它的邻居是 C、D 和 G。B 已被排除,因为我们已经访问过它。让我们更新 E 的邻居的值:
我们将 C 的值设为 5.6,因为它的值是从 B 到 C 的权重累计和。G 同理。不过,如果您注意到,D 的值仍为 3.5,而不是像其他节点那样应为 3.5 + 2.8 = 6.3。
规则是:如果新的累计和大于该节点当前的值,我们就不更新,因为该节点的当前值已经记录了到根的最短距离。在本例中,D 当前的 3.5 就是它到 B 的最短距离,因为它们是邻居。
我们以此方式继续,直到所有节点都被访问。届时,我们就得到了从 B 到每个节点的最短距离,可以直接查看 B 到 F 的值。
总结而言,步骤是:
在 Python 中逐步实现 Dijkstra 算法
现在,为了加深您对 Dijkstra 算法的理解,我们将一步步用 Python 来实现。
将图理解为字典
首先,我们需要一种在 Python 中表示图的方式——最常见的选择是使用字典。如果这是我们的图:
我们可以用如下字典表示它:
字典的每个键是一个节点,每个值是一个字典,包含该键的邻居及到邻居的距离。我们的图有七个节点,因此字典必须有七个键。
其他资料通常将上述字典称为邻接表字典(dictionary adjacency list)。接下来我们也沿用这一术语。
1. 创建一个
Graph类为使代码更模块化、更易理解,我们将实现一个简单的
Graph类来表示图。在其内部,它将使用邻接表字典。它还会有一个方法,用于轻松地在两个节点之间添加连接:使用该类,我们可以像这样从零开始迭代构建图:
或者我们可以直接传入一个邻接表字典,这更快捷:
2. 用字典存储源点到各节点的距离
我们再创建一个类方法——这次用于算法本身——
shortest_distances:在该方法内部,我们首先定义一个包含节点-取值对的字典。每次访问某个节点及其邻居时,该字典都会更新。正如我们在算法的可视化解释中提到的,所有节点的初始值设为无穷大,而源点的值为 0。
当方法编写完成后,会返回这个字典,其中包含从源点到所有节点的最短距离。我们就可以像这样找到从 B 到 F 的距离:
3. 使用优先队列迭代节点
现在,我们需要一种方式,按照 Dijkstra 算法的规则遍历图中的节点。如果您还记得,在每次迭代中,我们都需要选择取值最小的节点并访问它的邻居。
您可能会说,我们可以直接遍历字典的键:
这种方法不行,因为它无法根据节点的值进行排序。因此,简单数组无法满足需求。为解决这一问题,我们将使用优先队列。
优先队列类似于常规列表(数组),但每个元素都有一个额外的优先级值。下面是一个示例优先队列:
这个队列包含三个元素——A、C 和 D——每个都有一个决定队列操作顺序的优先级。Python 提供了内置的
heapq库来处理优先队列:该库有三个我们关心的函数:
heapify:将由“优先级-值”对组成的列表转换为优先队列。heappush:将带有其优先级的元素加入队列。heappop:移除并返回优先级最高(数值最小)的元素。如您所见,将 pq 用 heapify 转为队列后,我们就能按优先级取出 C。我们再试着推入一个值:
队列已根据新元素的优先级正确更新,因为
heappop返回了优先级为 0 的新元素。注意,一旦我们弹出一个元素,它将不再属于队列。
现在回到我们的算法,在初始化好
distances后,我们创建一个新的优先队列,该队列初始只包含源节点:pq中每个元素的优先级就是它的当前取值。我们还初始化一个空集合来记录已访问的节点。4. 用
while循环实现 Dijkstra 算法现在,我们使用
while循环开始遍历所有未访问的节点:当优先队列不为空时,我们不断弹出优先级最高(数值最小)的节点,并将其取值与名称分别赋给
current_distance与current_node。如果current_node已在visited中,我们就跳过;否则将其标记为已访问,然后转而访问其邻居:对于每个邻居,我们通过将连接边的权重加到当前节点的值上,计算从当前节点到该邻居的暂定距离。然后检查该距离是否小于该邻居在
distances中的距离。如果是,则更新distances字典,并将带有暂定距离的邻居加入优先队列。就是这样!我们已经实现了 Dijkstra 算法。来测试一下:
因此,从 B 到 F 的距离是 9.1。但等等:这条路径是哪一条?
5. 构造最短路径
到目前为止,我们只有最短距离而非路径,使得我们的图看起来像这样:
要从计算得到的最短距离构造最短路径,我们可以从目标点开始回溯。对于连接到 F 的每条边,分别相减其权重:
然后查看这些结果是否与 F 的邻居的取值相匹配。只有 5.6 与 C 的值匹配,这意味着 C 在最短路径上。
我们对 C 的邻居 A 和 E 再重复一次:
由于 2.8 与 E 的值相匹配,它也在最短路径上。E 也是 B 的邻居,因此 B 与 F 之间的最短路径是 BECF。
让我们把这一过程的实现添加到我们的类中。代码本身与上述解释略有不同,因为它会找出 B 与所有其他节点之间的最短路径,而不仅仅是 F。
此外,它是反向完成的,但思路相同。
将以下代码添加到
shortest_distances方法的末尾,并删除旧的return语句:当代码运行完成时,
predecessors字典将包含参与到源点最短路径中的每个节点的直接父节点。我们来试试:首先,让我们用
predecessors字典从 F 开始回溯,手动找出最短路径:就是这样!该字典正确告诉我们,B 与 F 之间的最短路径是 BECF。
不过我们是半手动完成的。我们希望有一个方法,能从任意源点到任意目标点,基于
predecessors字典自动返回最短路径。我们将该方法定义为shortest_path(将其添加到类的末尾):通过调用
shortest_distances函数,我们生成predecessors字典。随后启动一个 while 循环,每次迭代都从当前节点回溯到其父节点,直到到达源节点。然后我们反转并返回该列表,即从源点到目标点的路径。测试一下:
可以!再试一个:
一切顺利。
结语
在本教程中,我们学习了图论中最基础的算法之一——Dijkstra。它常用于在类图结构中寻找两点之间的最短路径。由于其简单性,它在多个行业中都有应用,例如:
希望用 Python 实现 Dijkstra 算法能够激发您对精彩图算法世界的兴趣。如果您想进一步学习,我强烈推荐以下资源: