Dijkstra算法的堆优化版本是针对其朴素算法效率不足的改进,特别适用于稀疏图(边数远小于顶点数平方的图)。下面我将从算法思想和代码实现两方面进行解释。
算法思想
Dijkstra算法用于求解单源最短路径问题 ,即从给定的起点出发,计算其到图中所有其他顶点的最短路径。它的核心是贪心策略,基本步骤如下:
-
初始化:将起点到自身的距离设为0,到其他所有顶点的距离初始化为无穷大。
-
选择未访问最近节点:从所有尚未确定最短路径的顶点中,选出当前距离起点最近的一个。
-
松弛操作:对于刚选出的这个顶点的所有邻居,检查是否存在一条通过当前顶点到达该邻居的更短路径。如果有,则更新该邻居顶点的最短距离。
-
标记已访问:将当前顶点标记为已处理,其最短距离已确定。
-
重复:重复步骤2-4,直到所有顶点都被处理完毕。
在朴素Dijkstra算法中,步骤2"选择未访问最近节点"是通过遍历所有未访问节点来实现的,这使得其时间复杂度为O(n²)。当顶点数量n很大时,效率较低。
堆优化的核心思想 在于使用一个最小堆(优先队列) 来维护所有当前"距离起点距离已知但尚未最终确定"的顶点。堆顶元素始终是当前距离最小的顶点。这样,步骤2中获取最小距离节点的操作时间复杂度可以从O(n)降低到O(1)(获取堆顶),而每次调整堆(插入或删除)的时间复杂度为O(log n),从而将算法的整体时间复杂度优化到O((n + m) log n),其中m是边的数量。
代码实现解析
python
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. 初始化距离数组
python
minDist = [float("inf")] * (n + 1) # 所有顶点初始距离为无穷大
minDist[1] = 0 # 源点(起点)距离设为0
-
创建一个数组
minDist,用于记录从源点(顶点1)到各个顶点的当前最短距离估计值。 -
初始时,源点自身的距离设为0,其他顶点设为无穷大,表示尚未找到路径。
2. 核心松弛操作
python
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)