|
| 1 | +## 1. 多源最短路径简介 |
| 2 | + |
| 3 | +> **多源最短路径(All-Pairs Shortest Paths)**:对于一个带权图 $G = (V, E)$,计算图中任意两个顶点之间的最短路径长度。 |
| 4 | +
|
| 5 | +多源最短路径问题的核心是找到图中任意两个顶点之间的最短路径。这个问题在许多实际应用中都非常重要,比如: |
| 6 | + |
| 7 | +1. 网络路由中的路由表计算 |
| 8 | +2. 地图导航系统中的距离矩阵计算 |
| 9 | +3. 社交网络中的最短关系链分析 |
| 10 | +4. 交通网络中的最优路径规划 |
| 11 | + |
| 12 | +常见的解决多源最短路径问题的算法包括: |
| 13 | + |
| 14 | +1. **Floyd-Warshall 算法**:一种动态规划算法,可以处理负权边,但不能处理负权环。 |
| 15 | +2. **Johnson 算法**:结合了 Bellman-Ford 算法和 Dijkstra 算法,可以处理负权边,但不能处理负权环。 |
| 16 | +3. **重复 Dijkstra 算法**:对每个顶点运行一次 Dijkstra 算法,适用于无负权边的图。 |
| 17 | + |
| 18 | +## 2. Floyd-Warshall 算法 |
| 19 | + |
| 20 | +### 2.1 Floyd-Warshall 算法的算法思想 |
| 21 | + |
| 22 | +> **Floyd-Warshall 算法**:一种动态规划算法,通过逐步考虑中间顶点来更新任意两点之间的最短路径。 |
| 23 | +
|
| 24 | +Floyd-Warshall 算法的核心思想是: |
| 25 | + |
| 26 | +1. 对于图中的任意两个顶点 $i$ 和 $j$,考虑是否存在一个顶点 $k$,使得从 $i$ 到 $k$ 再到 $j$ 的路径比已知的从 $i$ 到 $j$ 的路径更短 |
| 27 | +2. 如果存在这样的顶点 $k$,则更新从 $i$ 到 $j$ 的最短路径 |
| 28 | +3. 通过考虑所有可能的中间顶点 $k$,最终得到任意两点之间的最短路径 |
| 29 | + |
| 30 | +### 2.2 Floyd-Warshall 算法的实现步骤 |
| 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. 返回最终的距离矩阵 |
| 38 | + |
| 39 | +### 2.3 Floyd-Warshall 算法的实现代码 |
| 40 | + |
| 41 | +```python |
| 42 | +def floyd_warshall(graph, n): |
| 43 | + # 初始化距离矩阵 |
| 44 | + dist = [[float('inf') for _ in range(n)] for _ in range(n)] |
| 45 | + |
| 46 | + # 设置直接相连的顶点之间的距离 |
| 47 | + for i in range(n): |
| 48 | + dist[i][i] = 0 |
| 49 | + for j, weight in graph[i].items(): |
| 50 | + dist[i][j] = weight |
| 51 | + |
| 52 | + # 考虑每个顶点作为中间顶点 |
| 53 | + for k in range(n): |
| 54 | + for i in range(n): |
| 55 | + 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]) |
| 58 | + |
| 59 | + return dist |
| 60 | +``` |
| 61 | + |
| 62 | +代码解释: |
| 63 | + |
| 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$ 的最短路径长度。 |
| 71 | + |
| 72 | +### 2.4 Floyd-Warshall 算法复杂度分析 |
| 73 | + |
| 74 | +- **时间复杂度**:$O(V^3)$ |
| 75 | + - 需要三层嵌套循环,分别遍历所有顶点 |
| 76 | + - 因此总时间复杂度为 $O(V^3)$ |
| 77 | + |
| 78 | +- **空间复杂度**:$O(V^2)$ |
| 79 | + - 需要存储距离矩阵,大小为 $O(V^2)$ |
| 80 | + - 不需要额外的空间来存储图的结构,因为使用邻接表表示 |
| 81 | + |
| 82 | +Floyd-Warshall 算法的主要优势在于: |
| 83 | + |
| 84 | +1. 实现简单,容易理解 |
| 85 | +2. 可以处理负权边 |
| 86 | +3. 可以检测负权环(如果某个顶点到自身的距离变为负数,说明存在负权环) |
| 87 | +4. 适用于稠密图 |
| 88 | + |
| 89 | +主要缺点: |
| 90 | + |
| 91 | +1. 时间复杂度较高,不适用于大规模图 |
| 92 | +2. 空间复杂度较高,需要存储完整的距离矩阵 |
| 93 | +3. 不能处理负权环 |
| 94 | + |
| 95 | +## 3. Johnson 算法 |
| 96 | + |
| 97 | +### 3.1 Johnson 算法的算法思想 |
| 98 | + |
| 99 | +> **Johnson 算法**:一种结合了 Bellman-Ford 算法和 Dijkstra 算法的多源最短路径算法,可以处理负权边,但不能处理负权环。 |
| 100 | +
|
| 101 | +Johnson 算法的核心思想是: |
| 102 | + |
| 103 | +1. 通过重新赋权,将图中的负权边转换为非负权边 |
| 104 | +2. 对每个顶点运行一次 Dijkstra 算法,计算最短路径 |
| 105 | +3. 将结果转换回原始权重 |
| 106 | + |
| 107 | +### 3.2 Johnson 算法的实现步骤 |
| 108 | + |
| 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)$ |
| 114 | + |
| 115 | +### 3.3 Johnson 算法的实现代码 |
| 116 | + |
| 117 | +```python |
| 118 | +from collections import defaultdict |
| 119 | +import heapq |
| 120 | + |
| 121 | +def johnson(graph, n): |
| 122 | + # 添加新顶点 s |
| 123 | + new_graph = defaultdict(dict) |
| 124 | + for u in graph: |
| 125 | + for v, w in graph[u].items(): |
| 126 | + new_graph[u][v] = w |
| 127 | + new_graph[n][u] = 0 # 从 s 到所有顶点的边权重为 0 |
| 128 | + |
| 129 | + # 使用 Bellman-Ford 算法计算 h(v) |
| 130 | + h = [float('inf')] * (n + 1) |
| 131 | + h[n] = 0 |
| 132 | + |
| 133 | + for _ in range(n): |
| 134 | + for u in new_graph: |
| 135 | + for v, w in new_graph[u].items(): |
| 136 | + if h[v] > h[u] + w: |
| 137 | + h[v] = h[u] + w |
| 138 | + |
| 139 | + # 检查是否存在负权环 |
| 140 | + for u in new_graph: |
| 141 | + for v, w in new_graph[u].items(): |
| 142 | + if h[v] > h[u] + w: |
| 143 | + return None # 存在负权环 |
| 144 | + |
| 145 | + # 重新赋权 |
| 146 | + reweighted_graph = defaultdict(dict) |
| 147 | + for u in graph: |
| 148 | + for v, w in graph[u].items(): |
| 149 | + reweighted_graph[u][v] = w + h[u] - h[v] |
| 150 | + |
| 151 | + # 对每个顶点运行 Dijkstra 算法 |
| 152 | + dist = [[float('inf') for _ in range(n)] for _ in range(n)] |
| 153 | + for source in range(n): |
| 154 | + # 初始化距离数组 |
| 155 | + d = [float('inf')] * n |
| 156 | + 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: |
| 165 | + continue |
| 166 | + visited.add(u) |
| 167 | + |
| 168 | + 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 | + # 转换回原始权重 |
| 174 | + for v in range(n): |
| 175 | + if d[v] != float('inf'): |
| 176 | + dist[source][v] = d[v] - h[source] + h[v] |
| 177 | + |
| 178 | + return dist |
| 179 | +``` |
| 180 | + |
| 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 | +### 3.4 Johnson 算法复杂度分析 |
| 193 | + |
| 194 | +- **时间复杂度**:$O(VE \log V)$ |
| 195 | + - 需要运行一次 Bellman-Ford 算法,时间复杂度为 $O(VE)$ |
| 196 | + - 需要运行 $V$ 次 Dijkstra 算法,每次时间复杂度为 $O(E \log V)$ |
| 197 | + - 因此总时间复杂度为 $O(VE \log V)$ |
| 198 | + |
| 199 | +- **空间复杂度**:$O(V^2)$ |
| 200 | + - 需要存储距离矩阵,大小为 $O(V^2)$ |
| 201 | + - 需要存储重新赋权后的图,大小为 $O(E)$ |
| 202 | + - 因此总空间复杂度为 $O(V^2)$ |
0 commit comments