本文最后更新于 2026年8月9日
CSDN MIGRATION · REWRITTEN
这篇笔记最初发布于 CSDN 。本次迁移保留了“松弛与正确性”的主线,重新整理了算法边界,并把伪代码改写为可运行的 Python 模板。
最短路的共同语言:松弛
若当前已知源点到 u u u 的距离为 d [ u ] d[u] d [ u ] ,边 ( u , v ) (u,v) ( u , v ) 的权重为 w ( u , v ) w(u,v) w ( u , v ) ,则一次松弛操作是:
d [ v ] ← min ( d [ v ] , d [ u ] + w ( u , v ) ) . d[v] \leftarrow \min\left(d[v], d[u]+w(u,v)\right).
d [ v ] ← min ( d [ v ] , d [ u ] + w ( u , v ) ) .
三类经典最短路算法的差异,在于按什么顺序选择节点或中间点,以及需要重复多少次松弛 。
flowchart TD
A[最短路径问题] --> B{需要所有点对吗}
B -->|是| F[Floyd]
B -->|否| C{存在负权边吗}
C -->|是| D[Bellman-Ford]
C -->|否| E[Dijkstra]
D --> G{限制边数或中转次数}
G -->|是| H[分轮松弛并保存上一轮距离]
Bellman-Ford:允许负权边
Bellman-Ford 对所有边重复执行松弛。若图中不存在从源点可达的负权环,一条最短简单路径最多包含 ∣ V ∣ − 1 |V|-1 ∣ V ∣ − 1 条边,因此经过 ∣ V ∣ − 1 |V|-1 ∣ V ∣ − 1 轮后所有最短距离都会确定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from math import infdef bellman_ford (n, edges, source ): dist = [inf] * n dist[source] = 0 for _ in range (n - 1 ): changed = False for u, v, weight in edges: if dist[u] != inf and dist[u] + weight < dist[v]: dist[v] = dist[u] + weight changed = True if not changed: break for u, v, weight in edges: if dist[u] != inf and dist[u] + weight < dist[v]: raise ValueError("negative cycle is reachable from source" ) return dist
时间复杂度是 O ( V E ) O(VE) O ( V E ) ,空间复杂度是 O ( V ) O(V) O ( V ) 。它比 Dijkstra 慢,但能处理负权边,还能通过第 V V V 轮是否继续松弛来检测负权环。
限制 K 次中转为何要复制数组
“最多经过 K K K 个中转站”意味着路径最多包含 K + 1 K+1 K + 1 条边。此时每一轮只能使用上一轮已经确定的距离,否则同一轮内连续更新会偷偷使用多条边,破坏边数约束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from math import infdef cheapest_flight (n, flights, source, target, max_stops ): dist = [inf] * n dist[source] = 0 for _ in range (max_stops + 1 ): previous = dist.copy() for u, v, price in flights: if previous[u] != inf: dist[v] = min (dist[v], previous[u] + price) return -1 if dist[target] == inf else dist[target]
这里复制数组不是实现细节,而是在显式表达动态规划状态:第 i i i 轮只允许使用不超过 i i i 条边的路径。
Dijkstra:非负权图上的贪心
Dijkstra 每次从尚未确定的节点中取出当前距离最小者。因为所有边权非负,后续经过更远节点绕回来不可能让它更短,所以该节点可以立即“收敛”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import heapqfrom math import infdef dijkstra (graph, source ): dist = [inf] * len (graph) dist[source] = 0 heap = [(0 , source)] while heap: current_dist, node = heapq.heappop(heap) if current_dist != dist[node]: continue for neighbor, weight in graph[node]: candidate = current_dist + weight if candidate < dist[neighbor]: dist[neighbor] = candidate heapq.heappush(heap, (candidate, neighbor)) return dist
使用邻接表和二叉堆时,复杂度通常写作 O ( ( V + E ) log V ) O((V+E)\log V) O (( V + E ) log V ) 。代码中的过期状态判断很重要:堆中可能同时存在同一节点的新旧距离,而 Python 的 heapq 不提供原地 decrease-key。
Floyd:所有点对最短路
Floyd-Warshall 使用动态规划。令 d ( k ) [ i ] [ j ] d^{(k)}[i][j] d ( k ) [ i ] [ j ] 表示只允许前 k k k 个节点作为中间点时,从 i i i 到 j j j 的最短距离,则:
d ( k ) [ i ] [ j ] = min ( d ( k − 1 ) [ i ] [ j ] , d ( k − 1 ) [ i ] [ k ] + d ( k − 1 ) [ k ] [ j ] ) . d^{(k)}[i][j] = \min\left(
d^{(k-1)}[i][j],
d^{(k-1)}[i][k] + d^{(k-1)}[k][j]
\right).
d ( k ) [ i ] [ j ] = min ( d ( k − 1 ) [ i ] [ j ] , d ( k − 1 ) [ i ] [ k ] + d ( k − 1 ) [ k ] [ j ] ) .
1 2 3 4 5 6 7 def floyd_warshall (dist ): n = len (dist) for k in range (n): for i in range (n): for j in range (n): dist[i][j] = min (dist[i][j], dist[i][k] + dist[k][j]) return dist
它的时间复杂度是 O ( V 3 ) O(V^3) O ( V 3 ) 、空间复杂度是 O ( V 2 ) O(V^2) O ( V 2 ) ,适合节点数较小且需要频繁查询任意点对距离的场景。
选择速查
场景
算法
关键限制
单源、边权非负
Dijkstra
负权边会破坏贪心收敛
单源、允许负权边
Bellman-Ford
需排除可达负权环
限制路径边数
分轮 Bellman-Ford
每轮必须读取上一轮状态
所有点对最短路
Floyd-Warshall
O ( V 3 ) O(V^3) O ( V 3 ) ,适合较小图
无权图最短路
BFS
每条边权可视为 1
真正需要记住的不是四份模板,而是三个判断:是否有负权、查询是单源还是多源、路径是否带边数约束。