Dijkstra算法的堆优化版本是针对其朴素算法效率不足的改进,特别适用于稀疏图(边数远小于顶点数平方的图)。下面我将从算法思想和代码实现两方面进行解释。
算法思想
Dijkstra算法用于求解单源最短路径问题,即从给定的起点出发,计算其到图中所有其他顶点的最短路径。它的核心是贪心策略,基本步骤如下:
初始化:将起点到自身的距离设为0,到其他所有顶点的距离初始化为无穷大。
选择未访问最近节点:从所有尚未确定最短路径的顶点中,选出当前距离起点最近的一个。
松弛操作:对于刚选出的这个顶点的所有邻居,检查是否存在一条通过当前顶点到达该邻居的更短路径。如果有,则更新该邻居顶点的最短距离。
标记已访问:将当前顶点标记为已处理,其最短距离已确定。
重复:重复步骤2-4,直到所有顶点都被处理完毕。
在朴素Dijkstra算法中,步骤2“选择未访问最近节点”是通过遍历所有未访问节点来实现的,这使得其时间复杂度为O(n²)。当顶点数量n很大时,效率较低。
堆优化的核心思想在于使用一个最小堆(优先队列) 来维护所有当前“距离起点距离已知但尚未最终确定”的顶点。堆顶元素始终是当前距离最小的顶点。这样,步骤2中获取最小距离节点的操作时间复杂度可以从O(n)降低到O(1)(获取堆顶),而每次调整堆(插入或删除)的时间复杂度为O(log n),从而将算法的整体时间复杂度优化到O((n + m) log n),其中m是边的数量。
代码实现解析
import heapq class Edge: def __init__(self, to, val): self.to = to # 这条边指向的顶点 self.val = val # 边的权重 def dijkstra(n, m, edges, start, end): # 1. 图的构建:使用邻接表 grid = [[] for _ in range(n + 1)] for p1, p2, val in edges: grid[p1].append(Edge(p2, val)) # 存储从p1出发到p2的边 # 2. 初始化数据结构 minDist = [float('inf')] * (n + 1) # 记录起点到每个顶点的最短距离估计值 visited = [False] * (n + 1) # 标记顶点是否已确定最短距离 pq = [] # 最小堆(优先队列) # 起点初始化 minDist[start] = 0 heapq.heappush(pq, (0, start)) # 将起点(距离0, 顶点编号)入堆 # 3. 主循环 while pq: # 3.1 取出当前距离起点最近的未访问顶点 cur_dist, cur_node = heapq.heappop(pq) # 关键优化:如果该顶点已被处理过,则跳过(由于堆中可能存在同一顶点的多个历史距离) if visited[cur_node]: continue # 标记当前顶点为已访问,此时cur_dist就是它的最终最短距离 visited[cur_node] = True # 3.2 松弛操作:遍历当前顶点的所有邻居 for edge in grid[cur_node]: # 如果邻居未被访问,且通过当前顶点到达邻居的距离更短 if not visited[edge.to] and cur_dist + edge.val < minDist[edge.to]: # 更新邻居的最短距离估计值 minDist[edge.to] = cur_dist + edge.val # 将新的距离和邻居顶点入堆。注意:这里入堆的是新的估计值,旧的估计值依然在堆中,但会被上面的continue跳过。 heapq.heappush(pq, (minDist[edge.to], edge.to)) # 返回起点到终点的最短距离,若不可达则返回-1 return -1 if minDist[end] == float('inf') else minDist[end]核心要点与优化解释
邻接表 (grid):使用邻接表而非邻接矩阵来存储图,特别适合边数较少的稀疏图,节省空间。
最小堆 (pq):
heapq模块实现了最小堆。堆中的元素是元组(distance, node)。Python比较元组时按顺序比较,因此以距离作为第一项,能保证堆总是按距离从小到大排序。
visited数组的重要性:这是避免重复处理的关键。当一个顶点从堆中弹出时,如果它已经被标记为visited,说明有更短的路径已经确定了它的距离,当前弹出的这个(可能更大的)距离记录就是无效的,直接跳过。冗余入堆:代码中在更新一个邻居的距离后,会直接将其新距离入堆,而不是先删除堆中该邻居的旧距离。这会导致堆中可能存在同一个顶点的多个不同距离记录。这是一种常见的“惰性删除”优化,虽然增加了堆的大小,但避免了复杂的堆内元素修改操作,在实践中通常更高效。最终的正确性由
visited数组和出堆时的检查来保证。
Bellman-Ford 算法是解决带负权边的有向图单源最短路径问题的一种经典算法。下面我将结合你提供的代码,详细解释其原理和实现。
⚙️ 算法核心思想
Bellman-Ford 算法通过动态规划和松弛操作来求解最短路径。其核心步骤是进行最多V-1轮(V 为顶点数)的松弛操作,确保找到从源点到所有其他顶点的最短路径。如果图中存在从源点可达的负权环,算法能够检测出来。
关键特性:
处理负权边:与 Dijkstra 算法不同,Bellman-Ford 允许图中包含负权重的边。
检测负权环:通过额外的松弛操作可以判断图中是否存在负权环。
简单路径限制:最短路径最多包含
V-1条边,因此最多需要V-1轮松弛。
算法流程与代码解析
以下流程图展示了 Bellman-Ford 算法的主要步骤和你代码中的关键逻辑:
对应你提供的代码,其执行过程如下:
1. 初始化距离数组
minDist = [float("inf")] * (n + 1) # 所有顶点初始距离为无穷大 minDist[1] = 0 # 源点(起点)距离设为0创建一个数组
minDist,用于记录从源点(顶点1)到各个顶点的当前最短距离估计值。初始时,源点自身的距离设为0,其他顶点设为无穷大,表示尚未找到路径。
2. 核心松弛操作
for i in range(1, n): # 最多进行n-1轮松弛 updated = False for src, dest, weight in edges: # 遍历所有边 if minDist[src] != float("inf") and minDist[src] + weight < minDist[dest]: minDist[dest] = minDist[src] + weight # 松弛操作 updated = True if not updated: # 提前终止优化 break外层循环:最多进行
n-1轮。因为任何不含负权环的最短路径最多包含n-1条边。内层循环:遍历图中的所有边。对于每条边
(src, dest, weight),进行松弛操作:检查如果从源点经过顶点src再到顶点dest的路径距离比当前已知到dest的最短距离更短,则更新minDist[dest]。提前终止优化:如果在一轮松弛中没有任何距离被更新,说明所有最短路径已被找到,可以提前结束循环,减少不必要的计算。
3. 结果返回
if minDist[-1] == float("inf"): # 检查终点是否可达 return "unconnected" return minDist[-1] # 返回到达终点的最短距离检查目标顶点(通常是编号为
n的顶点)的最短距离是否仍是无穷大。如果是,则表示从源点无法到达该顶点,返回 "unconnected"。否则,返回计算得到的最短距离。
关键点说明
1. 负权环处理
代码没有显式检测负权环。标准的 Bellman-Ford 算法在完成n-1轮松弛后,会再进行一轮松弛。如果在这一轮中任何顶点的距离还能被更新,则说明图中存在从源点可达的负权环(因为可以不断绕环降低总路径成本)。添加检测的代码如下:
# 在n-1轮松弛之后,添加负权环检测 for src, dest, weight in edges: if minDist[src] != float("inf") and minDist[src] + weight < minDist[dest]: return "图中存在从源点可达的负权环,无最短路径"在你的代码中,由于题目可能确保无负权环,或者只关心连通性,因此省略了此步骤。
2. 时间复杂度与空间复杂度
时间复杂度:代码中有两层循环,外层最多
n-1次,内层遍历所有m条边,因此最坏时间复杂度为 O(nm)*。空间复杂度:主要是存储边集和距离数组,空间复杂度为 O(n + m)