Skip to content

Lec 11 加权最短路径

  • 加权图定义和表示
  • 加权最短路径问题
  • 加权最短路径算法
  • 松弛算法
  • 最短路径树
  • DAG松弛
  • 练习题

加权图

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

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

  • 常用表示方法

    • 在图内部表示:在邻接表中存储每个顶点的边权重
    • 存储一个单独的Set,将每条边映射到其权重
    python
    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|) 的空间来存储,并且可以在常数时间内返回一条边的权重。当引用一条边 e=(u,v) 的权重时,我们通常会使用w(u,v) 这一符号与 w(e) 互换使用,以表示这条边的权重。

加权最短路径

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

    • i=1k1w(vi,vi+1)
  • 单源加权最短路径问题:要求从sVtV的加权最短路径是从s到t权重最小的路径,或者指出其不存在最小权重路径

  • δ(s,t)=inf{w(π)|stπ} 表示s到t的最短路径权重(inf 意味着最小下确界)

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

  • 与无权图类似,

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

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

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

何时会发生?

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

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

加权最短路径算法

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

image-20240801015248096

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) = +

  • 思路:维护每个顶点uV的距离估计d(s, v)(初始为),始终是实际距离δ(s,v)的上界,然后逐步降低直到d(s, v) = δ(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)​,否则的话, 途经u的路径将成为当下的最短路径估计。

换个说法就是:

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

    • 如果某条边(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(或者是vV的加权最短路径

  • 证明:假设d(s,v)是所有vV的路径的权重(或者是​​)。松弛某条边(u, v), 将d(s, v)设置为d(s, u) + w(u, v), 这也是s到v途经u的路径权重

python
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

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

python
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) 对于所有vV 成立
    • 证明。 假设反例d(s,v)>δ(s,v)成立,那么存在一个s到v的最短路径π,令(a,b)为π的第一条边使得d(s,b)>δ(s,b)成立的边,那么边(a, b)能够被松弛,与假设矛盾

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

python
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条边的加权有向图,这种图中,如果松弛顺序不佳,可能会执行指数级的修改松弛操作。

image-20240924070920176

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

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

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

最短路径树

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

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

步骤

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

  • 对于每个有限 δ(s,v) 的顶点 uV

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

  • for each unmasked uV(包括后来未标记的顶点):

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

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

DAG松弛

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

伪代码

  • for all vV, Set d(s, v) = ,and Set d(s, s) = 0
  • 处理G的拓扑排序顺序中的每个顶点u:
    • 对于每个出边邻居vAdj+(u)
      • 如果d(s,v)>d(s,u)+w(u,v)
        • 松弛边,即设置d(s,v)=d(s,u)+w(u,v)
python
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), 对于所有的 vV都成立
  • 证明:归纳法证明,d(s, v) = δ(s,v) 对于拓扑排序中前k个顶点满足
    • 基本情况: 顶点s和拓扑排序中s之前的每个顶点在开始时满足断言
    • 归纳步骤: 假设断言对前k'个顶点成立,让v为k'+1个顶点
      • 考虑从s到v的最短路径,并让u为路径上v之前的顶点
      • u在拓扑顺序中位于v之前, 所以归纳假设d(s, u) = δ(s,u)
      • d(s,v)δ(s,v), 因为松弛是安全的,所以d(s, v) = δ(s,v)
  • 或者,
    • 对于任何顶点v, DAG松弛设置d(s, v) = min{d(s, u) + w(u, v) | uAdj(v)}
    • 到v的最短路径必须经过v的某个入边邻居u
    • 所以根据归纳法d(s, u) = δ(s,u),对于uAdj(v),则d(s, v) = δ(s,v)

运行时间

  • 初始化需要O(|V|)时间, 拓扑排序需要O(|V|+|E|)
  • 额外工作上限O(1)×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 总压力最小的课程,并返回其反向的最短路径。