Archipelago

Lec 11加权最短路径

讲义完成更新于 2026-09-16

Lec 11 加权最短路径

资料依据:课程视频与 transcript · Lecture notes

TL;DR

  • 加权最短路径最小化路径上的边权之和,负权环可使最短路径没有有限最优值。
  • 松弛边 (u,v)(u,v) 尝试用 d(u)+w(u,v)d(u)+w(u,v) 改善 d(v)d(v);当某条最短路上的边按顺序被松弛后,路径估计会变为精确值。
  • DAG 的拓扑序为每条边提供一次正确的松弛顺序,即使存在负边也可在 O(∣V∣+∣E∣)O(|V|+|E|) 时间求解。

加权图

对于许多应用来说,将数值权重与图中的边关联起来是非常有用的。例如,一个建模道路网络的图可能会为每条边赋予对应于该条道路长度的权重,或者一个建模在线交友网络的图可能会为从一个用户到另一个用户的边赋予表示定向吸引力的权重。因此,带权图定义为 G=(V,E)G = (V, E),并伴随一个权重函数 w:E→Rw : E → R,该函数将边映射到实数值的权重上。

  • 加权图就是图 G=(V, E)中,为每个边 e=(u, v) 分配了一个权重函数 w(e) = w(u, v)

  • 常用表示方法

    • 在图内部表示:在邻接表中存储每个顶点的边权重
    • 存储一个单独的 Set,将每条边映射到其权重
    W2 = {
      0: {1: 1, 3: 2, 4: -1}, # 0
      1: {0: 1}, # 1
      2: {3: 0}, # 2
      3: {0: 2, 2: 0}, # 3
      4: {0: -1}
    } # 4
    
    def w(u, v):
      return W2[u][v]

你可以简单地假设一个权重函数 w 可以使用O(∣E∣)O(|E|) 的空间来存储,并且可以在常数时间内返回一条边的权重。当引用一条边 e=(u,v)e = (u, v) 的权重时,我们通常会使用w(u,v)w(u, v) 这一符号与 w(e)w(e) 互换使用,以表示这条边的权重。

加权最短路径

  • 在加权图中,路径π=(v1,v2,...,vk)\pi=(v_1, v_2, ...,v_k)的权重 w(π\pi)是路径中各边的权重总和

    • 即∑i=1k−1w(vi,vi+1)\sum^{k-1}_{i=1}w(v_i, v_{i+1})
  • 单源加权最短路径问题:要求从s∈Vs \in V到t∈Vt\in V的加权最短路径是从 s 到 t 权重最小的路径,或者指出其不存在最小权重路径

  • δ(s,t)=inf⁡{w(π)∣从s到t的路径π}\delta(s, t) = \inf \left\{w(\pi) | 从s到t的路径\pi\right\} 表示 s 到 t 的最短路径权重(inf 意味着最小下确界)

  • 在加权图中,通常使用“距离”(distance)表示最短路径权重,而不是边的数量

  • 与无权图类似,

    • 如果从 s 到 t 没有路径,则表示δ(s,t)=∞\delta(s, t) = \infty
    • 最短路径的子路径也是最短路径(否则可以拼接出更短的路径)

为什么用下确界(inf)而不是最小值?可能不存在有限长度的最小权重路径?

Solution: 可能不存在有限长度的最小权重路径

何时会发生?

Solution: 如果图中存在负权重环,可能会发生这种情况,负权重环是指以同一顶点开始和结束且权重小于 0 的路径 π。如果存在通过负权重环上的顶点从 s 到 t 的路径,则 δ(s, t) = −∞。如果发生这种情况,某些最短路径可能根本不存在

如果从 s 到 v 没有路径存在,那么我们会认为从 s 到 v 的最短路径未定义,其权重为 +∞+\infty。除了广度优先搜索之外,我们将介绍另外三种用于计算单源最短路径的算法,它们分别适用于不同类型的带权图。

加权最短路径算法

除了广度优先搜索之外,我们将介绍另外三种用于计算单源最短路径的算法,它们分别适用于不同类型的带权图。

加权最短路径图示 1

BFS 的局限性

广度优先搜索(BFS)通常只能在无权图或所有边权重相同的图上高效运行。如果图中的边有不同的权重,BFS 无法直接应用,因为它无法区分较短的路径和较长的路径。在以下情况下运行时间为 O(|V| + |E|):

  • 图有正权重,且所有权重相同
  • 图有正权重,且所有权重的总和至多为 O(|V| + |E|)

对于一般加权图,无法在 O(|V| + |E|) 时间内解决单源最短路径问题

但如果图是有向无环图(DAG),可以做到!这就是 DAG 松弛法

松弛算法

Relaxation,一种通用的算法范式,松弛算法通过从一个非最优解开始,逐步改进解,直到找到原始问题的最优解。在 SSSP 问题中,对于每个顶点 v,我们将初始化一个 s 到 v 的最短路径权重的上界估计 d(v),除了 d(s, s) = 0 之外,所有 d(s, v) = +∞+\infty

  • 思路:维护每个顶点u∈Vu \in V的距离估计 d(s, v)(初始为∞\infty),始终是实际距离δ(s,v)\delta(s, v)的上界,然后逐步降低直到 d(s, v) = δ(s,v)\delta(s, v), 我们称d(s,v)d(s, v)已经完全松弛。当所有最短路径估计都完全松弛时,问题的解。

什么时候降低?或者说, 如何“松弛”顶点?

当一条边违反三角不等式时!

为了松弛最短路径估计 d(s, v), 我们需要松弛到 v 的一条入边(假设途经顶点 u,且 e(u, v)是 v 的入边),如果我们保持 d(s, u)始终是 s 到 u 的最短路径上界,那么真实的最短路径δ(s,v)≤d(s,u)+w(u,v)\delta(s, v) \le d(s, u) + w(u, v),否则的话, 途经 u 的路径将成为当下的最短路径估计。

换个说法就是:

  • 三角不等式,从 u 到 v 的最短路径权重不能大于从 u 到 v 途经另一个顶点 x 的最短路径,即δ(u,v)≤δ(u,x)+δ(x,v)\delta(u, v) \le \delta(u, x) + \delta(x, v), 对于所有的u,v,x∈Vu, v, x \in V成立

    • 如果某条边(u, v), d(s, v) > d(s, u) + w(u, v)则,三角不等式被违反

    • 通过将 d(s, v)降低到 d(s, u) + w(u, v)来修复,即松弛(u, v) 以满足被违反的约束

  • 断言: 松弛是安全的: 维护每个 d(u, v) 作为到 v(或者是∞\infty)∀v∈V\forall v \in V的加权最短路径

  • 证明:假设d(s,v′)d(s, v')是所有v′∈Vv'\in V的路径的权重(或者是∞\infty)。松弛某条边(u, v), 将 d(s, v)设置为 d(s, u) + w(u, v), 这也是 s 到 v 途经 u 的路径权重

def general_relax(Adj, w, s):  # Adj: adjacency list, w: weights, s: start
  d = [float('inf') for _ in Adj] # shortest path estimates d(s, v)
  parent = [None for _ in Adj] # initialize parent pointers
  d[s], parent[s] = 0, s   #  initialize source
  while True:							# repeat forever!
    relax some d[v] 			# relax a shortest path estimate d(s, v)
  return d, parent			# return weights, paths via parents

这个算法存在许多问题,最主要的是它永远不会终止!

def try_to_relax(Adj, w, d, parent, u, v):
  if d[v] > d[u] + w(u, v):
  	d[v] = d[u] + w(u, v)
    parent[v] = u
  • 中止断言:如果没有边可以松弛,那么d(s,v)≤δ(s,v)d(s, v) \le \delta(s,v) 对于所有v∈Vv \in V 成立
    • 证明。 假设反例d(s,v)>δ(s,v)d(s, v) \gt \delta(s,v)成立,那么存在一个 s 到 v 的最短路径π\pi,令(a,b)为π\pi的第一条边使得d(s,b)>δ(s,b)d(s, b) \gt \delta(s, b)成立的边,那么边(a, b)能够被松弛,与假设矛盾

因此我们将中止条件加入到代码中

def general_relax(Adj, w, s):  # Adj: adjacency list, w: weights, s: start
  d = [float('inf') for _ in Adj] # shortest path estimates d(s, v)
  parent = [None for _ in Adj] # initialize parent pointers
  d[s], parent[s] = 0, s   #  initialize source
  while some_edge_relaxable(Adj, w, d):
    (u, v) = get_relaxable_edge(Adj, w, d)
    try_to_relax(Adj, w, d, parent, u, v)
  return d, parent			# return weights, paths via parents

如果图中存在一个从源点 s 可达的负权重环,那么该算法将永远不会终止,因为沿着该环的边可以无限次松弛。然而,即使对于无环图,这个算法也可能需要指数级的时间

指数级松弛

在一个无环图中,最多能发生多少次修改边的松弛操作,才能使所有边完全松弛?

下面是有 2n+1 个顶点和 3n 条边的加权有向图,这种图中,如果松弛顺序不佳,可能会执行指数级的修改松弛操作。

加权最短路径图示 2

这个图包含 n 个部分,每个部分 i 包含三条边,分别是(v2i,v2i+1),(v2i,v2i+2),(v2i,v2i+2)(v_{2i}, v_{2i+1}), (v_{2i}, v_{2i+2}), (v_{2i}, v_{2i+2}),每条边的权重是2n−i2^{n-i}。我们称这些边为该部分的左边、上边和右边。在这个构造中,从v0v_0到viv_i的最小权重路径是通过遍历上边直到达到viv_i所在的部分来实现。从v0v_0 开始的最短路径能够通过线性次数的修改边松弛来轻松找到:松弛每个连续部分的上边和左边。

下面展示一个不佳的松弛顺序,将所有最小路径估计初始化为∞\infty,除了源点。首先松弛第 0 不分的左边,然后是右边,更新v2v_2的最短路径估计d(s,v2)=2n+2n=2n+1d(s, v_2) = 2^n + 2^n = 2^{n+1}。实际上,应该通过上边即δ(s,v2)=2n\delta(s, v_2) = 2^n。按照上面不佳是顺序,最后松弛第 0 部分的上边后,d(s,v2)d(s, v_2)被修改为其正确值2n2^n。最后,递归地再次完全松弛第 1 部分到第 n-1 部分,使它们达到正确且最终的值。

这个边松弛顺序,总共执行了多少次松弛操作?,设 T(n)为边松弛次数,其递归关系为 T(n) = 3 + 2T(n-2),递归解为 T(n) = O(2n/22^{n/2})

最短路径树

对于广度优先搜索(BFS),我们在搜索过程中跟踪父指针。又或者,我们可以在搜索后计算它们!

  • 如果知道所有顶点 v∈Vv \in V的 δ(s,v)\delta(s, v),可以在 O(|V| + |E|) 时间内构建最短路径树
  • 对于从 s 开始的加权最短路径,只需要保留有限 δ(s,v)\delta(s, v) 的顶点 v 的父指针

步骤

  • 初始化空的 P , 并设置 P(s) = None

  • 对于每个有限 δ(s,v)\delta(s, v) 的顶点 u∈Vu \in V:

    • 对于每个其出边邻居 v∈Adj+(u)v \in Adj^+(u):
      • 如果 P(v) 未分配且 δ(s,v)=δ(s,u)+w(u,v)\delta(s, v) = \delta(s, u) + w(u, v):
        • 则存在通过边 (u, v)的最短路径,因此设置 P(v) = u
  • 父指针可能会穿过零权重的循环。标记这些循环中的每个顶点

  • for each unmasked u∈Vu \in V(包括后来未标记的顶点):

    • for each v∈Adj+(u)v \in Adj^+(u) 且 v 被标记且 δ(s,v)=δ(s,u)+w(u,v)\delta(s, v) = \delta(s, u) + w(u, v):
      • 通过从 v 开始沿父指针遍历,取消标记包含 v 的循环中的顶点
      • 设置 P(v) = u,打破循环

  • 练习:证明该算法在线性时间内正确计算父指针
  • 因为我们可以在之后计算父指针,所以我们专注于计算距离

DAG 松弛

在有向无环图 (DAG) 中,不可能存在负权重环,因此松弛最终一定会终止。事实证明,如果按照顶点的拓扑排序,对每个顶点的每条出边执行一次松弛操作,就能够正确地计算最短路径。这种最短路径算法有时被称为 DAG 松弛(DAG Relaxation)

伪代码

  • for all v∈Vv \in V, Set d(s, v) = ∞\infty,and Set d(s, s) = 0
  • 处理 G 的拓扑排序顺序中的每个顶点 u:
    • 对于每个出边邻居v∈Adj+(u)v\in Adj^+{(u)}
      • 如果d(s,v)>d(s,u)+w(u,v)d(s, v) > d(s, u) + w(u, v)
        • 松弛边,即设置d(s,v)=d(s,u)+w(u,v)d(s, v) = d(s, u) + w(u, v)
def DAG_Relaxation(Adj, w, s):
  _, order = dfs(Ads, s)
  order.reverse()
  d = [float('inf') for _ in Adj]
  parent = [None for _ in Adj]
  d[s], parent[s] = 0, s
  for u in order:
    for v in Adj[u]:
      try_to_relax(Adj, w, d, parent, u, v)
  return d, parent

正确性证明

  • 断言: 在 DAG 松弛结束时,d(s, v) = δ(s,v)\delta(s, v), 对于所有的 v∈Vv \in V都成立
  • 证明:归纳法证明,d(s, v) = δ(s,v)\delta(s, v) 对于拓扑排序中前 k 个顶点满足
    • 基本情况: 顶点 s 和拓扑排序中 s 之前的每个顶点在开始时满足断言
    • 归纳步骤: 假设断言对前 k'个顶点成立,让 v 为 k'+1 个顶点
      • 考虑从 s 到 v 的最短路径,并让 u 为路径上 v 之前的顶点
      • u 在拓扑顺序中位于 v 之前, 所以归纳假设 d(s, u) = δ(s,u)\delta(s, u)
      • 但d(s,v)≥δ(s,v)d(s, v) \ge \delta(s, v), 因为松弛是安全的,所以 d(s, v) = δ(s,v)\delta(s, v)
  • 或者,
    • 对于任何顶点 v, DAG 松弛设置 d(s, v) = min{d(s, u) + w(u, v) | u∈Adj−(v)u \in Adj^-(v)}
    • 到 v 的最短路径必须经过 v 的某个入边邻居 u
    • 所以根据归纳法 d(s, u) = δ(s,u)\delta(s, u),对于u∈Adj−(v)u\in Adj^-(v),则 d(s, v) = δ(s,v)\delta(s, v)

运行时间

  • 初始化需要 O(|V|)时间, 拓扑排序需要 O(|V|+|E|)
  • 额外工作上限O(1)×deg+(u)=O(∣E∣)O(1) \times deg^+(u) = O(|E|)
  • 总的运行时间是线性的 O(|V| + |E|)

练习题

你已经被 MIT 招募参与一个新的兼职学生计划,每个学期只选一门课。你并不在乎毕业,只想选修 19.854 高级量子机器学习区块链:神经接口 课程,但你担心它庞大的先修课要求。MIT 的教授们允许你在先修课程中只修完一门之后就能选修该课程。然而,没有完成所有先修课直接通过课程会很困难。通过对同学们的调查,你得知每门课程及其先修课需要多少压力时间。给定一个课程、先修课以及调查得出的压力值的列表,描述一种线性时间的算法,找到一系列课程的顺序,以最小化修 19.854 课程所需的压力,并且不会同时修多于一门先修课。你可以假设每个学期都会提供所有课程。

SOLUTION:构建一个图,每个课程对应一个顶点,如果课程 b 是课程 a 的先修课,则从课程 a 到课程 b 画一条带权的有向边,边的权重为修完课程 b 后再修课程 a 需要的压力值。使用拓扑排序松弛来找到从课程 19.854 到每个其他课程的最短路径。从那些没有先修课的课程(DAG 的终点)中,找到一个到 19.854 总压力最小的课程,并返回其反向的最短路径。

我的理解

我的理解

松弛是后续最短路径算法的共同原语:算法之间的差别主要不在更新公式,而在如何选择下一条边,以及何时可以证明某个估计已经定型。