一文彻底搞懂Dijkstra算法

一文彻底搞懂Dijkstra算法(迪杰斯特拉算法)

1. 算法本质

Dijkstra算法 是解决非负权图单源最短路径的经典贪心算法。
由荷兰计算机科学家 Edsger W. Dijkstra 于1956年提出。
核心思想:按路径长度递增的顺序,逐步确定从源点到其余各顶点的最短路径。


2. 适用场景与前提

  • 适用:带权有向图或无向图,边权 ≥ 0。
  • 不适用:含负权边的图(此时应使用 Bellman-Ford 或 SPFA)。
  • 目标:求给定源点 s 到所有其他顶点的最短距离。

3. 核心数据结构

  • dist[]:记录源点到每个顶点的当前最短距离估计值,初始化 dist[s]=0,其余为 ∞。
  • visited[](或 settled[]):标记顶点是否已确定最短距离(永久标号)。
  • 优先队列(小顶堆):快速取出当前 dist 最小的未确定顶点,优化时间复杂度。

4. 算法步骤(手动模拟版)

  1. 初始化:dist[s]=0,其余 dist=∞,所有顶点未访问。
  2. 从未访问顶点中选择 dist 最小的顶点 u(初始为源点)。
  3. 标记 u 为已访问(确定其最短距离)。
  4. 松弛操作:遍历 u 的所有邻接边 (u, v, w),若 dist[u] + w < dist[v],则更新 dist[v] = dist[u] + w。
  5. 重复步骤 2~4,直到所有顶点被访问或剩余 dist 全为 ∞(非连通图)。

5. 图解示例(手动推演)

图:
顶点:A(源点), B, C, D
边:A-B(4), A-C(2), B-C(1), B-D(5), C-D(8)

步骤 已确定顶点 dist[A] dist[B] dist[C] dist[D] 选择
初始 无 0 ∞ ∞ ∞ A
1 A 0 4 2 ∞ C
2 A,C 0 3(4→2+1) 2 10(2+8) B
3 A,C,B 0 3 2 8(3+5) D
结束 全部 0 3 2 8 -

最终最短路径:
A→C(2), A→B(3) [A→C→B], A→D(8) [A→C→B→D]


6. 代码实现(Python + 堆优化)

import heapq

def dijkstra(graph, start):
    # graph: {节点: [(邻接节点, 权重), ...]}
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    pq = [(0, start)]          # (当前距离, 节点)
    visited = set()

    while pq:
        d, u = heapq.heappop(pq)
        if u in visited:
            continue
        visited.add(u)

        for v, w in graph[u]:
            if v not in visited and dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(pq, (dist[v], v))

    return dist

# 示例图(无向)
graph = {
    'A': [('B', 4), ('C', 2)],
    'B': [('A', 4), ('C', 1), ('D', 5)],
    'C': [('A', 2), ('B', 1), ('D', 8)],
    'D': [('B', 5), ('C', 8)]
}
print(dijkstra(graph, 'A'))   # {'A':0, 'B':3, 'C':2, 'D':8}

7. 复杂度分析

实现方式 时间复杂度 空间复杂度
邻接矩阵 + 朴素 O(V²) O(V²)
邻接表 + 二叉堆 O((V+E) log V) O(V+E)
邻接表 + 斐波那契堆 O(E + V log V) O(V+E)

V:顶点数,E:边数。优先队列堆优化是实际中最常用方式。


8. 正确性证明(简要)

  • 不变式:每次从 visited 中取出的顶点 u,其 dist[u] 已是全局最小值。
  • 反证法:若存在更短路径到达 u,则该路径上第一个未访问顶点 x 的 dist[x] 必小于 dist[u],与 u 是当前最小矛盾。
  • 由于所有边权非负,后续路径不可能使 dist[u] 更小,故贪心选择安全。

9. 常见误区与注意事项

  • 不能处理负权边:负权会使贪心失效(可能绕负权环减小距离)。
  • 不一定适用于所有最短路变种:如求最长路、含负权图、多源最短路等。
  • 图必须连通吗? 不必须,非连通部分距离保持 ∞。
  • 堆中可能出现旧数据:用 visited 或比较 d == dist[u] 来跳过过期条目。

10. 路径还原(附加)

若需要输出具体最短路径,维护 parent[] 数组,在松弛时记录 parent[v] = u,最后反向追溯。

def dijkstra_with_path(graph, start):
    dist = {node: float('inf') for node in graph}
    parent = {node: None for node in graph}
    dist[start] = 0
    pq = [(0, start)]
    visited = set()

    while pq:
        d, u = heapq.heappop(pq)
        if u in visited:
            continue
        visited.add(u)

        for v, w in graph[u]:
            if v not in visited and dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                parent[v] = u
                heapq.heappush(pq, (dist[v], v))

    return dist, parent

# 使用示例
dist, parent = dijkstra_with_path(graph, 'A')
# 追溯 A -> D 的路径
path = []
cur = 'D'
while cur is not None:
    path.append(cur)
    cur = parent[cur]
path.reverse()
print(path)  # ['A', 'C', 'B', 'D']

11. 总结

特点 说明
算法类型 贪心算法
适用图 非负权有向图/无向图
时间复杂度 O((V+E) log V)(堆优化)
空间复杂度 O(V+E)
稳定性 稳定,无负权环时必得正确解
典型应用 地图导航、网络路由、任务调度等

一句话记忆:

每次从「未确定」的顶点中挑一个「当前最近」的,然后用它去「松弛」邻居,直到所有顶点都确定下来。

posted @ 2026-08-29 19:27  PHKLSA  阅读(27)  评论(0)    收藏  举报