|
1 | | -## 1.1 次短路径简介 |
| 1 | +## 1. 次短路径简介 |
2 | 2 |
|
3 | | -> **次短路径**:给定一个带权有向图,求从起点到终点的次短路径。次短路径是指长度严格大于最短路径的所有路径中长度最小的那条路径。 |
| 3 | +> **次短路径(Second Shortest Path)**:指从起点到终点的所有简单路径中,路径总权值严格大于最短路径、且在此条件下最小的那条路径。 |
4 | 4 |
|
5 | | -### 1.1.1 问题特点 |
| 5 | +次短路径的本质,是在所有从起点到终点的路径中,找到一条「长度严格大于最短路径」但又尽可能短的路径。换句话说,最短路径是最优解,次短路径则是在排除所有最优解(即所有与最短路径等长的路径)后,找到的次优解。 |
6 | 6 |
|
7 | | -- 次短路径必须严格大于最短路径 |
8 | | -- 可能存在多条最短路径,但次短路径是唯一的 |
9 | | -- 如果不存在次短路径(如最短路径是唯一的),则返回 $-1$。 |
| 7 | +这种问题在实际生活和工程中非常常见,主要应用于需要「备用方案」或「容错能力」的场景。例如: |
10 | 8 |
|
11 | | -### 1.1.2 常见变体 |
| 9 | +- **网络路由**:当主路由失效时,快速切换到次短路径,保证数据传输不中断。 |
| 10 | +- **交通导航与物流**:为司机或配送员提供绕行路线,规避拥堵或突发状况。 |
| 11 | +- **通信网络**:设计冗余链路,提高网络的健壮性和可靠性。 |
| 12 | +- **算法竞赛**:常见「求第 2 优解」类问题,或需要多方案备选时。 |
12 | 13 |
|
13 | | -1. 允许重复边的次短路径 |
14 | | -2. 不允许重复边的次短路径 |
15 | | -3. 带约束条件的次短路径(如必须经过某些节点) |
| 14 | +> **注意**:本文默认边权非负(与 Dijkstra 条件一致)。如果有负权边,请使用适配的算法并谨慎处理。 |
16 | 15 |
|
17 | | -## 1.2 次短路径基本思路 |
| 16 | +## 2. 次短路径常见解法 |
18 | 17 |
|
19 | | -求解次短路径的常用方法是使用 Dijkstra 算法的变体。基本思路如下: |
| 18 | +在实际问题中,寻找次短路径(即严格大于最短路径的最优路径)通常需要对经典的最短路算法进行适当扩展。最常用且高效的方法是基于 Dijkstra 算法的变体。 |
20 | 19 |
|
21 | | -1. 使用 Dijkstra 算法找到最短路径。 |
22 | | -2. 在寻找最短路径的过程中,同时维护次短路径。 |
23 | | -3. 对于每个节点,我们需要维护两个距离值: |
24 | | - - $dist1[u]$:从起点到节点 u 的最短距离。 |
25 | | - - $dist2[u]$:从起点到节点 u 的次短距离。 |
| 20 | +### 2.1 扩展版 Dijkstra 的核心思路 |
26 | 21 |
|
27 | | -### 1.2.1 具体实现步骤 |
| 22 | +> **扩展版 Dijkstra 的核心思路**: |
| 23 | +> |
| 24 | +> 在使用优先队列寻找最短路的过程中,同时为每个节点记录两条路径长度:一条是目前已知的最短路径,另一条是比最短路径严格更长、但次优的路径。每次处理节点时,尝试用新路径更新这两条记录:如果新路径比当前最短路径还短,就把原最短路径作为次短路径,并更新最短路径;如果新路径介于最短和次短之间,就更新次短路径。其余情况直接跳过。最终,终点的次短路径记录就是所求答案。 |
28 | 25 |
|
29 | | -1. 初始化 $dist1$ 和 $dist2$ 数组,所有值设为无穷大。 |
30 | | -2. 将起点加入优先队列,距离为 0ドル$。 |
31 | | -3. 每次从队列中取出距离最小的节点 $u$。 |
32 | | -4. 遍历 $u$ 的所有邻接节点 $v$: |
33 | | - - 如果找到更短的路径,更新 $dist1[v]$。 |
34 | | - - 如果找到次短的路径,更新 $dist2[v]$。 |
35 | | -5. 最终 $dist2[target]$ 即为所求的次短路径长度。 |
| 26 | +这种方法思路清晰、实现简单,是解决次短路径问题的主流方案。 |
36 | 27 |
|
37 | | -### 1.2.2 算法正确性证明 |
| 28 | +### 2.2 扩展版 Dijkstra 的具体步骤 |
38 | 29 |
|
39 | | -1. 对于任意节点 $u,ドル$dist1[u]$ 一定是最短路径长度。 |
40 | | -2. 对于任意节点 $u,ドル$dist2[u]$ 一定是次短路径长度。 |
41 | | -3. 算法会考虑所有可能的路径,因此不会遗漏次短路径。 |
| 30 | +1. 初始化 $dist1$ 和 $dist2$ 数组,全部赋值为无穷大(表示尚未到达)。 |
| 31 | +2. 将起点的 $dist1$ 设为 0ドル,ドル并将起点以距离 0ドル$ 加入优先队列。 |
| 32 | +3. 每次从优先队列中取出距离最小的节点 $u$。 |
| 33 | +4. 枚举 $u$ 的所有邻接节点 $v,ドル尝试用 $u$ 的当前路径更新 $v$ 的最短和次短距离: |
| 34 | + - 如果新路径长度小于 $dist1[v],ドル则将 $dist1[v]$ 的原值赋给 $dist2[v],ドル并用新路径更新 $dist1[v],ドル同时将 $v$ 及其新距离加入队列。 |
| 35 | + - 如果新路径长度介于 $dist1[v]$ 与 $dist2[v]$ 之间(即 $dist1[v] <$ 新路径 $< dist2[v]$),则用新路径更新 $dist2[v],ドル并将 $v$ 及其新距离加入队列。 |
| 36 | +5. 算法结束后,$dist2[target]$ 即为所求的次短路径长度(如果为无穷大则表示不存在)。 |
42 | 37 |
|
43 | | -##1.3 次短路径代码实现 |
| 38 | +### 2.3 扩展版 Dijkstra 的代码实现 |
44 | 39 |
|
45 | 40 | ```python
|
46 | 41 | import heapq
|
| 42 | +from collections import defaultdict |
47 | 43 |
|
48 | | -def second_shortest_path(n: int, edges: List[List[int]], start: int, end: int) -> int: |
| 44 | +def second_shortest_path(n, edges, s, t): |
49 | 45 | """
|
50 | | - 计算从起点到终点的次短路径长度 |
51 | | - |
52 | | - 参数: |
53 | | - n: 节点数量 |
54 | | - edges: 边列表,每个元素为 [起点, 终点, 权重] |
55 | | - start: 起始节点 |
56 | | - end: 目标节点 |
57 | | - |
58 | | - 返回: |
59 | | - 次短路径的长度,如果不存在则返回 -1 |
| 46 | + 求解有向/无向图中从 s 到 t 的次短路径长度(严格大于最短路径的最小路径)。 |
| 47 | + 参数说明: |
| 48 | + n: 节点数(编号 0 ~ n - 1) |
| 49 | + edges: List[(u, v, w)],每条边 (u, v, w) 表示 u 到 v 有一条权重为 w 的边 |
| 50 | + s: 起点编号 |
| 51 | + t: 终点编号 |
| 52 | + 返回: |
| 53 | + s 到 t 的次短路径长度,若不存在返回 float('inf') |
| 54 | + 注意: |
| 55 | + - 默认边权非负 |
| 56 | + - 如果为无向图,请取消 graph[v].append((u, w))的注释 |
60 | 57 | """
|
61 | 58 | # 构建邻接表
|
62 | | - graph = [[] for _ inrange(n)] |
| 59 | + graph = defaultdict(list) |
63 | 60 | for u, v, w in edges:
|
64 | 61 | graph[u].append((v, w))
|
65 | | - |
66 | | - # 初始化距离数组 |
67 | | - dist1 = [float('inf')] * n # 最短距离 |
68 | | - dist2 = [float('inf')] * n # 次短距离 |
69 | | - dist1[start] = 0 |
70 | | - |
71 | | - # 优先队列,存储 (距离, 节点) 的元组 |
72 | | - pq = [(0, start)] |
73 | | - |
| 62 | + # 如果是无向图,取消下行注释 |
| 63 | + # graph[v].append((u, w)) |
| 64 | + |
| 65 | + INF = float('inf') |
| 66 | + dist1 = [INF] * n # dist1[i]:s 到 i 的最短路径长度 |
| 67 | + dist2 = [INF] * n # dist2[i]:s 到 i 的严格次短路径长度 |
| 68 | + |
| 69 | + dist1[s] = 0 |
| 70 | + # 优先队列,元素为(当前路径长度, 节点编号) |
| 71 | + pq = [(0, s)] |
| 72 | + |
74 | 73 | while pq:
|
75 | 74 | d, u = heapq.heappop(pq)
|
76 | | - |
77 | | - # 如果当前距离大于次短距离,跳过 |
| 75 | + # 剪枝:如果当前弹出的距离已大于该点的次短路,则无需处理 |
78 | 76 | if d > dist2[u]:
|
79 | 77 | continue
|
80 | | - |
81 | | - # 遍历所有邻接节点 |
| 78 | + # 遍历 u 的所有邻居 |
82 | 79 | for v, w in graph[u]:
|
83 | | - # 计算新的距离 |
84 | | - new_dist = d + w |
85 | | - |
86 | | - # 如果找到更短的路径 |
87 | | - if new_dist < dist1[v]: |
88 | | - dist2[v] = dist1[v] # 原来的最短路径变成次短路径 |
89 | | - dist1[v] = new_dist # 更新最短路径 |
90 | | - heapq.heappush(pq, (new_dist, v)) |
91 | | - # 如果找到次短的路径 |
92 | | - elif new_dist > dist1[v] and new_dist < dist2[v]: |
93 | | - dist2[v] = new_dist |
94 | | - heapq.heappush(pq, (new_dist, v)) |
95 | | - |
96 | | - return dist2[end] if dist2[end] != float('inf') else -1 |
97 | | - |
98 | | -# 使用示例 |
99 | | -n = 4 |
100 | | -edges = [ |
101 | | - [0, 1, 1], |
102 | | - [1, 2, 2], |
103 | | - [2, 3, 1], |
104 | | - [0, 2, 4], |
105 | | - [1, 3, 5] |
106 | | -] |
107 | | -start = 0 |
108 | | -end = 3 |
109 | | - |
110 | | -result = second_shortest_path(n, edges, start, end) |
111 | | -print(f"次短路径长度: {result}") |
| 80 | + nd = d + w # 新的路径长度 |
| 81 | + # 如果找到更短的路径,更新最短和次短 |
| 82 | + if nd < dist1[v]: |
| 83 | + dist2[v] = dist1[v] |
| 84 | + dist1[v] = nd |
| 85 | + heapq.heappush(pq, (dist1[v], v)) |
| 86 | + # 如果新路径严格介于最短和次短之间,更新次短 |
| 87 | + elif dist1[v] < nd < dist2[v]: |
| 88 | + dist2[v] = nd |
| 89 | + heapq.heappush(pq, (dist2[v], v)) |
| 90 | + # 其他情况(如 nd 等于 dist1[v] 或大于等于 dist2[v])无需处理 |
| 91 | + |
| 92 | + return dist2[t] # 如果为 INF 表示不存在次短路径 |
112 | 93 | ```
|
113 | 94 |
|
114 | | -## 1.4 算法复杂度分析 |
| 95 | +### 2.4 扩展版 Dijkstra 算法分析 |
| 96 | + |
| 97 | +- **时间复杂度**:$O((V + E)\log V),ドル其中 $V$ 是节点数,$E$ 是边数。与 Dijkstra 同阶,常数略大,因为每点维护两条距离。 |
| 98 | +- **空间复杂度**:$O(V),ドル用于存储距离数组和优先队列。 |
| 99 | + |
| 100 | +## 3. 进阶与常见问题 |
| 101 | + |
| 102 | +### 3.1 无权图 / 单位权图的次短路径 |
115 | 103 |
|
116 | | -- 时间复杂度:$O((V + E)\log V),ドル其中 $V$ 是节点数,$E$ 是边数。 |
117 | | -- 空间复杂度:$O(V),ドル用于存储距离数组和优先队列。 |
| 104 | +对于无权图或所有边权均为 1ドル$ 的单位权图,求次短路径时可以采用 BFS(广度优先搜索)思想,同样维护两个距离数组:$dist1$ 表示最短路径,$dist2$ 表示严格次短路径。使用普通队列按层推进,每当遇到更短路径或介于最短和次短之间的路径时,及时更新对应的距离并将节点入队。整体实现思路与扩展版 Dijkstra 类似,只是优先队列换成了普通队列。 |
118 | 105 |
|
119 | | -##1.5 应用场景 |
| 106 | +### 3.2 与 K 短路问题的关系 |
120 | 107 |
|
121 | | -1. 网络路由:寻找备用路径。 |
122 | | -2. 交通规划:寻找替代路线。 |
123 | | -3. 通信网络:寻找备用通信路径。 |
124 | | -4. 物流配送:规划备用配送路线。 |
| 108 | +次短路径实际上是 K 短路问题在 $K = 2$ 时的特例。经典的 K 短路算法有 Yen 算法、Eppstein 算法等,但在 $K = 2$ 的场景下,直接用「扩展版 Dijkstra 维护两条距离」往往更简单高效,代码实现也更直观。 |
125 | 109 |
|
126 | | -##1.6 注意事项 |
| 110 | +### 3.3 常见易错点与细节说明 |
127 | 111 |
|
128 | | -1. 次短路径必须严格大于最短路径。 |
129 | | -2. 如果不存在次短路径,返回 $-1$。 |
130 | | -3. 图中可能存在负权边,此时需要使用 Bellman-Ford 算法的变体。 |
131 | | -4. 对于无向图,需要将每条边都加入两次。 |
| 112 | +- **严格大于最短路径**:次短路径必须严格大于最短路径。如果不存在严格大于最短路径的方案,应返回 $-1$。如果题目允许等长但不同路径作为次短路径,需根据题意调整实现。 |
| 113 | +- **松弛顺序问题**:当 `nd < dist1[v]` 时,必须先将原有的 `dist1[v]` 赋值给 `dist2[v]`,再更新 `dist1[v]`,否则会丢失正确的次短路径信息。 |
| 114 | +- **重复 / 等长路径处理**:当 `nd == dist1[v]` 时,通常不应更新 `dist2[v]`(除非题目特别说明等长但不同路径也算次短路径)。 |
| 115 | +- **边权要求**:本算法默认所有边权为非负。如果存在负权边,需使用 Bellman-Ford 算法的变体,并仔细验证实现的正确性。 |
| 116 | +- **有向图与无向图的区别**:注意区分有向图和无向图。对于无向图,构图时每条边需正反各加入一次,避免遗漏路径。 |
132 | 117 |
|
133 | 118 | ## 练习题目
|
134 | 119 |
|
135 | 120 | - [2045. 到达目的地的第二短时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/second-minimum-time-to-reach-destination.md)
|
136 | 121 |
|
137 | | -- [次短路径题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%AC%A1%E7%9F%AD%E8%B7%AF%E5%BE%84%E9%A2%98%E7%9B%AE) |
| 122 | +- [次短路径题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%AC%A1%E7%9F%AD%E8%B7%AF%E5%BE%84%E9%A2%98%E7%9B%AE) |
| 123 | + |
| 124 | + |
| 125 | + |
| 126 | + |
| 127 | + |
0 commit comments