文章目录
单源最短路径
本文参考整理及图片来源:《算法导论》P374-P386
1 预备知识
1.1 最短路径问题
假设你需要找到一条从 A A A市到 B B B市的一条最短路径,先给定一幅道路交通图,上面标有所有相邻城市之间的距离。你要怎样才能找出 A A A到 B B B的最短距离呢?
要解决这个问题,我们最直观的想法就是把 A A A到 B B B的所有路径都找出来,计算这些路径的长度,选取其中最短的路径。但这种方法太麻烦了,我们要检查许多种可能的路径,而且大部分路径都不值得检查。因此需要探索如何高效地解决这个问题。
在最短路径问题中,我们给定一个带权重的有向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)和权重函数
w
:
E
→
R
w:E\rightarrow R
w:E→R,该权重函数将每条边映射到实数值的权重上。图中一条路径
p
=
⟨
v
0
,
v
1
,
⋯
,
v
k
⟩
p=\langle v_0,v_1,\cdots,v_k\rangle
p=⟨v0,v1,⋯,vk⟩的权重
w
(
p
)
w(p)
w(p)是构成该路径的所有边的权重之和:
w
(
p
)
=
∑
i
=
1
k
w
(
v
i
−
1
,
v
i
)
w(p) = \sum_{i=1}^kw(v_{i-1}, v_i)
w(p)=i=1∑kw(vi−1,vi)
定义从节点
u
u
u到节点
v
v
v的最短路径权重
δ
(
u
,
v
)
\delta(u, v)
δ(u,v)如下:
δ
(
u
,
v
)
=
{
m
i
n
{
w
(
p
)
:
u
→
p
v
}
如
果
存
在
一
条
从
节
点
u
到
节
点
v
的
路
径
∞
其
他
\delta(u, v)=\begin{cases} min\{w(p):u\stackrel{p}{\rightarrow} v\} & 如果存在一条从节点u到节点v的路径\\ \infty&其他 \end{cases}
δ(u,v)={min{w(p):u→pv}∞如果存在一条从节点u到节点v的路径其他
从节点
u
u
u到节点
v
v
v的最短路径则定义为一条权重为
w
(
p
)
=
δ
(
u
,
v
)
w(p)=\delta(u, v)
w(p)=δ(u,v)的从
u
u
u到
v
v
v的路径
p
p
p
那么单源最短路径问题为:给定一个图 G = ( V , E ) G=(V,E) G=(V,E),我们希望找到从给定源节点 s ∈ V s\in V s∈V到每个节点 v ∈ V v\in V v∈V的最短路径。
1.2 最短路径的几个变体问题
单源最短路径可以解决几个变体问题:
- 单目的地最短路径问题:找到每个节点 v v v到给定目的地的节点 t t t的最短路径。
- 单节点对最短路径问题:找到从给定节点 u u u到给定节点 v v v的最短路径。
- 所有节点对最短路径问题:对于每对节点 u u u和 v v v,找到从节点 u u u到节点 v v v的最短路径。
1.3 最短路径的最优子结构
最短路径算法通常依赖最短路径的一个重要性质:两个节点之间的一条最短路径包含着其他的最短路径,下述引理精确地描述了最短路径的最优子结构性质。
最短路径的子路径也是最短路径:给定带权重的有向图 G = ( V , E ) G=(V,E) G=(V,E)和权重函数 w : E → R w:E\rightarrow R w:E→R。设 p = ⟨ v 0 , v 1 , ⋯ , v k ⟩ p=\langle v_0,v_1,\cdots,v_k\rangle p=⟨v0,v1,⋯,vk⟩为从节点 v 0 v_0 v0到节点 v k v_k vk的一条最短路径,并且对于任意的 i i i和 j j j, 0 ≤ i ≤ j ≤ k 0\le i\le j\le k 0≤i≤j≤k,设 p i j = ⟨ v i , v i + 1 , ⋯ , v j ⟩ p_{ij}=\langle v_i,v_{i+1},\cdots,v_j\rangle pij=⟨vi,vi+1,⋯,vj⟩为路径 p p p中从节点 v i v_i vi到 v j v_j vj的子路径。那么 p i j p_{ij} pij是从节点 v i v_i vi到节点 v j v_j vj的一条最短路径。
1.4 负权重的边
某些单源最短路径问题可能包括权重为负值的边,下面来看看负权重的边可能带来的一些问题,并以一个实例进行讲解。
如果图 G = ( V , E ) G=(V,E) G=(V,E)不包含从源节点 s s s可以到达的权重为负值的环路(即环路中的边权重和为负值),则对于所有的节点 v ∈ V v\in V v∈V,最短路径权重 δ ( s , v ) \delta(s,v) δ(s,v)都有精确定义,即便其取值为负数。
相反,如果图 G = ( V , E ) G=(V,E) G=(V,E)包含从源节点 s s s可以到达的权重为负值的环路,则最短路径权重无定义。(因为我们只要沿着任何“最短”路径再遍历一次权重为负值的环路,就可以找到一条权重更小的路径)。
如果从节点 s s s到 v v v的某条路径上存在权重为负值的环路,那我们定义 δ ( s , v ) = − ∞ \delta(s,v)=-\infty δ(s,v)=−∞。
下面是一个例子,帮助理解这段话。根据下图,求源节点
s
s
s到其它所有节点的最短路径权重。
- δ ( s , a ) = w ( s , a ) = 3 \delta(s,a)=w(s,a)=3 δ(s,a)=w(s,a)=3, s s s到 a a a只有一条路径;
- δ ( s , b ) = w ( s , a ) + w ( a , b ) = − 1 \delta(s,b)=w(s,a)+w(a,b)=-1 δ(s,b)=w(s,a)+w(a,b)=−1,原因同上;
- δ ( s , c ) = w ( s , c ) = 5 \delta(s,c)=w(s,c)=5 δ(s,c)=w(s,c)=5, s s s到 c c c有无数条路径, ⟨ s , c ⟩ , ⟨ s , c , d , c ⟩ \langle s,c\rangle,\langle s,c,d,c\rangle ⟨s,c⟩,⟨s,c,d,c⟩等,由于 c , d c,d c,d组成的环路权重为 3 > 0 3\gt 0 3>0,因此最短路径权重应该不含环;
- δ ( s , d ) = w ( s , c ) + w ( c , d ) = 11 \delta(s,d)=w(s,c)+w(c,d)=11 δ(s,d)=w(s,c)+w(c,d)=11,原因同上;
- δ ( s , e ) = − ∞ \delta(s,e)=-\infty δ(s,e)=−∞, s s s到 e e e有无数条路径, ⟨ s , e ⟩ , ⟨ s , e , f , e ⟩ \langle s,e\rangle,\langle s,e,f,e\rangle ⟨s,e⟩,⟨s,e,f,e⟩,由于 e , f e,f e,f组成的环路权重为 − 3 < 0 -3\lt 0 −3<0,因此总能找到更小的权重,故为 − ∞ -\infty −∞;
- δ ( s , f ) = − ∞ \delta(s,f)=-\infty δ(s,f)=−∞,原因同上;
- δ ( s , g ) = − ∞ \delta(s,g)=-\infty δ(s,g)=−∞,由于 s s s到 g g g的某条路径上存在权重为负值的环路,故为 − ∞ -\infty −∞;
- δ ( s , h ) = δ ( s , i ) = δ ( s , j ) = ∞ \delta(s,h)=\delta(s,i)=\delta(s,j)=\infty δ(s,h)=δ(s,i)=δ(s,j)=∞,这三个节点根本无法到达,故为 ∞ \infty ∞。
1.5 环路
思考一个问题,一条最短路径可以包含环路吗?
首先它肯定是不能包含权重为负值的环路了;其次如果它包含权重为正的环路,那我们只要将环路从路径上删除就可以得到一条权重更小的路径;这样就只剩下权重为 0 0 0的环路了,这时我们也可以从任何路径上删除权重为 0 0 0的路径而得到另一条权重相同的路径。
因此,不失一般性,我们可以假定在找到的最短路径中没有环路,即它们都是简单路径。由于图 G = ( V , E ) G=(V,E) G=(V,E)中的任意无环路径最多包含 ∣ V ∣ |V| ∣V∣个不同的节点,它也最多包含 ∣ V ∣ − 1 |V|-1 ∣V∣−1条边。因此我们可以将注意力集中到至多只包含 ∣ V ∣ − 1 |V|-1 ∣V∣−1条边的最短路径上。
1.6 最短路径的表示
通常情况下,我们不但希望计算出最短路径权重,还希望计算出最短路径上的节点。给定图 G = ( V , E ) G=(V,E) G=(V,E),对于每个节点 v v v,我们维持一个前驱节点 v . π v.\pi v.π。该前驱节点可能是另一个节点或者 N o n e None None。最短路径算法将对每个节点的 π \pi π属性进行设置,这样,将从节点 v v v开始的前驱节点链反转过来,就是 s s s到 v v v的一条最短路径。
但是,在最短路径算法过程中, π \pi π值不一定能给出最短路径。我们感兴趣的是由 π \pi π值所诱导的前驱子图 G π = ( V π , E π ) G_{\pi}=(V_{\pi}, E_{\pi}) Gπ=(Vπ,Eπ)其中:
V π = { v ∈ V : v . π ≠ N o n e } ∪ { s } E π = { ( v . π , v ) ∈ E : v ∈ V π − { s } } V_{\pi}=\{v\in V:v.\pi\ne None\} \cup \{s\}\\ E_{\pi}=\{(v.\pi, v)\in E:v\in V_{\pi}-\{s\}\} Vπ={v∈V:v.π=None}∪{s}Eπ={(v.π,v)∈E:v∈Vπ−{s}}
最短路径算法终止时所产生的 G π G_{\pi} Gπ是一棵根节点为 s s s的”最短路径树“(一个有向子图),该树包含了从源节点 s s s到每个可以从 s s s到达的节点的一条最短路径。
注意:最短路径不一定唯一,最短路径树也不一定唯一。下图是一个带权重的有向图和两棵根节点相同的最短路径树。
1.7 松弛操作
最短路径算法需要使用松弛技术。对于每个节点 v v v来说,我们维持一个属性 v . d v.d v.d用来记录从源节点 s s s到节点 v v v的最短路径权重的上界。我们将 v . d v.d v.d称之为 s s s到 v v v的最短路径估计。下面的伪代码用来对 v . d v.d v.d和 v . π v.\pi v.π进行初始化:
I N I T I A L I Z E − S I N G L E − S O U R C E ( G , s ) INITIALIZE-SINGLE-SOURCE(G,s) INITIALIZE−SINGLE−SOURCE(G,s):初始化 v . d v.d v.d和 v . π v.\pi v.π
for each vertex v ∈ G.V:
v.d = ∞ //初始化每个节点的最短路径估计为无穷大,也就是上界为无穷
v.π = None //初始化前驱节点为None
end for
s.d = 0 //初始化源节点的最短路径估计为0
对每一条边 ( u , v ) (u,v) (u,v)的松弛过程为:首先测试一下是否可以对从 s s s到 v v v的最短路径进行改善。测试的方法是,将从节点 s s s到节点 u u u之间的最短路径距离加上节点 u u u与 v v v之间的边的权重,并与当前的 s s s到 v v v的最短路径估计进行比较,如果前者更小,则对 v . d v.d v.d和 v . π v.\pi v.π进行更新。松弛步骤可能降低最短路径的估计值 v . d v.d v.d并更新 v v v的前驱属性 v . π v.\pi v.π。下面的伪代码就是对边 ( u , v ) (u, v) (u,v)的松弛操作:
R E L A X ( u , v , w ) RELAX(u,v,w) RELAX(u,v,w):对 ( u , v ) (u,v) (u,v)进行松弛操作。
if v.d > u.d + w(u, v):
v.d = u.d + w(u, v)
v.π = u
end if
下图展示了对两条边进行松弛操作的例子,第一个
v
.
d
v.d
v.d因松弛而减小,第二个则没变。
下面介绍的每个算法都会调用 I N I T I A L I Z E − S I N G L E − S O U R C E INITIALIZE-SINGLE-SOURCE INITIALIZE−SINGLE−SOURCE和 R E L A X RELAX RELAX。
1.8 最短路径和松弛操作的性质
为了证明后面算法的正确性,需要使用最短路径和松弛操作的一些性质(证明略)。
- 三角不等式性质:对于任何边 ( u , v ) ∈ E (u,v)\in E (u,v)∈E,我们有 δ ( s , v ) ≤ δ ( s , u ) + w ( u , v ) \delta(s,v)\le \delta(s, u) + w(u, v) δ(s,v)≤δ(s,u)+w(u,v)
- 上界性质:对于所有的节点 v ∈ V v\in V v∈V,我们总是有 v . d ≥ δ ( s , v ) v.d\ge \delta(s,v) v.d≥δ(s,v)。一旦 v . d v.d v.d的取值达到 δ ( s , v ) \delta(s,v) δ(s,v),其值将不再发生变化。
- 非路径性质:如果从节点 s s s到节点 v v v之间不存在路径,则总是有 v . d = δ ( s , v ) = ∞ v.d=\delta(s,v)=\infty v.d=δ(s,v)=∞。
- 收敛性质:对于某些节点 u , v ∈ V u,v\in V u,v∈V,如果 s → ⋯ → u → v s\rightarrow \cdots \rightarrow u\rightarrow v s→⋯→u→v是图 G G G中的一条最短路径,并且在对边 ( u , v ) (u, v) (u,v)进行松弛前的任意时间有 u . d = δ ( s , u ) u.d=\delta(s,u) u.d=δ(s,u),则在之后的所有时间有 v . d = δ ( s , v ) v.d=\delta(s,v) v.d=δ(s,v)
- 路径松弛性质:如果 p = ⟨ v 0 , v 1 , ⋯ , v k ⟩ p=\langle v_0,v_1,\cdots,v_k\rangle p=⟨v0,v1,⋯,vk⟩是从源节点 s = v 0 s=v_0 s=v0到节点 v k v_k vk的一条最短路径,并且我们对 p p p中的边进行松弛的次序为 ( v 0 , v 1 ) , ( v 1 , v 2 ) , ⋯ , ( v k − 1 , v k ) (v_0, v_1),(v_1,v_2),\cdots ,(v_{k-1},v_k) (v0,v1),(v1,v2),⋯,(vk−1,vk),则 v k . d = δ ( s , v k ) v_k.d=\delta (s,v_k) vk.d=δ(s,vk)。该性质的成立与任何其他的松弛操作无关,即使这些松弛操作是与对 p p p上的边所进行的松弛操作穿插进行的。
- 前驱子图性质:对于所有的节点 v ∈ V v\in V v∈V,一旦 v . d = δ ( s , v ) v.d=\delta(s,v) v.d=δ(s,v),则前驱子图是一棵根节点为 s s s的最短路径树。
2 Bellman-Ford 算法
Bellman-Ford算法解决的是一般情况下的单源最短路径问题,所谓一般情况是指边的权重可以为负值。给定一个带权重的有向图 G = ( V , E ) G=(V,E) G=(V,E)和权重函数 w : E → R w:E\rightarrow R w:E→R,Bellman-Ford算法返回一个布尔值,以表明是否存在源节点可到达的权重为负值的环路,如果存在则算法告诉我们不存在解决方案;如果不存在则算法将给出最短路径和它们的权重。
Bellman-Ford算法通过对边进行松弛操作来渐近地降低从源节点 s s s到每个节点 v v v的最短路径的估计值 v . d v.d v.d,直到该估计值与实际的最短路径权重 δ ( s , v ) \delta(s,v) δ(s,v)相同时为止。该算法返回 T R U E TRUE TRUE当且仅当输入图不包含可以从源节点到达的权重为负值的环路。下面是Bellman-Ford算法的伪代码描述:
B e l l m a n − F o r d ( G , w , s ) Bellman-Ford(G,w,s) Bellman−Ford(G,w,s):解决单源最短路径问题。
INITIALIZE-SINGLE-SOURCE(G, s) //初始化v.d和v.π
for i = 1 to |G.V|-1: //对每一条边进行|G.V|-1次处理
for each edge(u, v) ∈ G.E:
RELAX(u, v, w)
end for
end for
for each edge(u, v) ∈ G.E:
if v.d > u.d + w(u, v):
return False
end if
end for
return True
下图展示了Bellman-Ford的算法流程,这里每一次的松弛操作对边的处理次序都是:
(
t
,
x
)
,
(
t
,
y
)
,
(
t
,
z
)
,
(
y
,
x
)
,
(
y
,
z
)
,
(
z
,
x
)
,
(
z
,
s
)
,
(
s
,
t
)
,
(
s
,
y
)
(t,x),(t,y),(t,z),(y,x),(y,z),(z,x),(z,s),(s,t),(s,y)
(t,x),(t,y),(t,z),(y,x),(y,z),(z,x),(z,s),(s,t),(s,y)
3 有向无环图中的单源最短路径问题
根据节点的拓扑排序次序来对带权重的有向无环图 G = ( V , E ) G=(V,E) G=(V,E)进行边的松弛操作,我们便可以在 O ( V + E ) O(V+E) O(V+E)的时间内计算出从单个源节点到所有节点之间的最短路径。在有向无环图中,即时存在权重为负值的边,但因为没有权重为负值的环路,最短路径都是存在的。
算法流程就是:首先对有向无环图进行拓扑排序,以便确定节点之间的一个线性词序,然后只需要按照拓扑排序的次序对节点进行一遍处理即可。每次对一个节点进行处理时,我们对从该节点出发的所有的边进行松弛操作。算法的伪代码描述如下:
D A G − S H O R T E S T − P A T H S ( G , w , s ) DAG-SHORTEST-PATHS(G,w,s) DAG−SHORTEST−PATHS(G,w,s):计算源节点 s s s到所有节点之间的最短路径。
topologically sort the vertices of G //对G进行拓扑排序
INITIALIZE-SINGLE-SOURCE(G,s) //初始化v.d和v.π
for each vertex u, taken in topologically sorted order:
for each vertex v ∈ G.Adj[u]:
RELAX(u,v,w)
下图展示了这个过程,其中
(
a
)
(a)
(a)为拓扑排序并初始化
v
.
d
v.d
v.d和
v
.
π
v.\pi
v.π的状态,
(
b
)
(b)
(b)到
(
g
)
(g)
(g)表示的是对每个当前节点的邻接边进行松弛操作之后的结果,粗边表示
v
.
π
v.\pi
v.π。
4 Dijkstra 算法
Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为非负值。
Dijkstra算法在运行过程中维持的关键信息是一组节点集合 S S S。从源节点 s s s到该集合中每个节点之间的最短路径已经被找到。算法重复从节点集 V − S V-S V−S中选择最短路径估计最小的节点 u u u,将 u u u加入到集合 S S S,然后对所有从 u u u出发的边进行松弛。因此我们需要采用一个最小优先队列 Q Q Q来保存节点集合,每个节点的关键字为其 d d d值。算法的伪代码描述如下:
D I J K S T R A ( G , w , s ) DIJKSTRA(G,w,s) DIJKSTRA(G,w,s):对不含负权重的图计算单源最短路径。
INITIALIZE-SINGLE-SOURCE(G,s)
S = ∅
Q = G.V
while Q != ∅:
u = EXTRACT-MIN(Q)
S = S ∪ {u}
for each vertex v ∈ G.Adj[u]:
RELAX(u, v, w)
end for
end while
下面图展示了Dijkstra算法的过程。
5 一个练习
下面这道题就是单源最短路径问题,可以使用Bellman-ford和Dijkstra两种方法实现: