图谱连接

草稿

Dijkstra 算法

在非负带权图中,每次确定当前暂定总代价最低的节点,从而找到从起点出发的最低代价路径。

algorithm intermediate graphsalgorithmsshortest-paths

问题从哪里来

当每条边都只算一步时,BFS 很好用。但道路、网络连接或选择常常有不同代价。现在问题不再是”几条边”,而是”从 A 出发的最小总代价是多少”。

地图现在带权:一条边可能比两条边更贵。Dijkstra 开始前的带权图。
412158361AA: tentative, dist 0CC: unseen, dist InfinityBB: unseen, dist InfinityDD: unseen, dist InfinityEE: unseen, dist InfinityFF: unseen, dist Infinity
追踪start
A:0
当前-
-
规则权重是明确标签;视觉长度不是代价。
从 A 出发的最终距离
节点distparent
A0-
B3C
C1A
D4B
E7D
F8E

第一个朴素想法

仍然尝试 BFS。它会从 A 直接发现 B,并保留一条边的路径 A -> B,即使 A -> C -> B 的总代价更低。

BFS 会先保留 A-B,但 A-C-B 代价更低。BFS 在带权图中的失败点。
412158361AA: current, dist 0CC: unseen, dist InfinityBB: tentative, dist 4DD: unseen, dist InfinityEE: unseen, dist InfinityFF: unseen, dist Infinity
追踪relax-A-B
B:4
当前A
A-B
规则按边数第一次发现不等于总代价最低。

它为什么会痛

在无权图中,第一次发现就足够。在带权图中,按边数第一次发现可能冻结错误的父节点或代价。

A-B 代价为 4;A-C-B 代价为 3。到 B 的路线代价对比。
412158361AA: settled, dist 0CC: current, dist 1BB: tentative, dist 3DD: unseen, dist InfinityEE: unseen, dist InfinityFF: unseen, dist Infinity
追踪relax-C-B
B:3, B:4*
当前C
C-B
规则比较总代价,而不是边数。

核心发明

Dijkstra 保留 BFS 的前沿思想,但前沿按暂定总代价排序。普通队列弹出最早进入的条目;优先队列(priority queue)弹出标签最小的条目。

优先队列取代先进先出顺序。C 更新 B 后的暂定距离表和堆。
412158361AA: settled, dist 0CC: current, dist 1BB: tentative, dist 3DD: unseen, dist InfinityEE: unseen, dist InfinityFF: unseen, dist Infinity
追踪relax-C-B
B:3, B:4*
当前C
C-B
规则优先级来自最小暂定距离。

松弛脚手架

松弛(relaxation)会问:经过当前节点能不能改善某个邻居?对于 C -> B,旧的 dist[B]4,而 dist[C] + 2 = 3,所以 B 被改善,父节点改为 C

C -> B 把 B 从 4 改善到 3,并把父节点改为 C。从 C 到 B 的松弛。
412158361AA: settled, dist 0CC: current, dist 1BB: tentative, dist 3DD: unseen, dist InfinityEE: unseen, dist InfinityFF: unseen, dist Infinity
追踪relax-C-B
B:3, B:4*
当前C
C-B
规则当候选值小于当前暂定距离时更新。

互动可视化

412158361AA: tentative, dist 0CC: unseen, dist InfinityBB: unseen, dist InfinityDD: unseen, dist InfinityEE: unseen, dist InfinityFF: unseen, dist Infinity

Dijkstra 追踪

把 A 初始化为距离 0;其他节点都是 Infinity。

当前-
当前边-
弹出-
已确定-
优先队列:A:0

为便于阅读而排序展示;真实堆只保证下一次取出最小项。

节点距离父节点
A0-
BInfinity-
CInfinity-
DInfinity-
EInfinity-
FInfinity-

形式化版本

对于每条边 (u, v),Dijkstra 要求 w(u, v) >= 0。算法维护暂定距离 dist[v]、父节点 parent[v]、已确定节点集合,以及候选条目的优先队列。

形式化状态命名当前节点、当前边、候选值、堆和表格。扫描 D-E 时的 Dijkstra 形式化状态。
412158361AA: settled, dist 0CC: settled, dist 1BB: settled, dist 3DD: current, dist 4EE: tentative, dist 9FF: unseen, dist Infinity
追踪scan-D-E
D:6*, E:9
当前D
D-E
规则candidate = dist[current] + weight(current, neighbor)。

算法会计算所有可达节点的距离。后面检查 F 只是为了演示如何恢复一条路径,并不是说 Dijkstra 只能处理单个目标。

实现草图

function dijkstra(start: NodeId, graph: WeightedGraph) {
  const dist = new Map<NodeId, number>([[start, 0]]);
  const parent = new Map<NodeId, NodeId>();
  const settled = new Set<NodeId>();
  const heap = new MinHeap([{ node: start, distance: 0 }]);

  while (!heap.isEmpty()) {
    const entry = heap.popMin();
    if (entry.distance !== dist.get(entry.node) || settled.has(entry.node)) continue;

    settled.add(entry.node);
    for (const edge of graph[entry.node]) {
      const candidate = entry.distance + edge.weight;
      if (settled.has(edge.to)) continue;
      if (candidate < (dist.get(edge.to) ?? Infinity)) {
        dist.set(edge.to, candidate);
        parent.set(edge.to, entry.node);
        heap.push({ node: edge.to, distance: candidate });
      }
    }
  }

  return { dist, parent };
}

允许重复的堆条目。过期条目保护会跳过那些距离已经不等于当前最佳 dist[node] 的旧条目。

过期条目保护让重复堆条目更简单。过期跳过的代码到追踪映射。
412158361AA: settled, dist 0CC: settled, dist 1BB: current, dist 3DD: tentative, dist 4EE: tentative, dist 9FF: unseen, dist Infinity
追踪skip-stale-B-4
D:4, D:6*, E:9
当前B
-
规则弹出的条目过期时跳过。

正确性直觉

在确定 B 之前,堆中最小项是 (B, 3),其他暂定条目至少是 4。任何经过未确定节点到达 B 的隐藏替代路径,都必须先以 >= 4 的代价到达前沿,再加上一条非负边。它不可能小于 3

确定 B 之前,堆最小项是 (B,3);其他暂定项至少为 4。确定 B 前的正确性前沿。
412158361AA: settled, dist 0CC: settled, dist 1BB: current, dist 3DD: tentative, dist 6EE: tentative, dist 9FF: unseen, dist Infinity
追踪pop-B-3
B:4*, D:6, E:9
当前B
-
规则任何隐藏替代路径都先从代价 >= 4 的前沿开始,再加非负边。

复杂度

使用二叉堆并允许重复条目时,每次成功松弛都会压入一个堆条目。每个堆操作需要 O(log V),所以常见界是 O((V + E) log V)

9 条无向边变成 18 次有向邻居扫描;重复入堆产生过期跳过。该例子的 Dijkstra 复杂度计数。
412158361AA: settled, dist 0CC: settled, dist 1BB: settled, dist 3DD: settled, dist 4EE: settled, dist 7FF: settled, dist 8
追踪done
-
当前-
-
规则边扫描加堆操作得到 O((V + E) log V)。

这个例子中 9 条无向边产生 18 次邻居扫描、10 次入堆和 4 次过期跳过。

常见混淆

这里为了阅读把优先队列按顺序展示,但真实堆只保证下一次取出的最小项。旧条目可以留在堆里,并在之后变成过期条目。

旧的 (B,4) 过期了,因为 dist[B] 已经是 3。B 的过期优先队列条目。
412158361AA: settled, dist 0CC: settled, dist 1BB: current, dist 3DD: tentative, dist 4EE: tentative, dist 9FF: unseen, dist Infinity
追踪skip-stale-B-4
D:4, D:6*, E:9
当前B
-
规则为便于阅读而排序展示;真实堆只保证下一个最小项。

堆条目为了阅读按序展示;真实堆只保证下一次取出最小项。

零权边是允许的。相同暂定距离可以用确定的显示顺序处理,但平局顺序不是 Dijkstra 正确性的来源。

不可达节点会保持距离 Infinity,且没有父节点。

孤立的 G 会保持 dist[G] = Infinity,且没有父节点。不可达孤立节点情况。
412158361AA: settled, dist 0CC: settled, dist 1BB: settled, dist 3DD: settled, dist 4EE: settled, dist 7FF: settled, dist 8
追踪done
-
当前-
-
规则不可达节点保持 Infinity,且永远不进入堆。

如果加入孤立的 G,它会保持 dist[G] = Infinity、parent[G] = 无,并且永远不会进入堆。

负边会破坏”已确定就是最终”这个承诺。这个警示使用单独的有向图,不是主无向地图。

后来的负边与已确定状态的承诺矛盾。Dijkstra 的有向负边警示。
25-4SAB有向警示,不是主地图
警示步骤negative-contradiction
候选值1
规则这是有向警示变体,不是主地图。

路径恢复

距离告诉总代价。父节点指针可以从目标反向追踪,再反转结果,从而恢复具体路径。

F 恢复出的路径是 A -> C -> B -> D -> E -> F。从 F 恢复路径。
412158361AA: settled, dist 0CC: settled, dist 1BB: settled, dist 3DD: settled, dist 4EE: settled, dist 7FF: settled, dist 8
追踪done
-
当前-
-
规则沿父节点箭头向后追踪,然后反转结果。
从 A 出发的最终距离
节点distparent
A0-
B3C
C1A
D4B
E7D
F8E

从 F 沿父节点反向追踪:F <- E <- D <- B <- C <- A;反转后得到 A -> C -> B -> D -> E -> F。

图谱连接

Dijkstra 推广了 BFS 的前沿和距离思想。BFS 使用先进先出,是因为每条边代价相同;Dijkstra 使用暂定总代价,是因为边可以有不同的非负权重。

预测练习

  1. A 确定之后,堆中哪个条目应该下一个弹出?
  2. 为什么 C -> B 会更新 B,但之后过期的 (B, 4) 会被跳过?
  3. 根据父节点表恢复到 F 的路径。
  4. 在有向负边警示中,哪个”已确定状态”的承诺失效了?

图谱连接 : Dijkstra 算法