1
1
## 1. 多源最短路径简介
2
2
3
- > ** 多源最短路径(All-Pairs Shortest Paths)** :对于一个带权图 $G = (V, E)$,计算图中任意两个顶点之间的最短路径长度 。
3
+ > ** 多源最短路径(All-Pairs Shortest Paths)** :指的是在一个带权图 $G = (V, E)$ 中,计算任意两个顶点之间的最短路径长度 。
4
4
5
- 多源最短路径问题的核心是找到图中任意两个顶点之间的最短路径。这个问题在许多实际应用中都非常重要,比如 :
5
+ 多源最短路径问题的本质,就是要找出图中每一对顶点之间的最短路径。这类问题在实际生活和工程中非常常见,例如 :
6
6
7
- 1 . 网络路由中的路由表计算
8
- 2 . 地图导航系统中的距离矩阵计算
9
- 3 . 社交网络中的最短关系链分析
10
- 4 . 交通网络中的最优路径规划
7
+ 1 . 网络通信中,生成路由表以确定任意两点之间的最优传输路径;
8
+ 2 . 地图导航系统中,计算所有地点之间的距离矩阵;
9
+ 3 . 社交网络分析中,寻找两个人之间的最短关系链;
10
+ 4 . 交通网络中,规划任意两地之间的最优行车路线。
11
11
12
- 常见的解决多源最短路径问题的算法包括 :
12
+ 常用的多源最短路径算法有 :
13
13
14
- 1 . ** Floyd-Warshall 算法** :一种动态规划算法,可以处理负权边,但不能处理负权环 。
15
- 2 . ** Johnson 算法** :结合了 Bellman-Ford 算法和 Dijkstra 算法,可以处理负权边,但不能处理负权环 。
16
- 3 . ** 重复 Dijkstra 算法** :对每个顶点运行一次 Dijkstra 算法,适用于无负权边的图 。
14
+ 1 . ** Floyd-Warshall 算法** :一种基于动态规划的方法,能处理负权边,但无法处理负权环 。
15
+ 2 . ** Johnson 算法** :结合了 Bellman-Ford 和 Dijkstra 算法,既能处理负权边,也能高效应对稀疏图,但同样不能处理负权环 。
16
+ 3 . ** 多次 Dijkstra 算法** :对每个顶点分别运行一次 Dijkstra 算法,适用于没有负权边的图 。
17
17
18
18
## 2. Floyd-Warshall 算法
19
19
20
- ### 2.1 Floyd-Warshall 算法的算法思想
20
+ ### 2.1 Floyd-Warshall 算法的核心思想
21
21
22
- > ** Floyd-Warshall 算法** :一种动态规划算法,通过逐步考虑中间顶点来更新任意两点之间的最短路径 。
22
+ > ** Floyd-Warshall 算法** :这是一种经典的动态规划算法,通过不断尝试引入不同的中间节点,来优化任意两点之间的最短路径 。
23
23
24
- Floyd-Warshall 算法的核心思想是 :
24
+ 通俗来说, Floyd-Warshall 算法的核心思想如下 :
25
25
26
- 1 . 对于图中的任意两个顶点 $i$ 和 $j$,考虑是否存在一个顶点 $k,ドル使得从 $i$ 到 $k$ 再到 $j$ 的路径比已知的从 $i$ 到 $j$ 的路径更短
27
- 2 . 如果存在这样的顶点 $ k,ドル则更新从 $i $ 到 $j$ 的最短路径
28
- 3 . 通过考虑所有可能的中间顶点 $k,ドル最终得到任意两点之间的最短路径
26
+ 1 . 假设要找从顶点 $i$ 到顶点 $j$ 的最短路径,试着经过某个中间顶点 $k,ドル看看能不能让路径更短。
27
+ 2 . 如果发现「先从 $i$ 到 $ k,ドル再从 $k $ 到 $j$」的路径比原来「直接从 $i$ 到 $j$」的路径更短,就用这个更短的路径来更新答案。
28
+ 3 . 依次尝试所有顶点作为中间点 $k,ドル每次都用上述方法去优化所有点对之间的最短路径,最终就能得到全局最优解。
29
29
30
30
### 2.2 Floyd-Warshall 算法的实现步骤
31
31
32
- 1 . 初始化距离矩阵 $dist,ドル其中 $dist[ i] [ j ] $ 表示从顶点 $i$ 到顶点 $j$ 的最短路径长度
33
- 2 . 对于每对顶点 $(i, j),ドル如果存在边 $(i, j),ドル则 $dist[ i] [ j ] $ 设为边的权重,否则设为无穷大
34
- 3 . 对于每个顶点 $k$,作为中间顶点 :
35
- - 对于每对顶点 $(i, j),ドル如果 $ dist[ i] [ k ] + dist[ k] [ j ] < dist[ i] [ j ] $,则更新 $dist[ i] [ j ] $
36
- 4 . 重复步骤 3,直到考虑完所有可能的中间顶点
37
- 5 . 返回最终的距离矩阵
32
+ 1 . 先初始化一个距离矩阵 $dist,ドル$dist[ i] [ j ] $ 表示从顶点 $i$ 到顶点 $j$ 的当前最短路径长度。
33
+ 2 . 如果 $i$ 和 $j$ 之间有直接的边,就把 $dist[ i] [ j ] $ 设为这条边的权重;如果没有,设为无穷大(表示不可达)。
34
+ 3 . 然后,依次枚举每个顶点 $k$ 作为「中转站」 :
35
+ - 对于所有顶点对 $(i, j),ドル如果「从 $i$ 经过 $k$ 到 $j$」的路径更短(即 $ dist[ i] [ k ] + dist[ k] [ j ] < dist[ i] [ j ] $),就用更短的路径更新 $dist[ i] [ j ] $。
36
+ 4 . 重复第 3 步,直到所有顶点都被作为中间点尝试过。
37
+ 5 . 最终,$dist$ 矩阵中每个 $dist [ i ] [ j ] $ 就是从 $i$ 到 $j$ 的最短路径长度。
38
38
39
39
### 2.3 Floyd-Warshall 算法的实现代码
40
40
41
41
``` python
42
42
def floyd_warshall (graph , n ):
43
- # 初始化距离矩阵
44
- dist = [[float (' inf' ) for _ in range (n)] for _ in range (n)]
43
+ """
44
+ Floyd-Warshall 算法,计算所有点对之间的最短路径。
45
+ :param graph: 邻接表,graph[i] = {j: weight, ...},节点编号为 0~n-1
46
+ :param n: 节点总数
47
+ :return: dist 矩阵,dist[i][j] 表示 i 到 j 的最短路径长度
48
+ """
49
+ # 初始化距离矩阵,所有点对距离设为无穷大
50
+ dist = [[float (' inf' )] * n for _ in range (n)]
45
51
46
- # 设置直接相连的顶点之间的距离
52
+ # 距离矩阵对角线设为 0,表示自己到自己的距离为 0
47
53
for i in range (n):
48
54
dist[i][i] = 0
49
- for j, weight in graph[i].items():
55
+ # 设置直接相连的顶点之间的距离
56
+ for j, weight in graph.get(i, {}).items():
50
57
dist[i][j] = weight
51
-
52
- # 考虑每个顶点作为中间顶点
58
+
59
+ # 三重循环,枚举每个中间点 k
53
60
for k in range (n):
54
61
for i in range (n):
62
+ # 跳过不可达的起点
63
+ if dist[i][k] == float (' inf' ):
64
+ continue
55
65
for j in range (n):
56
- if dist[i][k] != float (' inf' ) and dist[k][j] != float (' inf' ):
57
- dist[i][j] = min (dist[i][j], dist[i][k] + dist[k][j])
66
+ # 跳过不可达的终点
67
+ if dist[k][j] == float (' inf' ):
68
+ continue
69
+ # 如果经过 k 能让 i 到 j 更短,则更新
70
+ if dist[i][j] > dist[i][k] + dist[k][j]:
71
+ dist[i][j] = dist[i][k] + dist[k][j]
58
72
59
73
return dist
60
74
```
61
75
62
- 代码解释:
76
+ ### 2.4 Floyd-Warshall 算法分析
63
77
64
- 1 . ` graph ` 是一个字典,表示图的邻接表。例如,` graph[0] = {1: 3, 2: 4} ` 表示从节点 0 到节点 1 的边权重为 3,到节点 2 的边权重为 4。
65
- 2 . ` n ` 是图中顶点的数量。
66
- 3 . ` dist ` 是一个二维数组,存储任意两点之间的最短路径长度。
67
- 4 . 首先初始化距离矩阵,将对角线元素设为 0,表示顶点到自身的距离为 0。
68
- 5 . 然后设置直接相连的顶点之间的距离。
69
- 6 . 主循环中,对于每个顶点 $k,ドル考虑它作为中间顶点时,是否能缩短其他顶点之间的距离。
70
- 7 . 最终返回的距离矩阵中,$dist[ i] [ j ] $ 表示从顶点 $i$ 到顶点 $j$ 的最短路径长度。
78
+ - ** 时间复杂度** :$O(V^3)$
79
+ - 算法包含三重嵌套循环,分别枚举所有中间点、起点和终点,因此总时间复杂度为 $O(V^3)$。
71
80
72
- ### 2.4 Floyd-Warshall 算法复杂度分析
81
+ - ** 空间复杂度** :$O(V^2)$
82
+ - 主要空间消耗在距离矩阵 $dist,ドル需要 $O(V^2)$ 的空间。
83
+ - 由于采用邻接表存储原图结构,无需额外空间存储图的边。
73
84
74
- - ** 时间复杂度** :$O(V^3)$
75
- - 需要三层嵌套循环,分别遍历所有顶点
76
- - 因此总时间复杂度为 $O(V^3)$
77
-
78
- - ** 空间复杂度** :$O(V^2)$
79
- - 需要存储距离矩阵,大小为 $O(V^2)$
80
- - 不需要额外的空间来存储图的结构,因为使用邻接表表示
85
+ ** Floyd-Warshall 算法优点** :
81
86
82
- Floyd-Warshall 算法的主要优势在于:
87
+ 1 . 实现简洁,易于理解和编码。
88
+ 2 . 能处理负权边(但不能有负权环)。
89
+ 3 . 可用于检测负权环(若某个顶点 $i$ 满足 $dist[ i] [ i ] < 0,ドル则存在负权环)。
90
+ 4 . 特别适合稠密图(边数接近 $V^2$)。
83
91
84
- 1 . 实现简单,容易理解
85
- 2 . 可以处理负权边
86
- 3 . 可以检测负权环(如果某个顶点到自身的距离变为负数,说明存在负权环)
87
- 4 . 适用于稠密图
92
+ ** Floyd-Warshall 算法缺点** :
88
93
89
- 主要缺点:
90
-
91
- 1 . 时间复杂度较高,不适用于大规模图
92
- 2 . 空间复杂度较高,需要存储完整的距离矩阵
93
- 3 . 不能处理负权环
94
+ 1 . 时间复杂度较高,不适合节点数很大的图。
95
+ 2 . 空间复杂度较高,需要维护完整的 $V \times V$ 距离矩阵。
96
+ 3 . 无法处理存在负权环的情况(若有负权环,最短路无意义)。
94
97
95
98
## 3. Johnson 算法
96
99
97
- ### 3.1 Johnson 算法的算法思想
100
+ ### 3.1 Johnson 算法的核心思想
98
101
99
- > ** Johnson 算法** :一种结合了 Bellman-Ford 算法和 Dijkstra 算法的多源最短路径算法,可以处理负权边,但不能处理负权环 。
102
+ > ** Johnson 算法** :是一种结合 Bellman-Ford 和 Dijkstra 算法的多源最短路径算法,能够处理负权边,但无法处理负权环 。
100
103
101
- Johnson 算法的核心思想是 :
104
+ Johnson 算法的核心思想如下 :
102
105
103
- 1 . 通过重新赋权,将图中的负权边转换为非负权边
104
- 2 . 对每个顶点运行一次 Dijkstra 算法,计算最短路径
105
- 3 . 将结果转换回原始权重
106
+ 1 . 通过对图进行重新赋权,将所有边权变为非负,从而使 Dijkstra 算法适用;
107
+ 2 . 对每个顶点分别运行一次 Dijkstra 算法,计算其到其他所有顶点的最短路径;
108
+ 3 . 最后将结果还原为原图的最短路径权值。
106
109
107
110
### 3.2 Johnson 算法的实现步骤
108
111
109
- 1 . 添加一个新的顶点 $s,ドル并添加从 $s$ 到所有其他顶点的边,权重为 0
110
- 2 . 使用 Bellman-Ford 算法计算从 $s$ 到所有顶点的最短路径 $ h(v)$
111
- 3 . 重新赋权:对于每条边 $(u, v),ドル新的权重为 $w(u, v) + h(u) - h(v)$
112
- 4 . 对每个顶点 $v,ドル使用 Dijkstra 算法计算从 $v$ 到所有其他顶点的最短路径
113
- 5 . 将结果转换回原始权重:对于从 $u$ 到 $v$ 的最短路径,原始权重为 $d (u, v) - h(u) + h(v)$
112
+ 1 . 向原图添加一个新顶点 $s,ドル并从 $s$ 向所有其他顶点连一条权重为 0 的边;
113
+ 2 . 使用 Bellman-Ford 算法以 $s$ 为源点,计算 $s$ 到每个顶点 $v$ 的最短距离 $ h(v)$;
114
+ 3 . 对于原图中的每条边 $(u, v),ドル将其权重调整为 $w' (u, v) = w(u, v) + h(u) - h(v)$,使所有边权非负;
115
+ 4 . 对每个顶点 $u,ドル以 $u$ 为源点在重新赋权后的图上运行 Dijkstra 算法,得到 $u$ 到所有顶点的最短距离 $d'(u, v)$;
116
+ 5 . 最终结果还原为原图权重:$d(u, v) = d' (u, v) - h(u) + h(v)$,即为原图中 $u$ 到 $v$ 的最短路径长度。
114
117
115
118
### 3.3 Johnson 算法的实现代码
116
119
@@ -119,76 +122,69 @@ from collections import defaultdict
119
122
import heapq
120
123
121
124
def johnson (graph , n ):
122
- # 添加新顶点 s
125
+ """
126
+ Johnson 算法:多源最短路径,支持负权边但不支持负权环。
127
+ :param graph: 邻接表,graph[u] = {v: w, ...},节点编号 0~n-1
128
+ :param n: 节点总数
129
+ :return: dist 矩阵,dist[i][j] 表示 i 到 j 的最短路径长度;若有负权环返回 None
130
+ """
131
+ # 1. 构建新图,添加超级源点 s(编号为 n),从 s 向所有顶点连权重为 0 的边
123
132
new_graph = defaultdict(dict )
124
133
for u in graph:
125
134
for v, w in graph[u].items():
126
135
new_graph[u][v] = w
127
- new_graph[n][u] = 0 # 从 s 到所有顶点的边权重为 0
128
-
129
- # 使用 Bellman-Ford 算法计算 h(v)
136
+ for u in range (n):
137
+ new_graph[n][u] = 0 # s -> u,权重为 0
138
+
139
+ # 2. Bellman-Ford 算法,计算超级源点 s 到每个顶点的最短距离 h(v)
130
140
h = [float (' inf' )] * (n + 1 )
131
- h[n] = 0
132
-
141
+ h[n] = 0 # s 到自身距离为 0
142
+ # 最多 n 轮松弛
133
143
for _ in range (n):
144
+ updated = False
134
145
for u in new_graph:
135
146
for v, w in new_graph[u].items():
136
- if h[v] > h[u] + w:
147
+ if h[u] != float ( ' inf ' ) and h[ v] > h[u] + w:
137
148
h[v] = h[u] + w
138
-
139
- # 检查是否存在负权环
149
+ updated = True
150
+ if not updated:
151
+ break
152
+
153
+ # 检查负权环:如果还能松弛,说明有负环
140
154
for u in new_graph:
141
155
for v, w in new_graph[u].items():
142
- if h[v] > h[u] + w:
156
+ if h[u] != float ( ' inf ' ) and h[ v] > h[u] + w:
143
157
return None # 存在负权环
144
-
145
- # 重新赋权
158
+
159
+ # 3. 重新赋权:w'(u,v) = w(u,v) + h[u] - h[v],保证所有边权非负
146
160
reweighted_graph = defaultdict(dict )
147
161
for u in graph:
148
162
for v, w in graph[u].items():
149
163
reweighted_graph[u][v] = w + h[u] - h[v]
150
-
151
- # 对每个顶点运行 Dijkstra 算法
152
- dist = [[float (' inf' )for _ in range (n)] for _ in range (n)]
164
+
165
+ # 4. 对每个顶点运行 Dijkstra 算法,计算最短路径
166
+ dist = [[float (' inf' )] * n for _ in range (n)]
153
167
for source in range (n):
154
- # 初始化距离数组
155
168
d = [float (' inf' )] * n
156
169
d[source] = 0
157
-
158
- # 使用优先队列
159
- pq = [(0 , source)]
160
- visited = set ()
161
-
162
- while pq:
163
- current_dist, u = heapq.heappop(pq)
164
- if u in visited:
170
+ heap = [(0 , source)]
171
+ visited = [False ] * n
172
+ while heap:
173
+ cur_dist, u = heapq.heappop(heap)
174
+ if visited[u]:
165
175
continue
166
- visited.add(u)
167
-
176
+ visited[u] = True
168
177
for v, w in reweighted_graph[u].items():
169
- if d[v] > current_dist + w:
170
- d[v] = current_dist + w
171
- heapq.heappush(pq, (d[v], v))
172
-
173
- # 转换回原始权重
178
+ if d[v] > cur_dist + w:
179
+ d[v] = cur_dist + w
180
+ heapq.heappush(heap, (d[v], v))
181
+ # 5. 还原原图权重
174
182
for v in range (n):
175
183
if d[v] != float (' inf' ):
176
184
dist[source][v] = d[v] - h[source] + h[v]
177
-
178
185
return dist
179
186
```
180
187
181
- 代码解释:
182
-
183
- 1 . ` graph ` 是一个字典,表示图的邻接表。
184
- 2 . ` n ` 是图中顶点的数量。
185
- 3 . 首先添加一个新的顶点 $s,ドル并添加从 $s$ 到所有其他顶点的边,权重为 0。
186
- 4 . 使用 Bellman-Ford 算法计算从 $s$ 到所有顶点的最短路径 $h(v)$。
187
- 5 . 检查是否存在负权环,如果存在则返回 None。
188
- 6 . 重新赋权,将图中的负权边转换为非负权边。
189
- 7 . 对每个顶点运行一次 Dijkstra 算法,计算最短路径。
190
- 8 . 将结果转换回原始权重,得到最终的距离矩阵。
191
-
192
188
### 3.4 Johnson 算法复杂度分析
193
189
194
190
- ** 时间复杂度** :$O(VE \log V)$
@@ -197,9 +193,8 @@ def johnson(graph, n):
197
193
- 因此总时间复杂度为 $O(VE \log V)$
198
194
199
195
- ** 空间复杂度** :$O(V^2)$
200
- - 需要存储距离矩阵,大小为 $O(V^2)$
201
- - 需要存储重新赋权后的图,大小为 $O(E)$
202
- - 因此总空间复杂度为 $O(V^2)$
196
+ - 主要空间消耗在距离矩阵($O(V^2)$)以及重新赋权后的图($O(E)$)。
197
+ - 因此总体空间复杂度为 $O(V^2)$。
203
198
204
199
## 练习题目
205
200
0 commit comments