相关笔记

概览

本节介绍Dijkstra算法,用于解决单源最短路径问题。该算法由荷兰计算机科学家 Edsger W. Dijkstra 于 1956 年提出,适用于所有边权非负的加权有向图。Dijkstra算法是一种贪心算法,其核心思想是:在尚未确定最短路径的顶点中,始终选择当前距离源点最近的顶点,将其标记为”已确定”,然后通过松弛(RELAX)操作更新其邻接顶点的距离估计值。

要点列表:

  • Dijkstra算法的前提条件是所有边权
  • 使用二叉堆实现优先队列时,运行时间为 ====,对连通图即为
  • 使用斐波那契堆实现时,运行时间优化至 ====
  • 使用数组实现时,运行时间为 ====,适合稠密图
  • Dijkstra算法不适用于含负权边的图,这是与22.1 Bellman-Ford算法的关键区别
  • 算法结构与第21章的Prim算法高度相似,区别仅在于key的更新规则

知识结构总览

flowchart TD
    A["22.3 Dijkstra算法"] --> B["前提: 所有边权非负 w(u,v) ≥ 0"]
    A --> C["核心数据结构: 优先队列(最小堆)"]

    B --> D["贪心策略"]
    D --> D1["从源点 s 出发"]
    D --> D2["维护集合 S: 已确定最短路径的顶点"]
    D --> D3["维护集合 Q: 未确定最短路径的顶点"]

    C --> E["优先队列操作"]
    E --> E1["EXTRACT-MIN: 取出 d 值最小的顶点"]
    E --> E2["DECREASE-KEY: 更新顶点的距离估计值"]

    D3 --> F["主循环: while Q ≠ ∅"]
    F --> F1["u ← EXTRACT-MIN(Q)"]
    F --> F2["S ← S ∪ {u}"]
    F --> F3["对 u 的每条出边 (u,v) 执行 RELAX"]

    A --> G["正确性证明"]
    G --> G1["循环不变式: d[u] = δ(s,u)"]
    G --> G2["三步证明: 初始化 → 维持 → 终止"]

    A --> H["复杂度分析"]
    H --> H1["数组: O(V²)"]
    H --> H2["二叉堆: O(E lg V)"]
    H --> H3["斐波那契堆: O(V lg V + E)"]

    A --> I["与其他算法的关系"]
    I --> I1["vs Bellman-Ford: 负权边"]
    I --> I2["vs Prim: key 更新规则"]
    I --> I3["vs BFS: 边权全为 1 的特例"]

核心思想

2.1 前提条件

重要限制

Dijkstra算法要求图中所有边的权重非负,即对每条边 ,都有 。如果图中存在负权边,Dijkstra算法可能产生错误的结果。处理含负权边的图,应使用22.1 Bellman-Ford算法

为什么不能有负权边? Dijkstra算法的核心假设是:一旦某个顶点 被从优先队列中取出(加入集合 ),其距离值 就等于从源点 的最短路径值 ,此后不再被更新。如果存在负权边,一条通过 外顶点的路径可能比已确定的路径更短,但算法不会重新考虑 中的顶点,从而导致错误。

2.2 算法伪代码

Dijkstra算法的伪代码如下:

算法执行流程

  1. 初始化:源点 s 距离设为 0,其余顶点距离设为无穷大,所有顶点加入优先队列 Q
  2. 取出最小顶点:从 Q 中取出距离最小的顶点 u,将其加入已确定集合 S
  3. 松弛出边:对 u 的每条出边 (u,v) 尝试松弛操作,更新 v 的距离估计值
  4. 重复:重复步骤 2-3,直到优先队列 Q 为空
flowchart TD
    A["初始化: d[s]=0, d[v]=∞, Q ← G.V, S ← ∅"] --> B{"Q 非空?"}
    B -->|是| C["u ← EXTRACT-MIN(Q)"]
    C --> D["S ← S ∪ {u}"]
    D --> E["遍历 u 的每条出边 (u,v)"]
    E --> F["RELAX(u, v, w)"]
    F --> B
    B -->|否| G["完成: 所有 d[v] = δ(s,v)"]
DIJKSTRA(G, w, s)
1  INITIALIZE-SINGLE-SOURCE(G, s)
2  S ← ∅
3  Q ← G.V
4  while Q ≠ ∅
5      u ← EXTRACT-MIN(Q)
6      S ← S ∪ {u}
7      for each vertex v ∈ G.Adj[u]
8          RELAX(u, v, w)

其中,INITIALIZE-SINGLE-SOURCERELAX 是第22章通用的子过程:

INITIALIZE-SINGLE-SOURCE(G, s)
1  for each vertex v ∈ G.V
2      v.d ← ∞
3      v.π ← NIL
4  s.d ← 0
RELAX(u, v, w)
1  if v.d > u.d + w(u, v)
2      v.d ← u.d + w(u, v)
3      v.π ← u

变量说明:

  • :从源点 到顶点 当前最短路径估计值(上界估计)
  • :顶点 在最短路径树中的前驱顶点
  • :已经确定最短路径的顶点集合
  • :优先队列(最小堆),以 值为key,存储尚未确定最短路径的顶点
  • :从源点 到顶点 实际最短路径值(真实值)

算法执行流程:

  1. 初始化:将所有顶点的 值设为 值设为 NIL,源点 值设为 0。创建集合 ,将所有顶点加入优先队列
  2. 主循环:当 不为空时,反复执行:
    • 中取出 值最小的顶点 EXTRACT-MIN 操作)
    • 加入集合
    • 的每条出边 执行松弛操作 RELAX(u, v, w)
  3. 终止:当 为空时,所有从 可达的顶点的 值都已等于

直觉理解

Dijkstra算法的核心直觉是:如果所有边权非负,那么当前距离源点最近的未确定顶点,其最短路径不可能再被其他路径改善。就像在一个城市中找最短路线,如果你已经确认了到某个路口的距离是最短的,那么以后再绕路经过其他路口到达这里,只会更远(因为每段路都是正的)。

2.3 算法执行示例

考虑以下有向图,以顶点 为源点:

    10        5
s ───→ t ───→ x
│      │      ↑
│      │      │
5      2      3
│      ↓      │
│      y ────→│
│             │
└─────────────┘
       2

边权如下:

第0次迭代(初始状态):

顶点
0
NILNILNILNIL

第1次迭代: EXTRACT-MIN(Q) 取出 最小),

松弛 的邻边:

  • ,更新
  • ,更新
顶点
0105
NILNIL

第2次迭代: EXTRACT-MIN(Q) 取出 最小),

松弛 的邻边:

  • ,更新
  • ,更新
顶点
0875
NIL

第3次迭代: EXTRACT-MIN(Q) 取出 最小),

松弛 的邻边:

  • ,跳过
顶点
0875
NIL

第4次迭代: EXTRACT-MIN(Q) 取出 ),

松弛 的邻边:

  • ,跳过
  • ,跳过
顶点
0875
NIL

,算法终止。

最终最短路径树:

  • (权5)
  • (权3)
  • (权2)

2.4 正确性证明

定理 22.6(Dijkstra算法的正确性)

是一个加权有向图,其所有边权非负, 为源点。则对于从 可达的每个顶点 ,在 Dijkstra 算法终止时,,且前驱子图 是一棵以 为根的最短路径树。

证明思路: 使用循环不变式进行证明。

循环不变式: 在每次执行 while 循环体之前(即每次 EXTRACT-MIN 操作之前),有如下性质:

对每个顶点

即:集合 中的每个顶点的 值已经等于其真正的最短路径值。

证明分三步:

【循环不变式初始化: 时空集上全称命题平凡成立】 第一步:初始化。 在第一次进入 while 循环之前,,循环不变式平凡成立(空集上的全称命题为真)。

【循环不变式维持:反证法证明 第二步:维持。 假设在某次迭代开始时循环不变式成立,我们需要证明在本次迭代结束后它仍然成立。

是本次迭代中通过 EXTRACT-MIN(Q) 取出的顶点。我们需要证明

【反证:假设 ,构造最短路径上第一个不在 中的顶点 用反证法。假设 。由于 始终是 的上界(由松弛操作的性质保证),所以

考虑从 的一条最短路径 。由于 刚从 中取出),路径 上一定存在第一条不在 中的顶点。设 是这条路径上第一个不在 中的顶点, 在路径上的前驱(因此 )。

路径 可以分解为:

【由循环不变式 ,松弛 根据循环不变式的归纳假设,

由于 之前被加入 ,当 被处理时,边 被松弛,因此:

另一方面,由松弛操作的性质, 始终成立。因此:

又因为所有边权非负,路径 上的边权之和 ,所以:

【边权非负 ,又 (EXTRACT-MIN),矛盾】

值最小的顶点(因为它是 EXTRACT-MIN 的结果),所以:

综合以上不等式:

这与假设 矛盾。因此

加入 后, 中所有顶点的 值都等于其最短路径值,循环不变式得以维持。

第三步:终止。 当循环终止时,,因此 。由循环不变式,对所有 ,如果 可达,则 。由最短路径性质(定理22.5),前驱子图 是一棵以 为根的最短路径树。

2.5 不适用于负权边的原因

负权边导致错误

Dijkstra算法在存在负权边时可能产生错误结果。以下是一个简单的反例:

考虑图:(权1),(权4),(权

为源点运行 Dijkstra 算法:

  1. 初始:
  2. 取出 ,松弛邻边:
  3. 取出 ),松弛
  4. 取出

在这个例子中,结果恰好是正确的。但如果我们修改边权:

考虑图:(权2),(权5),(权

  1. 初始:
  2. 取出 ,松弛邻边:
  3. 取出 ),松弛
  4. 取出

结果仍然正确。关键问题出现在以下情况:

考虑图:(权3),(权2),(权5),(权

  1. 初始:
  2. 取出 ,松弛:
  3. 取出 ),松弛 (不变)
  4. 取出 ),松弛 :但 已经在 中,不会被重新考虑
  5. 算法结束:

但实际上,路径 的长度为 ,所以 应该是 ,而不是 Dijkstra算法给出了错误答案!

根本原因:当 被加入 时,算法”认为”到 的最短路径已经确定。但后来通过负权边 发现了一条更短的路径,而算法不会回头更新 中的顶点。

2.6 复杂度分析

Dijkstra算法的运行时间取决于优先队列的实现方式。设

操作分析:

  • INITIALIZE-SINGLE-SOURCE
  • EXTRACT-MIN:执行
  • DECREASE-KEY(隐含在 RELAX 中):最多执行

三种实现的复杂度

1. 数组实现:

  • EXTRACT-MIN:遍历数组找最小值,,共 次,总计
  • DECREASE-KEY:直接修改数组元素,,共 次,总计
  • 总运行时间:(因为
  • 适合稠密图

2. 二叉堆实现:

  • 建堆:
  • EXTRACT-MIN,共 次,总计
  • DECREASE-KEY,共 次,总计
  • 总运行时间:
  • 对连通图(),简化为
  • 适合稀疏图

3. 斐波那契堆实现:

  • EXTRACT-MIN 摊还,共 次,总计
  • DECREASE-KEY 摊还,共 次,总计
  • 总运行时间:
  • 时优于二叉堆

如何选择优先队列实现

实际工程中,对于稀疏图(如道路网络),二叉堆是最常用的选择,因为实现简单且性能优秀。斐波那契堆虽然理论最优,但常数因子大、实现复杂,实际应用较少。对于稠密图(如完全图),数组实现反而更高效。

2.7 与Prim算法的对比

Dijkstra算法与第21章的Prim算法在结构上几乎完全相同。两者的伪代码对比如下:

Prim算法(MST-PRIM):

MST-PRIM(G, w, r)
1  for each u ∈ G.V
2      u.key ← ∞
3      u.π ← NIL
4  r.key ← 0
5  Q ← G.V
6  while Q ≠ ∅
7      u ← EXTRACT-MIN(Q)
8      for each v ∈ G.Adj[u]
9          if v ∈ Q and w(u, v) < v.key
10             v.π ← u
11             v.key ← w(u, v)

Dijkstra算法(DIJKSTRA):

DIJKSTRA(G, w, s)
1  INITIALIZE-SINGLE-SOURCE(G, s)    // d[s]←0, d[v]←∞
2  S ← ∅
3  Q ← G.V
4  while Q ≠ ∅
5      u ← EXTRACT-MIN(Q)
6      S ← S ∪ {u}
7      for each v ∈ G.Adj[u]
8          RELAX(u, v, w)             // if d[v] > d[u]+w(u,v): d[v]←d[u]+w(u,v)

核心区别在于key的更新规则:

对比维度Prim算法Dijkstra算法
目标最小生成树单源最短路径
key含义连接到已选顶点集的最小边权从源点到该顶点的最短路径估计
更新规则
集合 已加入MST的顶点已确定最短路径的顶点
起始顶点任意顶点 源点
前提条件连通加权无向图边权非负的有向/无向图

记忆方法

Prim的key只看”一步之遥”的边权,Dijkstra的key看的是”从源点出发的累计距离”。Prim关心的是”怎么用最短的边把新顶点连进来”,Dijkstra关心的是”从源点走到这里总共要多远”。

2.8 与BFS的关系

第20章介绍的广度优先搜索(BFS)可以看作Dijkstra算法的一个特例

当图中所有边的权重都为1时:

Dijkstra算法的松弛操作变为:

这与BFS中通过层次遍历更新距离的方式完全一致。此时,优先队列中的 值恰好对应BFS的层次编号,EXTRACT-MIN 取出的顶点顺序与BFS的遍历顺序相同。

更一般地,如果所有边权相同(设为 ),Dijkstra算法等价于BFS(结果只需乘以常数 )。

从BFS到Dijkstra的认知桥梁

BFS解决的是”无权图”中的最短路径问题,Dijkstra解决的是”非负权图”中的最短路径问题。BFS用FIFO队列(先进先出),Dijkstra用优先队列(按距离排序)。两者都是贪心策略的体现:BFS贪心地认为”先发现的路径更短”,Dijkstra贪心地认为”当前距离最小的顶点已确定”。


补充理解

3.1 Dijkstra算法的发明历史

Dijkstra算法的诞生有一个广为流传的故事。荷兰计算机科学家 Edsger Wybe Dijkstra(1930—2002)在1956年构思了这个算法。

当时Dijkstra只有26岁,在阿姆斯特丹的荷兰数学中心(Mathematisch Centrum,现CWI)工作。他需要为计算机编程解决最短路径问题,但当时可用的计算机只有X1(一台早期计算机),而且他不想用机器语言或汇编语言来编程。

“一天早上,我和年轻的未婚妻在阿姆斯特丹购物,累了,我们就在咖啡馆的露台上坐下来喝杯咖啡。我正在思考是否能做到这一点,然后我就设计出了最短路径算法。” —— Edsger W. Dijkstra

Dijkstra在1959年发表了这篇论文,题目为 “A note on two problems in connexion with graphs”,发表在 Numerische Mathematik 期刊上。这篇仅有三页的论文同时提出了Dijkstra算法和最小生成树算法(Prim算法的一个等价版本)。

Dijkstra后来获得了1972年的图灵奖,他的贡献远不止最短路径算法,还包括信号量(semaphore)的概念、自顶向下编程方法论、以及著名的”Go To语句有害论”

参考来源:Edsger Dijkstra and the shortest-path algorithm - Cornell

3.2 GPS导航与地图路由

Dijkstra算法在现代生活中最直接的应用就是GPS导航系统。Google Maps、Apple Maps、高德地图、百度地图等导航软件,其核心路径规划引擎都基于Dijkstra算法或其变种。

然而,直接在道路网络上运行Dijkstra算法效率不够高。一个国家的道路网络可能有数千万个节点和数亿条边,虽然 的时间复杂度在理论上可行,但在实际应用中需要进一步优化。

实际工程中的优化技术:

  1. A* 算法:在Dijkstra的基础上加入启发函数 ,估计从顶点 到目标点的距离。优先队列的key变为 ,从而引导搜索朝目标方向进行,大幅减少探索的顶点数
  2. 双向Dijkstra:同时从源点和目标点运行Dijkstra算法,在中间相遇时停止。可将搜索空间减半
  3. 收缩层次(Contraction Hierarchies, CH):预处理阶段通过”收缩”不重要的节点来构建层次结构,查询时只需在高层次路径上搜索
  4. Hub标签(Hub Labeling):预处理阶段为每个节点计算”hub标签”,查询时只需比较两个节点的标签交集
  5. 分层路由:将道路网络分为不同层级(如高速公路、主干道、地方道路),优先在高层级道路上搜索

3.3 A*算法与Dijkstra的关系

A* 算法是最著名的路径搜索算法之一,广泛应用于游戏AI、机器人导航等领域。它与Dijkstra算法的关系非常密切:

具体区别:

维度DijkstraA*
优先队列key(从源点到 的距离)
搜索方向向所有方向均匀扩展朝目标方向引导扩展
启发函数无( 估计 到目标的距离
完备性是(需 可容许)
最优性是(需 可容许且一致)
效率探索较多顶点通常探索更少顶点

可容许启发函数(Admissible Heuristic): ,即启发函数永远不会高估从 到目标 的实际最短距离。

对所有 成立时,A* 退化为Dijkstra算法。因此,Dijkstra是A* 的特例

3.4 Dijkstra算法的其他应用

除了路径规划,Dijkstra算法还广泛应用于:

  • 网络路由协议:OSPF(Open Shortest Path First)协议使用Dijkstra算法计算最短路径树
  • 社交网络分析:计算用户之间的最短关系链
  • 编译器优化:寄存器分配中的干涉图分析
  • 机器人学:运动规划和避障
  • 游戏开发:NPC寻路(通常使用A*,但Dijkstra是基础)

易混淆点

4.1 Dijkstra中的key与dist

4.2 Dijkstra vs Bellman-Ford

4.3 Dijkstra vs Prim

4.4 为什么DECREASE-KEY是关键操作

4.5 Dijkstra能否处理无向图?

4.6 源点出发的边可以有负权吗?


习题精选

习题 22.3-1(对应第3版 24.3-1)

为源点的 值变化:

为源点的 值变化:

为源点的 值变化:

为源点的 值变化:

习题 22.3-2(对应第3版 24.3-2)

反例: 考虑一个包含负权环的图。RELAX 只被调用有限次,但负权环上每个顶点的最短路径值为 ,因此Dijkstra算法不可能给出正确结果。

证明失效的原因: 定理22.6的证明依赖于不等式 。当存在负权边时,这个不等式不再成立,因为从 的路径上可能有负权边,使得 ,从而

习题 22.3-3(对应第3版 24.3-3)

答案:是的,算法仍然正确。

【未取出顶点 :不可达时 ;可达时前驱边已被松弛, 是唯一一个未被从优先队列 中取出的顶点。

  • 如果 不可达,则 ,结果正确
  • 如果 可达,则存在一条最短路径 。当顶点 被取出时,(由循环不变式保证),然后边 被松弛,因此

无论哪种情况, 在算法终止时都等于

习题 22.3-4(对应第3版 24.3-4)

思路: 检查以下条件:

  1. 源点检查
  2. 前驱树检查 值构成一棵以 为根的树(通过从每个顶点沿 回溯到 来验证,总时间
  3. 边权一致性检查:对每个 ,检查 ,总时间
  4. 最短路径检查:对每条边 ,检查 (三角不等式),总时间

如果以上所有条件都满足,则输出确实对应一棵最短路径树。总运行时间

习题 22.3-5(对应第3版 24.3-5)

反例构造: 考虑以下图:

  • 顶点:
  • 边: 权1, 权3, 权1, 权1

的最短路径为 ,长度为

Dijkstra算法的执行过程:

  1. 取出 ,松弛
  2. 取出 ),松弛
  3. 取出 ),松弛
  4. 取出

注意:边 在第1步被松弛,但最短路径 中并不包含 。更关键的是,边 在边 之后才被松弛,而如果 是最短路径上的边,这种”乱序”松弛就不会影响结果。但Newman教授声称的是”按照最短路径上边的出现顺序松弛”,这在某些图结构中并不成立。

习题 22.3-6(对应第3版 24.3-6)

解法: 将问题转化为最短路径问题。

一条路径 的可靠性为各边可靠性的乘积:

要最大化 ,等价于最大化

由于 ,所以 。令 ,则最大化 等价于最小化

由于 ,可以直接使用Dijkstra算法求解。运行时间为 (使用二叉堆)。

习题 22.3-7(对应第3版 24.3-7)

顶点数: 的顶点数为 。每条权为 的边被替换为 条串联边,需要 个额外的中间顶点。

正确性论证: 中,从 的BFS距离恰好等于 中从 的最短路径权值。由于没有两个顶点的最短路径权值相同,BFS将按照最短路径权值从小到大的顺序将 中的顶点染黑,这与Dijkstra算法中 EXTRACT-MIN 的取出顺序完全一致。

习题 22.3-8(对应第3版 24.3-8)

解法: 由于边权为非负整数且不超过 ,可以使用一个大小为 桶数组(bucket array)代替优先队列。

每个桶 存储所有 值等于 的顶点。维护一个当前最小距离的指针 cur

  • EXTRACT-MIN:从 中取出一个顶点,如果 为空则递增 cur。均摊
  • DECREASE-KEY:将顶点从旧桶移到新桶。

由于 cur 最多从 0 增长到 (最短路径的最大可能值),总运行时间为

习题 22.3-9(对应第3版 24.3-9)

提示: 中任意时刻有多少个不同的最短路径估计值?

解法: 关键观察是:在任意时刻, 中顶点的 值最多有 个不同的取值(因为每次松弛操作将 设为 ,其中 ,所以 值的跨度不超过 )。

使用一个最小-最大堆(min-max heap)或多层桶(radix heap)数据结构,可以在 时间内完成 EXTRACT-MINDECREASE-KEY

  • EXTRACT-MIN,共 次,总计
  • DECREASE-KEY,共 次,总计
  • 总运行时间:

习题 22.3-10(对应第3版 24.3-10)

如果 出现在从 的一条最短路径上且 ,那么从 的路径上的所有边都具有非负权值(因为只有从 出发的边可能有负权,而 ,所以从 出发的边不可能有负权)。如果从 的路径上有负权边,这意味着路径经过了某条从 出发的负权边,这暗示路径中包含一个环,而只有当该环是负权环时才会导致问题,但题目已排除负权环。

因此 仍然成立,整个证明链不受影响。


视频学习指南

推荐学习资源

中文资源:

  • bilibili - 王道考研 数据结构:搜索”Dijkstra算法”,有详细的动画演示和手算过程
  • bilibili - 代码随想录:搜索”Dijkstra”,有算法实现和LeetCode题目讲解

英文资源:

  • MIT 6.006 Introduction to Algorithms(Lecture 17):Dijkstra算法的完整讲解,包含正确性证明
  • Abdul Bari - Dijkstra’s Algorithm(YouTube):10分钟直观讲解,适合快速理解
  • WilliamFiset - Dijkstra’s Algorithm(YouTube):有详细的逐步动画演示

可视化工具:


教材原文

CLRS 第4版 第22.3节(英文原版摘要)

“Dijkstra’s algorithm solves the single-source shortest-paths problem on a weighted, directed graph for the case in which all edge weights are nonnegative.”

“Dijkstra’s algorithm maintains a set of vertices whose final shortest-path weights from the source have already been determined. The algorithm repeatedly selects the vertex with the minimum shortest-path estimate, adds to , and relaxes all edges leaving .”

“The algorithm uses a min-priority queue keyed on the attributes. The implementation of EXTRACT-MIN and DECREASE-KEY determines the running time.”

教材原文参考

完整原文请参阅《算法导论(第4版)》第22章第3节,或原始素材目录中的PDF文件:

  • 00-Raw素材/算法导论_第四版/Part_VI_Graph_Algorithms/22_Single-Source_Shortest_Paths/22.3_Dijkstra's_algorithm.pdf

参见Wiki

概念页尚未创建

以下概念页待后续补充:

第22章-单源最短路径 Dijkstra算法