Lec 11 加权最短路径
- 加权图定义和表示
- 加权最短路径问题
- 加权最短路径算法
- 松弛算法
- 最短路径树
- DAG松弛
- 练习题
加权图
对于许多应用来说,将数值权重与图中的边关联起来是非常有用的。例如,一个建模道路网络的图可能会为每条边赋予对应于该条道路长度的权重,或者一个建模在线交友网络的图可能会为从一个用户到另一个用户的边赋予表示定向吸引力的权重。因此,带权图定义为
加权图就是图G=(V, E)中,为每个边e=(u, v) 分配了一个权重函数w(e) = w(u, v)
常用表示方法
- 在图内部表示:在邻接表中存储每个顶点的边权重
- 存储一个单独的Set,将每条边映射到其权重
pythonW2 = { 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 可以使用
加权最短路径
在加权图中,路径
的权重w( )是路径中各边的权重总和 - 即
- 即
单源加权最短路径问题:要求从
到 的加权最短路径是从s到t权重最小的路径,或者指出其不存在最小权重路径 表示s到t的最短路径权重(inf 意味着最小下确界) 在加权图中,通常使用“距离”(distance)表示最短路径权重,而不是边的数量
与无权图类似,
- 如果从s到t没有路径,则表示
- 最短路径的子路径也是最短路径(否则可以拼接出更短的路径)
- 如果从s到t没有路径,则表示
为什么用下确界(inf)而不是最小值?可能不存在有限长度的最小权重路径?
Solution: 可能不存在有限长度的最小权重路径
何时会发生?
Solution: 如果图中存在负权重环,可能会发生这种情况,负权重环是指以同一顶点开始和结束且权重小于 0 的路径 π。如果存在通过负权重环上的顶点从 s 到 t 的路径,则 δ(s, t) = −∞。如果发生这种情况,某些最短路径可能根本不存在
如果从 s 到 v 没有路径存在,那么我们会认为从 s 到 v 的最短路径未定义,其权重为
加权最短路径算法
除了广度优先搜索之外,我们将介绍另外三种用于计算单源最短路径的算法,它们分别适用于不同类型的带权图。

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) =
- 思路:维护每个顶点
的距离估计d(s, v)(初始为 ),始终是实际距离 的上界,然后逐步降低直到d(s, v) = , 我们称 已经完全松弛。当所有最短路径估计都完全松弛时,问题的解。
什么时候降低?或者说, 如何“松弛”顶点?
当一条边违反三角不等式时!
为了松弛最短路径估计d(s, v), 我们需要松弛到v的一条入边(假设途经顶点u,且e(u, v)是v的入边),如果我们保持d(s, u)始终是s到u的最短路径上界,那么真实的最短路径
换个说法就是:
三角不等式,从u到v的最短路径权重不能大于从u到v途经另一个顶点x的最短路径,即
, 对于所有的 成立 如果某条边(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(或者是
) 的加权最短路径 证明:假设
是所有 的路径的权重(或者是 )。松弛某条边(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- 中止断言:如果没有边可以松弛,那么
对于所有 成立 - 证明。 假设反例
成立,那么存在一个s到v的最短路径 ,令(a,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条边的加权有向图,这种图中,如果松弛顺序不佳,可能会执行指数级的修改松弛操作。

这个图包含n个部分,每个部分i包含三条边,分别是
下面展示一个不佳的松弛顺序,将所有最小路径估计初始化为
这个边松弛顺序,总共执行了多少次松弛操作?,设T(n)为边松弛次数,其递归关系为T(n) = 3 + 2T(n-2),递归解为T(n) = O(
最短路径树
对于广度优先搜索(BFS),我们在搜索过程中跟踪父指针。又或者,我们可以在搜索后计算它们!
- 如果知道所有顶点
的 ,可以在 O(|V| + |E|) 时间内构建最短路径树 - 对于从 s 开始的加权最短路径,只需要保留有限
的顶点 v 的父指针
步骤
初始化空的 P , 并设置 P(s) = None
对于每个有限
的顶点 : - 对于每个其出边邻居
: - 如果 P(v) 未分配且
: - 则存在通过边 (u, v)的最短路径,因此设置 P(v) = u
- 如果 P(v) 未分配且
- 对于每个其出边邻居
父指针可能会穿过零权重的循环。标记这些循环中的每个顶点
for each unmasked
(包括后来未标记的顶点): - for each
且 v 被标记且 : - 通过从 v 开始沿父指针遍历,取消标记包含 v 的循环中的顶点
- 设置 P(v) = u,打破循环
- for each
- 练习:证明该算法在线性时间内正确计算父指针
- 因为我们可以在之后计算父指针,所以我们专注于计算距离
DAG松弛
在有向无环图 (DAG) 中,不可能存在负权重环,因此松弛最终一定会终止。事实证明,如果按照顶点的拓扑排序,对每个顶点的每条出边执行一次松弛操作,就能够正确地计算最短路径。这种最短路径算法有时被称为 DAG 松弛(DAG Relaxation)
伪代码
- for all
, Set d(s, v) = ,and Set d(s, s) = 0 - 处理G的拓扑排序顺序中的每个顶点u:
- 对于每个出边邻居
- 如果
- 松弛边,即设置
- 松弛边,即设置
- 如果
- 对于每个出边邻居
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) =
, 对于所有的 都成立 - 证明:归纳法证明,d(s, v) =
对于拓扑排序中前k个顶点满足 - 基本情况: 顶点s和拓扑排序中s之前的每个顶点在开始时满足断言
- 归纳步骤: 假设断言对前k'个顶点成立,让v为k'+1个顶点
- 考虑从s到v的最短路径,并让u为路径上v之前的顶点
- u在拓扑顺序中位于v之前, 所以归纳假设d(s, u) =
- 但
, 因为松弛是安全的,所以d(s, v) =
- 或者,
- 对于任何顶点v, DAG松弛设置d(s, v) = min{d(s, u) + w(u, v) |
} - 到v的最短路径必须经过v的某个入边邻居u
- 所以根据归纳法d(s, u) =
,对于 ,则d(s, v) =
- 对于任何顶点v, DAG松弛设置d(s, v) = min{d(s, u) + w(u, v) |
运行时间
- 初始化需要O(|V|)时间, 拓扑排序需要O(|V|+|E|)
- 额外工作上限
- 总的运行时间是线性的O(|V| + |E|)
练习题
你已经被 MIT 招募参与一个新的兼职学生计划,每个学期只选一门课。你并不在乎毕业,只想选修 19.854 高级量子机器学习区块链:神经接口 课程,但你担心它庞大的先修课要求。MIT 的教授们允许你在先修课程中只修完一门之后就能选修该课程。然而,没有完成所有先修课直接通过课程会很困难。通过对同学们的调查,你得知每门课程及其先修课需要多少压力时间。给定一个课程、先修课以及调查得出的压力值的列表,描述一种线性时间的算法,找到一系列课程的顺序,以最小化修 19.854 课程所需的压力,并且不会同时修多于一门先修课。你可以假设每个学期都会提供所有课程。
SOLUTION:构建一个图,每个课程对应一个顶点,如果课程 b 是课程 a 的先修课,则从课程 a 到课程 b 画一条带权的有向边,边的权重为修完课程 b 后再修课程 a 需要的压力值。使用拓扑排序松弛来找到从课程 19.854 到每个其他课程的最短路径。从那些没有先修课的课程(DAG 的终点)中,找到一个到 19.854 总压力最小的课程,并返回其反向的最短路径。