6.3 最短路径算法
在图结构中,最短路径算法 是一类非常重要的算法,它用于找到从一个顶点到另一个顶点的最短路径。
最短路径问题广泛应用于导航、通信网络、路径规划等领域。
最短路径问题 是在图中找到从起点到目标顶点的最短路径,路径长度是由边的权重决定的。对于不同类型的图,最短路径问题可以有多种解法。
最短路径问题的两种主要分类:
-
单源最短路径:从一个源顶点出发,找到它到其他所有顶点的最短路径。
-
全源最短路径:找到图中任意两个顶点之间的最短路径。
本节将介绍常见的几种最短路径算法,包括 迪克斯特拉算法(Dijkstra) 算法和 贝尔曼-福特(Bellman-Ford) 算法,并使用 Go
语言进行实现。
本节代码存放目录为 lesson14
狄克斯特拉算法
Dijkstra 算法 是一种用于解决单源最短路径问题的算法,适用于权重为正的图。
算法步骤如下所示:
-
初始化:将起点的距离设为
0
,其他顶点的距离设为∞
(无穷大)。 -
对当前顶点的每个未访问的邻接顶点,计算其到起点的距离,更新邻接顶点的最短距离。
-
重复步骤
2
,直到所有顶点都已访问或找到了目标顶点的最短路径。
算法示例如下所示:
我们以 A
为起点,寻找 A -> D
的最短路径。
图结构:
(10)
A ------ B
\ / \
(3)\ (1) (2)
\ / \
C D
/ /
(4) (6)
\ /
E
-
初始化:将起点
A
的距离设为0
,其他顶点的距离设为无穷大∞
。这个无穷大表示的是
A
到这个顶点的距离,由于目前还是未知的,所以设置为无穷大。随着计算往下进行,这个无穷大会逐渐变小,最终收敛为一个正确的数值。
表示为:
A: 0, B: ∞, C: ∞, D: ∞, E: ∞
-
第一步:从
A
出发,寻找可以直达的未访问顶点B
和C
。-
A -> B
的距离为10
。 -
A -> C
的距离为3
。
因为
A -> C
的距离较短,因此选择C
作为当前顶点,并更新最短距离为3
:A: 0, B: 10, C: 3, D: ∞, E: ∞
-
-
第二步:从
C
出发,寻找可以直达的未访问顶点B
和E
。-
C -> B
的距离为1
,所以A -> C -> B
的距离是3 + 1 = 4
(比原来的A -> B
距离10
更短)。 -
C -> E
的距离为4
,所以A -> C -> E
的距离是3 + 4 = 7
。
更新距离
B
为更短的4
,E
为7
:A: 0, B: 4, C: 3, D: ∞, E: 7
-
-
第三步:从
B
出发,寻找可以直达的未访问顶点D
。B -> D
的距离为2
,所以A -> C -> B -> D
的距离是4 + 2 = 6
。
更新距离
D
为6
:A: 0, B: 4, C: 3, D: 6, E: 7
-
终止:此时,
D
的最短路径已经确定,A -> D
的最短路径为A -> C -> B -> D
,总距离为6
。虽然
E
也可达,但A -> E
的距离是7
,大于A -> D
的6
,因此最短路径为:A -> C -> B -> D
从上面我们可以看出,Dijkstra
算法会不断检查从起点到各个顶点的所有可能路径,逐步更新为更短的路径,直到找到最优解。
贝尔曼-福特算法
Bellman-Ford 算法 是另一种用于解决单源最短路径问题的算法,它能够处理带有负权重边的图,并且可以检测负权环。
它的核心思想就是:不断更新从起点到各个顶点的最短距离,直到所有可能的最短路径都被找到。
算法步骤如下所示:
-
初始化:设定起点的距离为
0
,其他所有顶点的距离设为∞
(表示未知或非常远)。 -
松弛操作:对每条边进行检查,如果通过这条边能找到一条更短的路径,那就更新目标顶点的距离。
-
重复松弛:你要经过所有边
V-1
次(V
是顶点数量),确保每个顶点的最短路径都被找到。比如有5
个顶点,那么就最少要经过4
次。 -
检查负环:再经过一次所有边,如果还能继续更新路径,那就说明有负权环,即无穷循环的负数路径。
算法示例如下所示:
我们以 A
为七点,找到从 A
到每个顶点的最短路径。
图结构:
A --(1)--> B
\ / \
(4)\ (-2) (3)
\ / \
C --(1)--> D
-
**初始化:**我们从起点
A
开始,所有点的初始距离如下:A: 0, B: ∞, C: ∞, D: ∞
-
松弛操作:
我们检查每条边,更新路径。- A -> B 距离是
1
,现在B
的最短距离是1
。
A: 0, B: 1, C: ∞, D: ∞
- A -> C 距离是
4
,现在C
的最短距离是4
。
A: 0, B: 1, C: 4, D: ∞
- B -> C 距离是
-2
,如果A
通过B
去C
,距离是1 + (-2) = -1
,比4
小,所以更新C
的距离为-1
。
A: 0, B: 1, C: -1, D: ∞
- B -> D 距离是
3
,A
通过B
到D
,距离是1 + 3 = 4
,更新D
的距离为4
。
A: 0, B: 1, C: -1, D: 4
- C -> D 距离是
1
,A
通过C
到D
的距离是-1 + 1 = 0
,比4
小,更新D
的距离为0
。
更新后的状态:
A: 0, B: 1, C: -1, D: 0
- A -> B 距离是
-
**重复松弛:**重复步骤 2,共进行
V-1
次(这里是3
次,因为有4
个顶点)。经过3
次松弛后,所有的距离都不会再更新了。在我们的这个例子中,后面
2
次松弛的结果都会是与上面相同的,因为已经都是最短的了。 -
检查负权环:
再做一次松弛。如果这时还能更新任何距离,说明存在负权环,否则就没有负环。
那么狄克斯特拉与贝尔曼-福特的主要区别是什么呢?
-
Dijkstra
在对邻接顶点进行计算后,就不会再次计算,一旦更新了某个顶点的最短路径,就不会再重复计算。适用于边较多的图结构。 -
Bellman-Ford
会在每一轮松弛中都对所有边进行检查,哪怕已经得到了最优解,还是会继续执行完多次检查,所以它的执行次数是比Dijkstra
要多的。适用于带有负权的图结构。
Go 语言的实现
Dijkstra 算法实现
图结构如下所示:
(10)
A ------ B
\ / \
(3)\ (1) (2)
\ / \
C D
/ /
(4) (6)
\ /
E
实现代码如下所示:
// Graph 定义图结构,使用邻接表表示
type Graph struct {
vertices map[string]map[string]int
}
// NewGraph 创建一个新的图
func NewGraph() *Graph {
return &Graph{
vertices: mak