迪杰斯特拉算法
1. 算法概述
迪杰斯特拉(Dijkstra)算法是一种用于求解带权有向图或无向图中单个源节点到其他所有节点的最短路径的贪心算法。该算法要求图中所有边的权值非负,其核心思想是从源节点开始,逐步扩展到距离源节点最近的节点,并更新这些节点到源节点的最短距离,直到所有可达节点的最短距离都被确定。
2. 算法步骤
2.1 初始化
- 距离数组:创建一个数组
dist
,用于存储源节点到图中每个节点的最短距离。初始时,将源节点的距离设为 0,其他节点的距离设为无穷大(通常用一个很大的数表示)。 - 访问标记数组:创建一个布尔型数组
visited
,用于标记每个节点是否已经被访问过。初始时,所有节点的标记都为False
。 - 优先队列(可选):可以使用优先队列(最小堆)来优化算法的时间复杂度。优先队列中存储的元素是节点及其到源节点的当前最短距离,按照距离从小到大排序。
2.2 迭代过程
- 选择未访问节点:从
dist
数组中选择距离源节点最近且未被访问过的节点u
。在使用优先队列的情况下,直接从队列中取出距离最小的节点。 - 标记节点:将节点
u
标记为已访问,即visited[u] = True
。 - 更新邻接节点距离:遍历节点
u
的所有邻接节点v
,如果通过节点u
到达节点v
的距离比当前记录的dist[v]
更小,则更新dist[v]
的值。具体来说,若dist[u] + weight(u, v) < dist[v]
,则dist[v] = dist[u] + weight(u, v)
,其中weight(u, v)
是边(u, v)
的权值。
2.3 终止条件
当所有可达节点都被访问过或者优先队列为空时,算法结束。此时,dist
数组中存储的就是源节点到每个节点的最短距离。
3. 代码实现(Python)
import heapq
from collections import defaultdictdef dijkstra(graph, start):# dist 字典用于记录从源节点到图中每个节点的当前最短距离。# 初始化这个字典dist = {node: float('inf') for node in graph}dist[start] = 0# 初始化优先队列# 使用列表并借助 heapq 模块来模拟最小堆实现优先队列# 主要用于高效地选择距离源节点最近且未被访问过的节点,从而优化算法的时间复杂度pq = [(0, start)]while pq:# 从优先队列中取出距离最小的节点current_dist, current_node = heapq.heappop(pq)# 如果节点已经被访问过,跳过if current_dist > dist[current_node]:continue# 遍历当前节点的邻接节点for neighbor, weight in graph[current_node].items():distance = current_dist + weight# 如果通过当前节点到达邻接节点的距离更短,更新距离if distance < dist[neighbor]:dist[neighbor] = distanceheapq.heappush(pq, (distance, neighbor))return dist# 示例图的邻接表表示
graph = {'A': {'B': 1, 'C': 4},'B': {'A': 1, 'C': 2, 'D': 5},'C': {'A': 4, 'B': 2, 'D': 1},'D': {'B': 5, 'C': 1}
}start_node = 'A'
shortest_distances = dijkstra(graph, start_node)
print(f"从节点 {start_node} 到其他节点的最短距离: {shortest_distances}")
4. 复杂度分析
- 时间复杂度:使用优先队列(最小堆)实现时,时间复杂度为 O((V+E)logV)O((V + E) \log V)O((V+E)logV),其中 VVV 是图中节点的数量,EEE 是图中边的数量。每次从优先队列中取出最小元素的时间复杂度为 O(logV)O(\log V)O(logV),总共需要进行 VVV 次操作;对于每条边,最多需要进行一次入队操作,时间复杂度为 O(logV)O(\log V)O(logV),总共需要进行 EEE 次操作。
- 空间复杂度:主要用于存储距离数组、优先队列和图的邻接表,空间复杂度为 O(V+E)O(V + E)O(V+E)。
5. 注意事项
- 迪杰斯特拉算法要求图中所有边的权值非负。如果图中存在负权边,该算法可能无法得到正确的最短路径,此时可以使用贝尔曼 - 福特(Bellman - Ford)算法。
- 该算法适用于求解单个源节点到其他所有节点的最短路径。如果需要求解所有节点对之间的最短路径,可以使用弗洛伊德(Floyd)算法。
6. 其他
6.1 为什么使用优先队列?
在迪杰斯特拉算法中,优先队列(这里使用的是一个列表并借助 heapq
模块来模拟最小堆实现优先队列)主要用于高效地选择距离源节点最近且未被访问过的节点,从而优化算法的时间复杂度。下面为你详细解释其作用。
迪杰斯特拉算法核心需求
迪杰斯特拉算法的核心步骤之一是在每一轮迭代中,从所有未确定最短路径的节点里,选出距离源节点最近的节点。若不使用优先队列,就需要遍历所有未访问节点的距离数组,找出最小距离对应的节点,这个操作的时间复杂度是 O(V)O(V)O(V),其中 VVV 是图中节点的数量。在每一轮迭代都进行这样的操作,会使算法的整体时间复杂度较高。
优先队列的作用
- 高效选择最小距离节点:优先队列可以按照节点到源节点的距离进行排序,每次从队列中取出距离最小的节点,时间复杂度为 O(logV)O(\log V)O(logV)。在 Python 里,
heapq
模块提供了堆操作的功能,能将列表当作最小堆来使用。在初始化优先队列时,pq = [(0, start)]
把源节点及其到自身的距离(为 0)加入队列。 - 动态更新节点距离:当通过某个已访问节点更新了其邻接节点的距离时,如果新的距离比之前记录的距离更小,就把更新后的距离和对应的节点加入优先队列。优先队列会自动调整元素顺序,保证队首元素始终是距离源节点最近的节点。
代码示例解释
import heapq
from collections import defaultdictdef dijkstra(graph, start):# 初始化距离数组dist = {node: float('inf') for node in graph}dist[start] = 0# 初始化优先队列pq = [(0, start)]while pq:# 从优先队列中取出距离最小的节点current_dist, current_node = heapq.heappop(pq)# 如果节点已经被访问过,跳过if current_dist > dist[current_node]:continue# 遍历当前节点的邻接节点for neighbor, weight in graph[current_node].items():distance = current_dist + weight# 如果通过当前节点到达邻接节点的距离更短,更新距离if distance < dist[neighbor]:dist[neighbor] = distanceheapq.heappush(pq, (distance, neighbor))return dist
- 初始化优先队列:
pq = [(0, start)]
把源节点及其到自身的距离(0)作为一个元组加入优先队列。 - 取出最小距离节点:
current_dist, current_node = heapq.heappop(pq)
从优先队列中取出距离最小的节点及其距离。 - 更新邻接节点距离:当发现通过当前节点到达邻接节点的距离更短时,使用
heapq.heappush(pq, (distance, neighbor))
把更新后的距离和邻接节点加入优先队列。
复杂度分析
使用优先队列后,迪杰斯特拉算法的时间复杂度从 O(V2)O(V^2)O(V2) 优化到了 O((V+E)logV)O((V + E) \log V)O((V+E)logV),其中 VVV 是节点数量,EEE 是边的数量。优先队列在取出最小元素和插入新元素时的时间复杂度都是 O(logV)O(\log V)O(logV),而对于每条边最多进行一次插入操作,所以总的时间复杂度得到了优化。