零基础理解 Dijkstra 算法:最短路径求解入门教程
前言:迷路的世界,最短的向导
想象一下,你站在一个陌生的城市中央,手持地图,需要从当前位置前往某个景点。地图上标注了各种道路、路口,每条道路都有不同的长度或者耗时。你自然会想知道:哪条路走起来最快?哪条路距离最短?
在计算机科学的世界里,这个问题无处不在:导航系统如何规划路线?网络路由器如何选择数据包的传输路径?游戏中的NPC如何找到前往目标的捷径?这些问题的核心,都离不开一个经典而强大的算法——Dijkstra(迪杰斯特拉)算法。
Dijkstra 算法由荷兰计算机科学家艾兹格·迪杰斯特拉在1956年提出,旨在解决“单源最短路径问题”。简单来说,就是从图中的一个起始点(源点)出发,找到到达所有其他可达点的最短路径。本教程将以最浅显易懂的方式,带领你从零开始,逐步掌握Dijkstra算法的原理、实现与应用。
第一章:走进图论世界——Dijkstra 的舞台
在深入Dijkstra算法之前,我们需要先了解它的“舞台”——图(Graph)是什么。如果你对图论一无所知,不用担心,我们将从最基本概念讲起。
1.1 什么是图(Graph)?
图是一种数据结构,用于表示对象之间的关系。它由两部分组成:
- 节点(Node)或顶点(Vertex):代表图中的对象。你可以把它们想象成城市中的路口、地铁站、网页等等。
- 边(Edge):连接两个节点的线,表示它们之间的关系。它可以是道路、地铁线路、超链接等等。
1.2 图的常见类型与属性
-
有向图(Directed Graph)vs. 无向图(Undirected Graph):
- 无向图:边没有方向。例如,A城和B城之间有一条双向路,你可以从A走到B,也可以从B走到A。
- 有向图:边有方向。例如,一条单行道,你只能从A走到B,不能从B走到A。
- Dijkstra算法通常应用于有向图,但无向图可以被视为每条边都有两个方向的有向图。
-
权重(Weight)或权值:
- 每条边可以有一个数值,表示通过这条边的“成本”或“代价”。这个数值就是权重。
- 例如,一条路的长度、通过网络连接的延迟、花费的时间或金钱等。
- Dijkstra算法的一个核心前提是,所有边的权重都必须是 非负数(即大于等于0)。
-
路径(Path):
- 从一个节点到另一个节点,经过一系列相连的边的序列。
- 路径长度(Path Length):路径上所有边的权重之和。
- 最短路径(Shortest Path):在所有可能的路径中,路径长度最小的那一条。
1.3 单源最短路径问题(Single-Source Shortest Path Problem)
Dijkstra算法解决的就是这类问题:给定一个带非负权重的图和一个起始节点(源点),找出从源点到图中所有其他节点的最短路径。
为什么强调“非负权重”?
这是一个非常关键的限制。如果图中存在负权边,Dijkstra算法的贪婪策略可能会失效,因为它会错误地认为某个路径已经是最终的最短路径,而实际上通过一条负权边可以进一步缩短它。对于含有负权边的图,你需要使用Bellman-Ford算法或SPFA算法。但在大多数实际应用中,距离、时间、费用等通常都是非负的,所以Dijkstra算法的应用非常广泛。
第二章:Dijkstra 算法核心思想——“贪婪”的探索者
Dijkstra算法的魅力在于其简洁而巧妙的“贪婪”思想。我们不妨把它想象成一个探险家,从起点出发,每次都选择当下看起来“最划算”的路走,最终却能保证找到全局的最短路径。
2.1 形象化理解:水波扩散与导航仪
水波扩散法:
想象你站在一个湖中央(源点),投入一颗石子。水波会以你为中心,一圈一圈地向外扩散。每次水波到达一个新区域,它都代表了从中心到该区域的最短“时间”路径。Dijkstra算法的工作方式与此类似:它从源点开始,像水波一样,不断向外“探测”离源点最近的未访问节点,并更新到达其邻居的距离。
导航仪的工作原理:
当你使用导航仪时,它不会一次性计算出所有可能的路径再选一个,而是从你的当前位置开始,逐步扩展“已确认最短路径”的区域。
- 它首先确认你当前位置到你自己的距离是0。
- 然后,它会查看从你当前位置可以直接到达的那些路口,计算到它们的距离。
- 接着,它会选择其中一个离你最近的路口(假设是路口A),并标记它为“已确认最短路径”。
- 然后,它以路口A为中转点,看通过A能否到达其他未确认最短路径的路口,并且走这条路(你的当前位置 -> A -> 某个路口)是否比之前知道的路径更短。如果是,就更新最短距离。
- 它不断重复这个过程,每次都选择当前“已确认最短路径”区域边缘上离你最近的那个路口进行扩展,直到到达目的地,或者所有路口都被探索完毕。
2.2 Dijkstra 算法的“贪婪”策略
Dijkstra算法的核心策略是:
- 初始化:给所有节点一个“到源点的距离”的估计值。源点到自己的距离是0,其他所有节点到源点的距离初始化为无穷大(表示未知或不可达)。
- 选择最小:在所有未被“确定最短路径”的节点中,选择当前到源点距离最小的那个节点。
- 确定并扩展:一旦一个节点被选中,它的最短路径就被确定了。然后,算法以这个节点为中转,去“松弛”(relax)它的所有邻居节点。所谓“松弛”,就是检查通过当前节点到达其邻居是否比之前记录的路径更短。如果更短,就更新邻居节点的距离。
- 重复:重复步骤2和3,直到所有节点都被确定了最短路径(或者所有可达节点都被访问)。
为什么这种“贪婪”策略在非负权图中有效?
这是因为,当我们选择一个未访问节点中距离源点最近的节点时,我们敢断定这个距离已经是该节点到源点的最短距离了。因为所有边的权重都是非负的,任何通过其他未访问节点再到达当前节点的路径,其总长度都必然会大于或等于当前已知的最短路径(因为任何额外的边都会增加或保持路径长度)。所以,一旦我们确定了某个节点的距离,这个距离就“板上钉钉”了。
第三章:Dijkstra 算法的详细步骤与数据结构
为了实现上述贪婪策略,我们需要借助一些数据结构来高效地管理信息。
3.1 核心数据结构
-
距离数组
dist[]:dist[v]存储从源点到节点v的当前最短距离估计值。- 初始化时,
dist[source] = 0,dist[other_nodes] = 无穷大。 - 在算法执行过程中,这个值会不断更新,直到达到最终的最短距离。
-
前驱节点数组
prev[](可选,用于路径回溯):prev[v]存储在从源点到节点v的最短路径上,v的前一个节点。- 通过回溯这个数组,我们可以重建从源点到任何节点的完整最短路径。
-
已访问集合
visited或布尔数组is_visited[]:- 用于标记哪些节点的最短路径已经被确定。
- 一旦一个节点从优先队列中取出并处理,就将其标记为已访问。
-
优先队列(Priority Queue):
- 这是Dijkstra算法效率的关键。优先队列能够高效地存储
(距离, 节点)对,并且总是能以 O(logN) 的时间复杂度取出当前距离最小的节点。 - 你可以把它理解为一个特殊的队列,每次你都从里面取出“最小”的那个元素,而不是“最早进入”的元素。
- 在Python中,
heapq模块就是一种基于堆(heap)实现的优先队列。
- 这是Dijkstra算法效率的关键。优先队列能够高效地存储
3.2 Dijkstra 算法步骤详解
假设源点为 S,目标是找出 S 到图中所有其他节点的最短路径。
-
初始化:
- 创建
dist数组,将dist[S]设为 0,所有其他节点的dist值设为无穷大。 - 创建
prev数组,所有值设为None(或-1)。 - 创建
is_visited布尔数组,所有值设为False。 - 创建一个空的优先队列
pq。将(0, S)(表示从源点S到S的距离为0)加入优先队列。
- 创建
-
主循环:
- 当优先队列
pq不为空时,重复以下步骤:- 从
pq中取出距离最小的节点u及其对应的距离d_u(即(d_u, u))。 - 检查是否已访问: 如果
u已经被标记为is_visited[u] = True,则跳过当前循环,因为我们已经找到了u的最短路径。 - 标记为已访问: 将
is_visited[u]设为True。此时,dist[u]的值就是从源点S到u的最终最短距离。 - 松弛邻居: 遍历节点
u的所有邻居v,以及边(u, v)的权重weight_uv:- 计算通过
u到达v的新路径距离:new_dist_v = d_u + weight_uv。 - 比较与更新: 如果
new_dist_v < dist[v](即,通过u到达v的路径比目前已知的最短路径更短):- 更新
dist[v] = new_dist_v。 - 更新
prev[v] = u(记录u是v的前驱节点)。 - 将
(dist[v], v)作为一个新的(距离, 节点)对加入优先队列pq。
- 更新
- 计算通过
- 从
- 当优先队列
-
算法结束: 当优先队列为空时,算法终止。此时,
dist数组中存储的就是从源点S到所有可达节点的最短距离。通过prev数组,可以回溯重建任意一条最短路径。
第四章:Dijkstra 算法实战演练——一步步看懂
理解算法最好的方式就是跟着一个例子一步步地走。
4.1 示例图
我们来构建一个简单的有向带权图。
节点:A, B, C, D, E
边及权重:
A -> B (权重: 4)
A -> C (权重: 2)
B -> C (权重: 1)
B -> D (权重: 5)
C -> D (权重: 8)
C -> E (权重: 10)
D -> E (权重: 2)
目标: 从源点 A 开始,找到到达所有其他节点的最短路径。
4.2 初始状态
dist数组:dist[A] = 0dist[B] = 无穷大dist[C] = 无穷大dist[D] = 无穷大dist[E] = 无穷大
prev数组:所有为Noneis_visited数组:所有为False- 优先队列
pq:[(0, 'A')]
4.3 迭代过程
第 1 步:处理节点 A
- 从
pq取出(0, 'A')。 A未被访问,标记is_visited['A'] = True。dist[A]最终确定为 0。- 松弛邻居:
- A 的邻居 B:
- 新距离:
dist[A] + weight(A,B) = 0 + 4 = 4。 4 < dist[B](无穷大)。更新dist[B] = 4,prev[B] = 'A'。pq.push((4, 'B'))。
- 新距离:
- A 的邻居 C:
- 新距离:
dist[A] + weight(A,C) = 0 + 2 = 2。 2 < dist[C](无穷大)。更新dist[C] = 2,prev[C] = 'A'。pq.push((2, 'C'))。
- 新距离:
- A 的邻居 B:
pq状态:[(2, 'C'), (4, 'B')](优先队列会自动排序,最小的在前面)dist状态:{'A': 0, 'B': 4, 'C': 2, 'D': 无穷大, 'E': 无穷大}
第 2 步:处理节点 C (当前 pq 中距离最小的是 (2, 'C'))
- 从
pq取出(2, 'C')。 C未被访问,标记is_visited['C'] = True。dist[C]最终确定为 2。- 松弛邻居:
- C 的邻居 D:
- 新距离:
dist[C] + weight(C,D) = 2 + 8 = 10。 10 < dist[D](无穷大)。更新dist[D] = 10,prev[D] = 'C'。pq.push((10, 'D'))。
- 新距离:
- C 的邻居 E:
- 新距离:
dist[C] + weight(C,E) = 2 + 10 = 12。 12 < dist[E](无穷大)。更新dist[E] = 12,prev[E] = 'C'。pq.push((12, 'E'))。
- 新距离:
- C 的邻居 D:
pq状态:[(4, 'B'), (10, 'D'), (12, 'E')]dist状态:{'A': 0, 'B': 4, 'C': 2, 'D': 10, 'E': 12}
第 3 步:处理节点 B (当前 pq 中距离最小的是 (4, 'B'))
- 从
pq取出(4, 'B')。 B未被访问,标记is_visited['B'] = True。dist[B]最终确定为 4。- 松弛邻居:
- B 的邻居 C:
- 新距离:
dist[B] + weight(B,C) = 4 + 1 = 5。 - 注意!
5不小于dist[C](2)。所以不更新dist[C]和prev[C]。这是因为我们已经找到了通过A到C更短的路径(权重为2)。
- 新距离:
- B 的邻居 D:
- 新距离:
dist[B] + weight(B,D) = 4 + 5 = 9。 9 < dist[D](10)。更新dist[D] = 9,prev[D] = 'B'。pq.push((9, 'D'))。(注意:此时优先队列中可能存在一个旧的(10, 'D'),它会被后面的处理跳过,因为D已经被访问过,或者我们可以在加入时检查。)
- 新距离:
- B 的邻居 C:
pq状态:[(9, 'D'), (10, 'D'), (12, 'E')](这里假设旧的(10, 'D')还在队列中,但不会被处理)dist状态:{'A': 0, 'B': 4, 'C': 2, 'D': 9, 'E': 12}
第 4 步:处理节点 D (当前 pq 中距离最小的是 (9, 'D'))
- 从
pq取出(9, 'D')。 D未被访问,标记is_visited['D'] = True。dist[D]最终确定为 9。- 松弛邻居:
- D 的邻居 E:
- 新距离:
dist[D] + weight(D,E) = 9 + 2 = 11。 11 < dist[E](12)。更新dist[E] = 11,prev[E] = 'D'。pq.push((11, 'E'))。
- 新距离:
- D 的邻居 E:
pq状态:[(10, 'D'), (11, 'E'), (12, 'E')]dist状态:{'A': 0, 'B': 4, 'C': 2, 'D': 9, 'E': 11}
第 5 步:处理节点 D (旧的 (10, 'D'))
- 从
pq取出(10, 'D')。 D已经被访问 (is_visited['D'] = True)。跳过。
第 6 步:处理节点 E (当前 pq 中距离最小的是 (11, 'E'))
- 从
pq取出(11, 'E')。 E未被访问,标记is_visited['E'] = True。dist[E]最终确定为 11。- 松弛邻居:
E没有出边。无事可做。
pq状态:[(12, 'E')]dist状态:{'A': 0, 'B': 4, 'C': 2, 'D': 9, 'E': 11}
第 7 步:处理节点 E (旧的 (12, 'E'))
- 从
pq取出(12, 'E')。 E已经被访问 (is_visited['E'] = True)。跳过。
算法结束: 优先队列为空。
4.4 最终结果
-
最短距离
dist:- A -> A: 0
- A -> B: 4 (路径: A -> B)
- A -> C: 2 (路径: A -> C)
- A -> D: 9 (路径: A -> B -> D)
- A -> E: 11 (路径: A -> B -> D -> E)
-
前驱节点
prev:prev['B'] = 'A'prev['C'] = 'A'prev['D'] = 'B'prev['E'] = 'D'
路径回溯示例(A -> E):
1. 从 E 开始,prev['E'] 是 D。
2. 到 D,prev['D'] 是 B。
3. 到 B,prev['B'] 是 A。
4. 到 A,停止(源点)。
5. 倒序得到路径:A -> B -> D -> E。
第五章:Dijkstra 算法的复杂度分析
了解算法的效率对于实际应用至关重要。Dijkstra算法的效率主要取决于图的表示方式和优先队列的实现。
5.1 时间复杂度
假设图有 V 个节点(Vertex)和 E 条边(Edge)。
- 初始化:O(V)
- 主循环:每个节点最多被从优先队列中取出一次(
V次)。当一个节点被取出时,我们会遍历它的所有出边。每条边(u, v)最多被松弛一次。- 优先队列操作:每次将一个元素插入优先队列或取出最小元素的时间复杂度是 O(log K),其中 K 是优先队列中元素的数量。在最坏情况下,优先队列中可以存储
E个元素(因为每条边可能导致一次插入)。 - 因此,
V次extract_min操作总计V * O(log E)。 E次decrease_key或insert操作(松弛操作导致)总计E * O(log E)。
- 优先队列操作:每次将一个元素插入优先队列或取出最小元素的时间复杂度是 O(log K),其中 K 是优先队列中元素的数量。在最坏情况下,优先队列中可以存储
综合起来,使用二叉堆实现的优先队列时,Dijkstra算法的时间复杂度为 O((V + E) log E)。
由于在连通图中 E >= V - 1,通常可以简化为 O(E log V) 或 O(E log E)。在稀疏图中(E 远小于 V^2),这是非常高效的。
如果使用更高级的优先队列(如斐波那契堆),时间复杂度可以达到 O(E + V log V),但在实际应用中,二叉堆(Python的heapq)已经足够高效且易于实现。
5.2 空间复杂度
Dijkstra算法需要存储:
- 图的表示(邻接表):O(V + E)
dist数组:O(V)prev数组:O(V)is_visited数组:O(V)- 优先队列:最坏情况下 O(E)
因此,Dijkstra算法的空间复杂度为 O(V + E)。
第六章:Dijkstra 算法的 Python 实现
下面是一个使用 Python 及其 heapq 模块实现 Dijkstra 算法的例子。
“`python
import heapq
def dijkstra(graph, start_node):
“””
实现Dijkstra算法,计算从start_node到图中所有其他节点的最短路径。
Args:
graph (dict): 图的邻接表表示,例如:
{'A': {'B': 4, 'C': 2},
'B': {'C': 1, 'D': 5},
'C': {'D': 8, 'E': 10},
'D': {'E': 2},
'E': {}}
start_node (str): 起始节点
Returns:
tuple: (distances, predecessors)
distances (dict): 从start_node到每个节点的最短距离。
predecessors (dict): 记录最短路径中每个节点的前驱节点。
"""
# 初始化距离字典,所有节点距离设为无穷大,起始节点距离设为0
# 获取所有节点名称
nodes = set(graph.keys())
for node_edges in graph.values():
nodes.update(node_edges.keys())
distances = {node: float('infinity') for node in nodes}
distances[start_node] = 0
# 初始化前驱节点字典,用于路径回溯
predecessors = {node: None for node in nodes}
# 优先队列:存储 (距离, 节点) 对,按距离从小到大排序
# 初始时只包含起始节点
priority_queue = [(0, start_node)]
# 已访问节点集合,避免重复处理
visited = set()
while priority_queue:
# 从优先队列中取出当前距离最小的节点
current_distance, current_node = heapq.heappop(priority_queue)
# 如果当前节点已访问,则跳过(避免处理同一节点的旧的、更长的路径)
if current_node in visited:
continue
# 标记当前节点为已访问,表示其最短路径已确定
visited.add(current_node)
# 遍历当前节点的所有邻居
for neighbor, weight in graph.get(current_node, {}).items():
# 计算通过current_node到达neighbor的新距离
distance = current_distance + weight
# 如果新距离比已知的到neighbor的距离更短
if distance < distances[neighbor]:
distances[neighbor] = distance # 更新最短距离
predecessors[neighbor] = current_node # 更新前驱节点
# 将更新后的 (距离, 节点) 对加入优先队列
heapq.heappush(priority_queue, (distance, neighbor))
return distances, predecessors
def reconstruct_path(predecessors, start_node, end_node):
“””
根据前驱节点字典重建从start_node到end_node的最短路径。
“””
path = []
current = end_node
while current is not None:
path.append(current)
if current == start_node:
break
current = predecessors[current]
if path and path[-1] == start_node: # 检查路径是否完整,即是否能追溯到起点
return path[::-1] # 反转路径,使其从起点到终点
else:
return [] # 如果无法到达终点,返回空列表
示例使用
if name == “main“:
# 使用字典表示图的邻接表
example_graph = {
‘A’: {‘B’: 4, ‘C’: 2},
‘B’: {‘C’: 1, ‘D’: 5},
‘C’: {‘D’: 8, ‘E’: 10},
‘D’: {‘E’: 2},
‘E’: {} # E没有出边
}
start_node = 'A'
distances, predecessors = dijkstra(example_graph, start_node)
print(f"从节点 {start_node} 出发的最短距离:")
for node, dist in distances.items():
if dist == float('infinity'):
print(f" 到 {node}: 不可达")
else:
print(f" 到 {node}: {dist}")
print("\n最短路径示例 (A -> E):")
path_to_e = reconstruct_path(predecessors, start_node, 'E')
if path_to_e:
print(f" {' -> '.join(path_to_e)}")
else:
print(f" 无法从 {start_node} 到达 {E_node}")
print("\n最短路径示例 (A -> D):")
path_to_d = reconstruct_path(predecessors, start_node, 'D')
if path_to_d:
print(f" {' -> '.join(path_to_d)}")
else:
print(f" 无法从 {start_node} 到达 {D_node}")
“`
代码解释:
-
dijkstra(graph, start_node)函数:graph:使用字典的字典表示邻接表,外层字典的键是节点,内层字典的键是邻居,值是边的权重。distances:存储从start_node到所有节点的当前最短距离,初始时除了start_node外都为float('infinity')。predecessors:存储每个节点在最短路径中的前一个节点,用于重建路径。priority_queue:使用heapq模块实现的小顶堆,存储(distance, node)元组。每次heappop都会取出距离最小的节点。visited:一个集合,记录已经确定最短路径的节点,避免重复处理。- 主
while循环:只要优先队列不为空,就继续处理。 current_distance, current_node = heapq.heappop(priority_queue):取出当前队列中距离源点最近的节点。if current_node in visited: continue:如果这个节点已经确定了最短路径,就跳过(因为优先队列中可能有更早加入的、更长的路径)。visited.add(current_node):标记当前节点为已访问,它的最短路径已确定。- 遍历
current_node的所有邻居,计算distance。 if distance < distances[neighbor]:如果找到一条更短的路径,就更新distances和predecessors,并将新的(distance, neighbor)对加入优先队列。
-
reconstruct_path(predecessors, start_node, end_node)函数:- 通过回溯
predecessors字典,从end_node逆向找到start_node。 - 最后将路径反转,得到从起点到终点的正确顺序。
- 通过回溯
第七章:Dijkstra 算法的应用场景
Dijkstra算法因其高效和稳定性,在许多领域都有广泛应用:
- 导航与地图应用:这是最直观的应用。高德、百度地图等各种导航系统,都使用Dijkstra或其变种(如A*算法,结合启发式搜索)来计算从起点到终点的最短(或最快)路线。
- 网络路由:互联网中的路由器需要确定数据包从源地址到目标地址的最短路径,以提高传输效率和降低延迟。Dijkstra算法可以用于计算网络拓扑中的最短路径。
- 交通规划:城市交通系统需要规划公交线路、优化交通信号,Dijkstra算法可以帮助找出最少拥堵、最快到达的路径。
- 游戏开发:游戏中的NPC(非玩家角色)需要找到前往目标位置的最短路径,Dijkstra算法是实现这一功能的常见选择。
- 资源分配与调度:在一些复杂的资源网络中,Dijkstra算法可以帮助找到最小成本或最高效率的资源分配路径。
- 生物信息学:在DNA序列分析中,寻找序列之间的最小编辑距离等问题,有时也可以建模为最短路径问题。
第八章:Dijkstra 算法的局限性与替代方案
尽管Dijkstra算法功能强大,但并非万能,它有其特定的局限性:
-
负权边问题:正如之前提到的,Dijkstra算法无法正确处理带有负权边的图。如果图中存在负权边或负权环,它的贪婪策略会失效,可能得不到正确结果。
- 替代方案:对于含有负权边的图,可以使用 Bellman-Ford算法(可以检测负权环)或 SPFA算法(Bellman-Ford的队列优化版本)。
-
单源问题:Dijkstra算法只能解决“单源最短路径”问题,即从一个起点到所有其他点的最短路径。
- 替代方案:如果需要解决“所有点对最短路径”问题(即图中任意两点之间的最短路径),可以使用 Floyd-Warshall算法。
-
效率优化:虽然Dijkstra算法对于稀疏图(边数 E 远小于 V^2)效率很高,但在某些特殊情况下,结合启发式搜索的 A* 算法 可以在目标明确时更快地找到最短路径(如在网格地图中)。A*算法本质上是Dijkstra算法的一种扩展,它通过引入一个启发函数来指导搜索方向,从而减少不必要的探索。
总结:掌握Dijkstra,开启图论大门
恭喜你!到这里,你已经从零基础全面理解了Dijkstra算法。我们从图论的基础概念开始,逐步深入到Dijkstra算法的核心思想、详细步骤、数据结构,并通过一个具体的例子进行了演练,最后还探讨了它的时间空间复杂度、Python实现、实际应用和局限性。
Dijkstra算法不仅仅是一个解决最短路径问题的工具,它更体现了计算机科学中“贪婪选择”和“动态规划”思想的巧妙结合。掌握它,你不仅能够解决实际问题,更能在遇到新的挑战时,培养出分析问题、设计算法的思维能力。
下一步建议:
- 动手实践:尝试修改上述Python代码,用不同的图结构进行测试。
- 挑战变体:思考如何在Dijkstra算法的基础上解决稍微复杂的变体问题(例如,寻找最短路径上的边数最少路径,或者在有向无环图DAG中进行优化)。
- 探索其他算法:了解Bellman-Ford、Floyd-Warshall、A*等其他图算法,比较它们的异同和适用场景。
图论的世界广阔而迷人,Dijkstra算法只是其中的一块敲门砖。希望这篇教程能为你打开一扇新的大门,激发你对算法和数据结构的兴趣!