文章目录
要点7:掌握 Dijkstra/Bellman-Ford 等最短路算法,理解其特性
📌 适合对象:算法学习者、计算机科学学生
⏱️ 预计阅读时间:70-80分钟
🎯 学习目标:掌握两种单源最短路径算法(Dijkstra和Bellman-Ford),理解它们的原理、区别和应用场景
📚 参考PPT:第 10 章-PPT-N2(1)(最短路径算法)- Dijkstra算法、Bellman-Ford算法相关内容
📚 学习路线图
本文内容一览(快速理解)
- 单源最短路径问题:给定图 G G G中一个顶点 s s s,求解从 s s s到图中所有其它顶点的最短路径
- Dijkstra算法:采用贪心法,要求边权非负,时间复杂度 O ( n 2 ) O(n^2) O(n2)或 O ( n log n + m ) O(n \log n + m) O(nlogn+m)
- Bellman-Ford算法:采用动态规划,允许负权值但不能有负回路,时间复杂度 O ( n m ) O(nm) O(nm)
- 算法比较:适用条件、时间复杂度、应用场景
- 实际应用:Internet路由协议(OSPF、BGP)
一、单源最短路径问题(Single-Source Shortest Path Problem):问题定义
这一章要建立的基础:理解单源最短路径问题是图算法中的经典问题,有多种求解方法。
核心问题:给定图 G G G中一个顶点 s s s,如何求解从 s s s到图中所有其它顶点的最短路径?
[!NOTE]
📝 关键点总结:有向带权图 G ( V , E , w ) G(V, E, w) G(V,E,w)中,边 ( u , v ) (u, v) (u,v)的权重记做 w ( u , v ) w(u, v) w(u,v)。路径长度:对于一条简单路径 p = ( v 0 , v 1 , … , v k ) p = (v_0, v_1, \ldots, v_k) p=(v0,v1,…,vk),该路径的长度为该路径上的链路权重之和,即 w ( p ) = ∑ i = 1 k w ( v i − 1 , v i ) w(p) = \sum_{i=1}^{k} w(v_{i-1}, v_i) w(p)=∑i=1kw(vi−1,vi)。单源最短路径问题:给定图 G G G中一个顶点 s s s,求解从 s s s到图中所有其它顶点的最短路径。
1.1 问题定义(Problem Definition):理解最短路径问题
概念的本质:
单源最短路径问题的定义:
- 输入:带权有向图 G ( V , E , w ) G(V, E, w) G(V,E,w),源点 s ∈ V s \in V s∈V
- 输出:从 s s s到所有其它顶点 v v v的最短路径及其长度 δ ( s , v ) \delta(s, v) δ(s,v)
路径长度:
- 对于一条简单路径
p
=
(
v
0
,
v
1
,
…
,
v
k
)
p = (v_0, v_1, \ldots, v_k)
p=(v0,v1,…,vk),路径长度为:
w ( p ) = ∑ i = 1 k w ( v i − 1 , v i ) w(p) = \sum_{i=1}^{k} w(v_{i-1}, v_i) w(p)=i=1∑kw(vi−1,vi)
最短路径:
- 从 s s s到 v v v的最短路径长度记为 δ ( s , v ) \delta(s, v) δ(s,v)
- 如果从 s s s到 v v v没有路径,则 δ ( s , v ) = ∞ \delta(s, v) = \infty δ(s,v)=∞
图解说明:
💡 说明:这里的权重可以是距离、时间、成本、惩罚、损失,或者其它满足累加特性的度量。初始情况下,设置信源节点 d [ s ] = 0 d[s] = 0 d[s]=0,从 s s s到其它任意顶点 v v v的初始距离 d [ v ] = ∞ d[v] = \infty d[v]=∞。 d [ v ] d[v] d[v]表示迄今为止,已经发现的从 s s s到 v v v的最短路径的长度。
类比理解:
就像在地图上找最短路线:
- 源点 s s s:起点(如你的家)
- 其他顶点:目的地(如学校、商店等)
- 边权重:两点之间的距离或时间
- 最短路径:从家到每个目的地的最短路线
实际例子:
单源最短路径问题示例:
有向图:
s --5--> a --3--> b
| | |
2 1 4
| | |
v v v
c --2--> d --1--> e
问题:从s到所有顶点的最短路径
答案:
- s到a:5(路径:s→a)
- s到b:8(路径:s→a→b)
- s到c:2(路径:s→c)
- s到d:3(路径:s→c→d)
- s到e:4(路径:s→c→d→e)
1.2 最短路径的最优子结构特性(Optimal Substructure):子路径也是最短路径
概念的本质:
最短路径具有最优子结构特性:给定带权有向图 G = ( V , E ) G=(V, E) G=(V,E)和权重函数 w : E → R w:E \to R w:E→R。设 p = ( v 0 , v 1 , … , v k ) p = (v_0, v_1, \ldots, v_k) 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 \leq i < j \leq k 0≤i<j≤k, p i j = ( v i , v i + 1 , … , v j ) p_{ij} = (v_i, v_{i+1}, \ldots, v_j) 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的最短路径。
证明:采用反证法。将路径 p p p分为3段, p 0 i + p i j + p j k p_{0i} + p_{ij} + p_{jk} p0i+pij+pjk。假设存在一条从顶点 v i v_i vi到顶点 v j v_j vj的路径 p i j ′ p'_{ij} pij′,且 w ( p i j ′ ) < w ( p i j ) w(p'_{ij}) < w(p_{ij}) w(pij′)<w(pij)。则有, p ′ = p 0 i + p i j ′ + p j k p' = p_{0i} + p'_{ij} + p_{jk} p′=p0i+pij′+pjk也是一条连接 v 0 v_0 v0到 v k v_k vk的路径,且其权重小于 w ( p ) w(p) w(p)。这与 p p p是从 v 0 v_0 v0到 v k v_k vk的一条最短路径的假设相矛盾。
图解说明:
💡 说明:最优子结构特性是动态规划和贪心算法的基础。它保证了我们可以通过组合子问题的最优解来得到原问题的最优解。
实际例子:
最优子结构示例:
最短路径:s → a → b → c → d(总长度10)
子路径:
- s → a → b(长度3)是最短路径
- b → c → d(长度4)是最短路径
- a → b → c(长度5)是最短路径
如果b → c → d不是最短路径,假设存在更短的路径b → x → d(长度2),
那么s → a → b → x → d(长度7)比原路径更短,矛盾!
二、Dijkstra算法:贪心法求解最短路径
这一章要建立的基础:理解Dijkstra算法采用贪心法,要求边权非负,可以高效求解单源最短路径问题。
核心问题:如何用贪心法求解单源最短路径问题?
[!NOTE]
📝 关键点总结:Dijkstra算法(1959)设计目标:构造以信源 s s s为根的最短路径树(Shortest Path Tree)。寻径策略:采用贪心法,确保可以找到最短路径。应用条件:算法要求图中的边权为非负实数,即 ∀ ( u , v ) ∈ E , w ( u , v ) ≥ 0 \forall (u,v) \in E, w(u,v) \geq 0 ∀(u,v)∈E,w(u,v)≥0。
2.1 Dijkstra算法的基本思想(Basic Idea):贪心选择
概念的本质:
Dijkstra算法的执行过程,网络中的节点可以分成三个子集:
- 黑色:已经计算完毕的集合,这类节点最短路径已确定
- 灰色:正在计算的节点集合,这类节点已被访问到,但最短路未定
- 白色:尚未访问的节点集合,尚未访问到,代价估计仍为无穷
算法步骤:
- 初始时刻:所有节点都是白色节点
- 第一步:信源首先变成黑色并获得其最短距离0,然后,其所有邻居节点变成灰色
- 第二步:每次都是从灰色节点集中选择 d [ ⋅ ] d[\cdot] d[⋅]最小的一个,将其变成黑色
- 第三步:通过新确定的黑色节点,松弛其所有出行边(outgoing links/edges),这时,可能有新的白色节点变成灰色,也可能有灰色节点 d [ ⋅ ] d[\cdot] d[⋅]值变小
- 重复:持续这一过程,直到灰色节点集为空
图解说明:
💡 说明:贪心法则:每次找出树以外拥有最小 d [ u ] d[u] d[u]的顶点 u u u。这时,顶点 u u u和边 ( π ( u ) , u ) (\pi(u), u) (π(u),u)就成了树的一部分。然后通过松弛操作更新顶点 u u u的邻居 v v v。
类比理解:
就像探索未知区域:
- 黑色节点:已经探索完毕的区域(最短路径已确定)
- 灰色节点:正在探索的区域(已发现但路径可能更优)
- 白色节点:尚未发现的区域(距离为无穷)
- 贪心选择:每次选择距离最近的灰色节点(最有可能找到最短路径)
实际例子:
Dijkstra算法执行过程:
图:s --5--> a
| |
2 3
| |
v v
b --1--> c
初始化:
- s: 黑色, d[s] = 0
- a, b: 灰色, d[a] = 5, d[b] = 2
- c: 白色, d[c] = ∞
第1轮:选择b(d[b] = 2最小)
- b变黑
- 松弛b→c:d[c] = min(∞, 2+1) = 3
第2轮:选择a(d[a] = 5)
- a变黑
- 松弛a→c:d[c] = min(3, 5+3) = 3(不变)
第3轮:选择c(d[c] = 3)
- c变黑
结果:
- s到a:5
- s到b:2
- s到c:3
2.2 Dijkstra算法实现(Algorithm Implementation):伪码
概念的本质:
Dijkstra算法由初始化和循环体两部分组成。
算法伪码:
SSP-Dijkstra(G(V, E, w), s)
1 for each v ∈ V
2 d[v] ← ∞
3 π[v] ← nil
4 endfor
5 d[s] ← 0
A ← ∅
construct T(V, A) //最短路径树开始没有边
8 Q ← V //所有树外的顶点组织为Q
9 while Q ≠ ∅
10 u ← Extract-Min(Q) //d(u)值最小并从Q中剥离
11 for each v ∈ Adj[u] //方便采用基于链表的"邻接表"方式!
12 if d[u] + w(u, v) < d[v] //如果从s到u到v更短,更新d[v]
13 then d[v] ← d[u] + w(u, v)
14 π[v] ← u
15 endif endfor endwhile
16 A ← {(π[u], u): u∈V –{s}}
17 return T(V, A)
End
松弛操作(Relax):
Relax(u, v, w)
if d[u] + w(u, v) < d[v] then
d[v] ← d[u] + w(u, v)
π(v) ← u
endif
End
图解说明:
💡 说明:Dijkstra算法的做法与Prim算法几乎完全一样。Prim算法构造最小生成树,而Dijkstra算法构造最短路径树。区别在于:Prim算法中 d [ v ] d[v] d[v]表示当前集合 A A A形成的树与 v v v的距离,而在Dijkstra算法中表示树中信源 s s s到顶点 v v v的距离(累计距离)。
实际例子:
Dijkstra算法示例:
图:s --1--> a --2--> b
| |
4 3
| |
v v
c --1--> d
执行过程:
初始化:d[s]=0, d[a]=∞, d[b]=∞, d[c]=∞, d[d]=∞
Q = {s, a, b, c, d}
第1轮:u = s
- 松弛s→a:d[a] = 1, π[a] = s
- 松弛s→c:d[c] = 4, π[c] = s
Q = {a, b, c, d}
第2轮:u = a(d[a]=1最小)
- 松弛a→b:d[b] = 3, π[b] = a
- 松弛a→d:d[d] = 4, π[d] = a
Q = {b, c, d}
第3轮:u = b(d[b]=3最小)
- b没有未访问邻居
Q = {c, d}
第4轮:u = c(d[c]=4最小)
- 松弛c→d:d[d] = min(4, 4+1) = 4(不变)
Q = {d}
第5轮:u = d
Q = ∅
最短路径树:
s --1--> a --2--> b
|
4
|
v
c
结果:
- s到a:1
- s到b:3(路径:s→a→b)
- s到c:4
- s到d:4(路径:s→a→d)
2.3 Dijkstra算法的复杂度(Complexity):O(n²)或O(n log n + m)
概念的本质:
Dijkstra算法的复杂度取决于如何组织优先队列 Q Q Q:
-
数组方式:
- E x t r a c t − M i n Extract-Min Extract−Min: O ( n ) O(n) O(n),调用 n n n次 → O ( n 2 ) O(n^2) O(n2)
- D e c r e a s e K e y DecreaseKey DecreaseKey: O ( 1 ) O(1) O(1),最多执行 m m m次 → O ( m ) O(m) O(m)
- 总复杂度: O ( n 2 + m ) = O ( n 2 ) O(n^2 + m) = O(n^2) O(n2+m)=O(n2)
-
最小堆方式:
- E x t r a c t − M i n Extract-Min Extract−Min: O ( log n ) O(\log n) O(logn),调用 n n n次 → O ( n log n ) O(n \log n) O(nlogn)
- D e c r e a s e K e y DecreaseKey DecreaseKey: O ( log n ) O(\log n) O(logn),最多执行 m m m次 → O ( m log n ) O(m \log n) O(mlogn)
- 总复杂度: O ( n log n + m log n ) = O ( ( n + m ) log n ) O(n \log n + m \log n) = O((n+m) \log n) O(nlogn+mlogn)=O((n+m)logn)
-
斐波那契堆:
- E x t r a c t − M i n Extract-Min Extract−Min: O ( log n ) O(\log n) O(logn),调用 n n n次 → O ( n log n ) O(n \log n) O(nlogn)
- D e c r e a s e K e y DecreaseKey DecreaseKey: O ( 1 ) O(1) O(1),最多执行 m m m次 → O ( m ) O(m) O(m)
- 总复杂度: O ( n log n + m ) O(n \log n + m) O(nlogn+m)
图解说明:
💡 说明:斐波那契堆是专门为了Dijkstra算法而设计的——因为Dijkstra算法是Internet自治系统内部路由的主要协议OSPF的基础,其收敛速度对Internet性能具有非常重要的影响。
实际例子:
复杂度比较示例:
图:n = 1000个顶点,m = 5000条边
数组方式:
- Extract-Min:1000 × 1000 = 1,000,000次操作
- DecreaseKey:5000次操作
- 总计:O(1,005,000) ≈ O(n²)
最小堆方式:
- Extract-Min:1000 × log(1000) ≈ 10,000次操作
- DecreaseKey:5000 × log(1000) ≈ 50,000次操作
- 总计:O(60,000) ≈ O((n+m)log n)
斐波那契堆:
- Extract-Min:1000 × log(1000) ≈ 10,000次操作
- DecreaseKey:5000 × 1 = 5,000次操作
- 总计:O(15,000) ≈ O(n log n + m)
2.4 Dijkstra算法的限制(Limitations):不支持负权值
概念的本质:
Dijkstra算法的限制:
- 不支持负权值:算法要求图中的边权为非负实数,即 ∀ ( u , v ) ∈ E , w ( u , v ) ≥ 0 \forall (u,v) \in E, w(u,v) \geq 0 ∀(u,v)∈E,w(u,v)≥0
- 不支持负回路:如果网络中存在总权值为负值的环路,则将出现两点最短距离为负无穷的情况
- 负权边的影响:即使图中不存在负环路,负权边的存在仍然可能导致计算错误
原因:
- Dijkstra算法采用贪心策略,当一个节点作为(树外)最小被选中后,其 d [ ⋅ ] d[\cdot] d[⋅]就不会再重新计算(其距离值)
- 如果存在负权边,可能通过负权边找到更短的路径,但节点已经被标记为黑色,无法更新
图解说明:
💡 说明:负权边对Dijkstra算法的影响:即使图中不存在负环路,负权边的存在仍然可能导致计算错误。原因在于Dijkstra算法采用贪心策略,当一个节点作为(树外)最小被选中后,其 d [ ⋅ ] d[\cdot] d[⋅]就不会再重新计算。
实际例子:
Dijkstra算法负权边示例:
图:s --6--> a
| |
4 -3
| |
v v
b <--1--- c
Dijkstra执行(错误):
1. 选择b(d[b]=4最小)
- b变黑
2. 选择a(d[a]=6)
- a变黑
- 松弛a→c:d[c] = 6+(-3) = 3
3. 选择c(d[c]=3)
- c变黑
结果:d[c] = 3
但实际最短路径:
s → b → c:4 + 1 = 5
s → a → c:6 + (-3) = 3 ✓
虽然这个例子结果正确,但如果:
s --6--> a --(-3)--> c
| ↑
4 |
| |
v |
b -----1------|
Dijkstra会选择b(d[b]=4),然后b变黑
但实际最短路径是s→a→c(长度3),
而Dijkstra已经无法更新b到c的路径了
三、Bellman-Ford算法:动态规划求解最短路径
这一章要建立的基础:理解Bellman-Ford算法采用动态规划,允许负权值但不能有负回路,可以检测负回路。
核心问题:如何用动态规划求解允许负权值的单源最短路径问题?
[!NOTE]
📝 关键点总结:Bellman-Ford算法(1955)允许有负权值但不能包含从源点可达的负回路。否则,Bellman-Ford算法将报告有负回路,前面的运算结果作废。Bellman-Ford算法无法正确解决有负回路的情况下最短路径问题。
3.1 Bellman-Ford算法的基本思想(Basic Idea):动态规划
概念的本质:
Bellman-Ford算法由三部分组成:
-
初始化:与Dijkstra算法的初始化完全相同
- d [ s ] = 0 d[s] = 0 d[s]=0
- d [ v ] = ∞ d[v] = \infty d[v]=∞, ∀ v ≠ s \forall v \neq s ∀v=s
- π [ v ] = n i l \pi[v] = nil π[v]=nil, ∀ v \forall v ∀v
-
循环部分:做 n − 1 n-1 n−1轮循环
- 每次循环只做一件事,即对图中每一条边 ( u , v ) (u, v) (u,v)进行一次松弛(relax)操作
- 这与Dijkstra算法不同:Dijkstra算法只对与新选中顶点 u u u关联的边进行松弛操作
-
检查部分:
- 在 n − 1 n-1 n−1轮松弛之后,再做一次松弛遍历
- 如果某个顶点 v v v的 d [ v ] d[v] d[v]可以变得更小,则说明图中有源点可达的负回路,算法失败
- 否则,算法成功结束
算法伪码:
Bellman-Ford (G(V, E, W), s)
1. Initialize-Single-Source (G(V, E, W), s) //初始化与Dijkstra算法相同
2. for k ← 1 to n -1 //路径上的边数
3. for each edge (u, v) ∈ E
4. Relax(u, v, w)
5. endfor
6. endfor
7. for each edge (u, v) ∈ E //开始检测负回路
8. if d[u] + w(u, v) < d[v]
9. then return False //有源点可达负回路,失败
10. endif
11. endfor
12. A ← {(π(v), v) | v ≠ s, v ∈ V}
13. construct T(V, A) //构造最短路径树
14. return (True, T(V, A))
15. End
图解说明:
💡 说明:实际执行过程中,只要在某一轮松弛遍历后,如果源点到所有顶点的最短路径长度都没有发生改变,那么Bellman-Ford算法就可以提前结束了。Bellman-Ford算法的复杂度是 O ( n m ) O(nm) O(nm),因为每一轮松弛遍历需要 O ( m ) O(m) O(m)时间而包括检测在内一共需要 n n n轮松弛遍历。
类比理解:
就像逐步传播信息:
- 第1轮:信息从源点传播到距离为1跳的所有节点
- 第2轮:信息传播到距离为2跳的所有节点
- 继续:直到信息传播到所有可达节点
- 检测:如果还能继续传播(距离还能变小),说明存在负回路
实际例子:
Bellman-Ford算法执行过程:
图:s --1--> a --2--> b
| |
4 -1
| |
v v
c <--1--- d
初始化:d[s]=0, d[a]=∞, d[b]=∞, d[c]=∞, d[d]=∞
第1轮松弛(所有边):
- s→a:d[a] = 1
- s→c:d[c] = 4
- a→b:d[b] = 3
- a→d:d[d] = 2
- d→c:d[c] = min(4, 2+1) = 3
第2轮松弛:
- s→a:d[a] = 1(不变)
- s→c:d[c] = 3(不变)
- a→b:d[b] = 3(不变)
- a→d:d[d] = 2(不变)
- d→c:d[c] = min(3, 2+1) = 3(不变)
第3轮松弛:无变化
检测:再松弛一次,无变化 → 无负回路
结果:
- s到a:1
- s到b:3
- s到c:3(路径:s→a→d→c)
- s到d:2
3.2 Bellman-Ford算法的动态规划解释(Dynamic Programming Interpretation)
概念的本质:
从动态规划的角度解释Bellman-Ford算法:
令 d ( v , k ) d(v,k) d(v,k)表示从 s s s到 v v v且至多包含 k k k条边的最短路长度。
归纳公式:
d ( v , k ) = min { d ( v , k − 1 ) , min u ∈ V { d ( u , k − 1 ) + w ( u , v ) } } d(v,k) = \min\{d(v,k-1), \min_{u \in V}\{d(u,k-1) + w(u,v)\}\} d(v,k)=min{d(v,k−1),u∈Vmin{d(u,k−1)+w(u,v)}}
含义:
- 要找一条从 s s s到 v v v由 k k k条链路组成的最短路,可以变换为:先寻找从 s s s到 v v v的每一个邻居且由 k − 1 k-1 k−1条边组成的最短路,然后再从每个邻居到 v v v,从中选小的
图解说明:
💡 说明:如果这步起决定作用的话,则 d ( v , k ) d(v,k) d(v,k)对应的路径正好是 k k k条边构成的。在Dijkstra算法中,短的加权路径先被确定,而在Bellman-Ford算法中,跳数少(即:边数少)的最短路径,先确定。
实际例子:
动态规划解释示例:
图:s --1--> a --2--> b
d(a,1) = 1(s→a,1条边)
d(b,1) = ∞(无法用1条边到达)
d(a,2) = min{d(a,1), d(s,1)+w(s,a)} = min{1, 0+1} = 1
d(b,2) = min{d(b,1), d(a,1)+w(a,b)} = min{∞, 1+2} = 3
d(a,3) = 1(不变)
d(b,3) = min{d(b,2), d(a,2)+w(a,b)} = min{3, 1+2} = 3
最终:d(b) = d(b,n-1) = 3
3.3 Bellman-Ford算法的正确性证明(Correctness Proof)
概念的本质:
Bellman-Ford算法正确性证明包括两部分:
第一部分:证明在图 G ( V , E , W ) G(V, E, W) G(V,E,W)不含源点可达负回路的情况下,Bellman-Ford算法正确地算出源点到各顶点的最短路径。
定理10.1:如果有向图 G ( V , E , w ) G(V, E, w) G(V,E,w)中不含源点 s s s可达的负回路,那么在算法Bellman-Ford进行了 n − 1 n-1 n−1轮松弛遍历后,图 G G G中从源点 s s s到任意顶点 v v v的最短路径的距离是 d [ v ] d[v] d[v],即 δ ( s , v ) = d [ v ] \delta(s, v) = d[v] δ(s,v)=d[v]。
证明思路:
- 因为没有负回路,从 s s s到任意顶点 v v v的所有最短路径中,必定有一条是简单路径(不含回路)
- 简单路径最多有 n − 1 n-1 n−1条边
- 用归纳法证明:如果从 s s s到 v v v的最短路径有 k k k条边,那么在第 k k k轮松弛遍历后,这条最短路径及其距离 d [ v ] = δ ( s , v ) d[v] = \delta(s, v) d[v]=δ(s,v)就确定下来了
第二部分:证明在 G ( V , E , W ) G(V, E, W) G(V,E,W)含有源点可达负回路的情况下,Bellman-Ford算法正确地检测出负回路并报告。
定理10.2:如果带权有向图 G ( V , E , W ) G(V, E, W) G(V,E,W)中含有一个源点 s s s可达的负回路,那么它一定会被 n − 1 n-1 n−1轮松弛遍历之后的检测中正确地检测出来。
图解说明:
💡 说明:要注意的是,只有在所有边的权值都非负的情况下,Bellman-Ford算法才能用于无向图。这是因为如果无向图有一条边 ( u , v ) (u, v) (u,v)权值为负,则将其变成有向边之后,其自身(如: ( u , v , u ) (u, v, u) (u,v,u))就构成了一个负环路。
实际例子:
正确性证明示例:
无负回路情况:
图:s --1--> a --2--> b
第1轮:d[a] = 1, d[b] = ∞
第2轮:d[b] = 3
检测:无变化 → 正确
有负回路情况:
图:s --1--> a --(-3)--> b --2--> a(形成回路a→b→a,总权-1)
第1轮:d[a] = 1, d[b] = -2
第2轮:d[a] = -1(通过b→a更新)
第3轮:d[b] = -4(通过a→b更新)
检测:d[a]还能变小 → 发现负回路
3.4 Bellman-Ford算法的复杂度(Complexity):O(nm)
概念的本质:
Bellman-Ford算法的复杂度是 O ( n m ) O(nm) O(nm),因为:
- 每一轮松弛遍历需要 O ( m ) O(m) O(m)时间(遍历所有边)
- 包括检测在内一共需要 n n n轮松弛遍历
- 总复杂度: O ( n m ) O(nm) O(nm)
最坏情况:
- 最坏情况下,Bellman-Ford算法需要对所有边松弛 n n n轮
- 例如:如果松弛过程是从右到左的话,可能需要 n n n轮
- 每一轮松弛中,对于全网 2 n − 1 2n-1 2n−1条边的松弛,其实只有最后一步是有用的,其它的都是无效松弛
优化:
- 实际执行过程中,只要在某一轮松弛遍历后,如果源点到所有顶点的最短路径长度都没有发生改变,那么Bellman-Ford算法就可以提前结束
图解说明:
💡 说明:对于采用贪心法的Dijkstra算法来说,每条边只需要访问一次,复杂度为 O ( m log n ) O(m \log n) O(mlogn)(使用堆)或 O ( n 2 ) O(n^2) O(n2)(使用数组)。Bellman-Ford算法虽然复杂度更高,但可以处理负权值和检测负回路。
实际例子:
复杂度比较:
图:n = 1000个顶点,m = 5000条边
Dijkstra(堆):
- 每条边访问1次
- 复杂度:O(m log n) = O(5000 × 10) = O(50,000)
Bellman-Ford:
- 每轮访问所有边:5000次
- n轮:1000 × 5000 = 5,000,000次
- 复杂度:O(nm) = O(5,000,000)
Bellman-Ford更慢,但可以处理负权值
四、两种算法的比较(Comparison):理解它们的区别
这一章要建立的基础:理解Dijkstra和Bellman-Ford两种算法的区别,知道在什么情况下使用哪种算法。
核心问题:两种算法有什么相同点和不同点?如何选择合适的方法?
[!NOTE]
📝 关键点总结:Dijkstra算法要求图中的链路权重为非负实数,而Bellman-Ford算法允许有负权值但不能包含从源点可达的负回路。Dijkstra算法采用贪心法,Bellman-Ford算法采用动态规划。
4.1 相同点(Similarities):都求解单源最短路径
概念的本质:
两种算法的相同点:
- 都求解单源最短路径问题:给定源点 s s s,求到所有其他顶点的最短路径
- 都利用最优子结构特性:最短路径的子路径也是最短路径
- 都使用松弛操作:通过松弛操作更新距离估计
- 初始化相同: d [ s ] = 0 d[s] = 0 d[s]=0, d [ v ] = ∞ d[v] = \infty d[v]=∞, ∀ v ≠ s \forall v \neq s ∀v=s
图解说明:
4.2 不同点(Differences):算法策略、适用条件、复杂度
概念的本质:
两种算法的不同点:
| 特性 | Dijkstra算法 | Bellman-Ford算法 |
|---|---|---|
| 算法策略 | 贪心法 | 动态规划 |
| 适用条件 | 边权非负 | 允许负权值,但不能有负回路 |
| 时间复杂度 | O ( n 2 ) O(n^2) O(n2)或 O ( n log n + m ) O(n \log n + m) O(nlogn+m) | O ( n m ) O(nm) O(nm) |
| 空间复杂度 | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) |
| 松弛方式 | 只对选中顶点的出边松弛 | 对所有边松弛 |
| 确定顺序 | 按距离从小到大确定 | 按边数从少到多确定 |
| 负回路检测 | 不支持 | 支持(可以检测) |
图解说明:
💡 说明:在Dijkstra算法中,短的加权路径先被确定,而在Bellman-Ford算法中,跳数少(即:边数少)的最短路径,先确定。Dijkstra算法只对与新选中顶点 u u u关联的边进行松弛操作,而Bellman-Ford算法对图中每一条边都进行松弛操作。
实际例子:
算法选择示例:
场景1:所有边权非负
- 推荐:Dijkstra算法
- 理由:时间复杂度更低(O(n log n + m) vs O(nm))
场景2:存在负权值但无负回路
- 推荐:Bellman-Ford算法
- 理由:Dijkstra算法可能给出错误结果
场景3:需要检测负回路
- 推荐:Bellman-Ford算法
- 理由:Dijkstra算法不支持
场景4:稠密图(m接近n²)
- 推荐:Dijkstra算法(数组实现)
- 理由:O(n²) vs O(n³),Dijkstra更快
4.3 实际应用(Applications):Internet路由协议
概念的本质:
最短路径算法在Internet路由中的应用:
-
OSPF协议(基于Dijkstra算法):
- 从20世纪80年代开始采用基于Dijkstra算法的OSPF协议(RFC 2328)进行Internet自治系统内部路由
- 优势:收敛性能好
- 缺点:需要全局拓扑信息
-
BGP协议(基于改进Bellman-Ford算法):
- Internet全球路由仍然采用基于改进Bellman-Ford算法的边界网关协议BGP(Border Gateway Protocol, RFC 4271)
- 优势:不需要知道各自治系统的内部拓扑结构
- 缺点:存在路由震荡和慢收敛问题
-
RIP协议(早期,基于Bellman-Ford算法):
- Internet的早期版本arpanet采用了基于Bellman-Ford算法的RIP协议(RFC 1058)
- 优势:只需要邻居之间交换路由表
- 缺点:存在路由震荡和慢收敛问题
图解说明:
💡 说明:当前已经改进为根据Incremental Shortest Path Tree Algorithm。斐波那契堆是专门为了Dijkstra算法而设计的——因为Dijkstra算法是Internet自治系统内部路由的主要协议OSPF的基础,其收敛速度对Internet性能具有非常重要的影响。
实际例子:
路由协议应用示例:
OSPF(Dijkstra):
- 应用范围:自治系统(AS)内部
- 特点:需要知道AS内所有路由器的拓扑
- 优势:快速收敛,无路由环路
- 复杂度:O(n log n + m)
BGP(改进Bellman-Ford):
- 应用范围:全球Internet路由
- 特点:只需要知道邻居AS的路由信息
- 优势:不需要全局拓扑,适合大规模网络
- 复杂度:O(nm),但实际中可以通过优化提前结束
📝 本章总结
核心要点回顾:
-
单源最短路径问题:
- 给定源点 s s s,求到所有其他顶点的最短路径
- 路径长度 = 路径上所有边权重之和
- 具有最优子结构特性
-
Dijkstra算法:
- 策略:贪心法
- 条件:边权非负
- 复杂度: O ( n 2 ) O(n^2) O(n2)或 O ( n log n + m ) O(n \log n + m) O(nlogn+m)
- 特点:按距离从小到大确定,每条边只访问一次
- 应用:OSPF路由协议
-
Bellman-Ford算法:
- 策略:动态规划
- 条件:允许负权值,但不能有负回路
- 复杂度: O ( n m ) O(nm) O(nm)
- 特点:按边数从少到多确定,对所有边松弛 n − 1 n-1 n−1轮
- 应用:BGP路由协议,检测负回路
-
算法比较:
- 相同点:都求解单源最短路径,都使用松弛操作
- 不同点:策略、适用条件、复杂度不同
- 选择:根据图的特点(是否有负权值)选择
知识地图:
关键决策点:
- 何时使用Dijkstra:所有边权非负,需要高效算法
- 何时使用Bellman-Ford:存在负权值,或需要检测负回路
- 如何优化Dijkstra:使用斐波那契堆可以降到 O ( n log n + m ) O(n \log n + m) O(nlogn+m)
- 如何优化Bellman-Ford:如果某一轮无变化,可以提前结束
💡 延伸学习:最短路径算法是图算法的基础,掌握它们有助于我们:
- 理解网络路由协议的工作原理
- 解决实际的最短路径问题(导航、物流等)
- 为学习更复杂的图算法打下基础
3140

被折叠的 条评论
为什么被折叠?



