最短路径算法:Floyd-Warshall(弗洛伊德)算法详解
Floyd-Warshall算法是解决带权图中任意两点间最短路径 的经典动态规划算法,核心优势是一次运行即可求出所有节点对的最短路径 ,同时支持检测图中的负权环(无需额外遍历)。其核心思想是:通过中间节点松弛路径 ,即对于每对节点 (i,j),判断是否存在一个中间节点 k,使得 i→k→j 的路径比当前 i→j 的路径更短。
资料:https://pan.quark.cn/s/43d906ddfa1b、https://pan.quark.cn/s/90ad8fba8347、https://pan.quark.cn/s/d9d72152d3cf
一、核心概念
-
适用条件
- 支持有向图/无向图、正权边/负权边(无向图需将边视为双向有向边)。
- 能检测负权环(若某节点到自身的最短距离小于0,则存在负权环)。
- 时间复杂度较高,适合节点数较少的图(通常V≤400)。
-
动态规划状态定义
设
dp[k][i][j]表示只允许使用前k个节点作为中间节点 时,节点i到节点j的最短路径长度。状态转移方程:
dp[k][i][j]=min(dp[k−1][i][j], dp[k−1][i][k]+dp[k−1][k][j]) dp[k][i][j] = \min(dp[k-1][i][j],\ dp[k-1][i][k] + dp[k-1][k][j]) dp[k][i][j]=min(dp[k−1][i][j], dp[k−1][i][k]+dp[k−1][k][j])空间优化:可省略
k维度,直接用二维数组dist[i][j]迭代更新,最终dist[i][j]即为i到j的最短路径。 -
负权环检测
若最终
dist[i][i] < 0,说明节点i所在的环是负权环(绕环一圈路径长度会缩短)。
二、算法步骤
-
初始化距离矩阵
- 构建二维数组
dist,dist[i][j]表示i到j的直接边权。 - 若
i == j,dist[i][j] = 0(自身到自身距离为0)。 - 若
i和j无直接边,dist[i][j] = ∞(无穷大)。
- 构建二维数组
-
中间节点松弛迭代
遍历每个中间节点
k(从0到V-1),再遍历所有起点i和终点j,执行状态转移:
dist[i][j]=min(dist[i][j], dist[i][k]+dist[k][j]) dist[i][j] = \min(dist[i][j],\ dist[i][k] + dist[k][j]) dist[i][j]=min(dist[i][j], dist[i][k]+dist[k][j]) -
负权环检测
遍历所有节点
i,若dist[i][i] < 0,则图中存在负权环。 -
路径还原(可选)
维护一个前驱矩阵
prev,prev[i][j]表示i到j的最短路径中,j的前一个节点。迭代过程中更新前驱,最终通过回溯得到具体路径。
三、完整实现(Python)
包含距离矩阵计算+负权环检测+路径还原,基于邻接矩阵存储图:
python
import sys
def floyd_warshall(graph):
"""
Floyd-Warshall算法求解任意两点间最短路径
:param graph: 邻接矩阵,graph[i][j]为i到j的边权,无边为INF,i==j为0
:return: (dist, prev, has_negative_cycle)
dist:最短距离矩阵;prev:前驱矩阵;has_negative_cycle:是否存在负权环
"""
V = len(graph)
INF = sys.maxsize
# 1. 初始化距离矩阵和前驱矩阵
dist = [row[:] for row in graph]
prev = [[-1 for _ in range(V)] for _ in range(V)]
for i in range(V):
for j in range(V):
if i != j and dist[i][j] != INF:
prev[i][j] = i # 直接边的前驱为i
# 2. 中间节点松弛迭代
for k in range(V): # k为中间节点
for i in range(V): # i为起点
for j in range(V): # j为终点
# 若i→k和k→j可达,且i→k→j更短,则更新
if dist[i][k] != INF and dist[k][j] != INF:
if dist[i][j] > dist[i][k] + dist[k][j]:
dist[i][j] = dist[i][k] + dist[k][j]
prev[i][j] = prev[k][j] # 更新前驱为k→j的前驱
# 3. 检测负权环:存在i使得dist[i][i] < 0
has_negative_cycle = any(dist[i][i] < 0 for i in range(V))
return dist, prev, has_negative_cycle
def get_path(prev, start, end):
"""
回溯前驱矩阵,生成start到end的最短路径
:param prev: 前驱矩阵
:param start: 起始节点
:param end: 目标节点
:return: 路径列表,不可达则返回空
"""
path = []
current = end
if prev[start][current] == -1 and start != end:
return [] # 无路径
# 回溯直到起始节点
while current != -1:
path.append(current)
current = prev[start][current]
# 反转路径(回溯是逆序)
return path[::-1]
# 示例调用
if __name__ == "__main__":
INF = sys.maxsize
# ========== 示例1:无负权环的图 ==========
print("=== 示例1:无负权环的图 ===")
# 邻接矩阵(节点0-3)
graph1 = [
[0, 3, INF, 7],
[INF, 0, 2, INF],
[INF, INF, 0, 1],
[INF, INF, INF, 0]
]
dist1, prev1, has_cycle1 = floyd_warshall(graph1)
print(f"是否存在负权环:{has_cycle1}")
print("任意两点间最短距离矩阵:")
for row in dist1:
print([x if x != INF else "INF" for x in row])
# 还原0→3的路径
start, end = 0, 3
path = get_path(prev1, start, end)
print(f"节点{start}到节点{end}的最短路径:{' → '.join(map(str, path))}")
# ========== 示例2:含负权环的图 ==========
print("\n=== 示例2:含负权环的图 ===")
# 节点1→2→1构成负权环(1→2:-3,2→1:1,总权重-2)
graph2 = [
[0, 1, INF, INF],
[INF, 0, -3, 2],
[INF, 1, 0, INF],
[INF, INF, INF, 0]
]
dist2, _, has_cycle2 = floyd_warshall(graph2)
print(f"是否存在负权环:{has_cycle2}")
print("距离矩阵(含负权环,对角线为负):")
for row in dist2:
print([x if x != INF else "INF" for x in row])
四、输出结果
=== 示例1:无负权环的图 ===
是否存在负权环:False
任意两点间最短距离矩阵:
[0, 3, 5, 6]
[INF, 0, 2, 3]
[INF, INF, 0, 1]
[INF, INF, INF, 0]
节点0到节点3的最短路径:0 → 1 → 2 → 3
=== 示例2:含负权环的图 ===
是否存在负权环:True
距离矩阵(含负权环,对角线为负):
[0, 1, -2, 3]
[INF, -2, -3, -1]
[INF, -1, -2, 1]
[INF, INF, INF, 0]
解释:示例2中
dist[1][1] = -2 < 0,dist[2][2] = -2 < 0,说明节点1、2所在的环是负权环。
五、Floyd-Warshall算法特性
| 特性 | 说明 |
|---|---|
| 时间复杂度 | O(V3)O(V^3)O(V3)(V为节点数),三重循环分别遍历中间节点、起点、终点 |
| 空间复杂度 | O(V2)O(V^2)O(V2)(存储距离矩阵和前驱矩阵,可优化为一维,但可读性差) |
| 负权边支持 | ✅ 支持(核心优势之一) |
| 负权环检测 | ✅ 直接通过距离矩阵对角线元素判断,无需额外操作 |
| 适用图类型 | 有向图/无向图、稠密图(节点数少的场景,V≤400) |
| 局限性 | 时间复杂度高,节点数过多时(如V>1000)效率极低 |
六、Floyd-Warshall vs 多源Dijkstra
当需要求解所有节点对的最短路径时,有两种选择:循环执行Dijkstra(每个节点作为起点)、直接执行Floyd-Warshall。两者对比如下:
| 对比维度 | Floyd-Warshall | 多源Dijkstra(堆优化) |
|---|---|---|
| 时间复杂度 | O(V3)O(V^3)O(V3) | O(V×ElogV)O(V \times E \log V)O(V×ElogV) |
| 负权边支持 | ✅ 支持 | ❌ 仅支持非负权边 |
| 负权环检测 | ✅ 内置检测 | ❌ 无法检测 |
| 适用场景 | 稠密图、节点少、含负权边 | 稀疏图、节点多、非负权边 |
| 实现难度 | 简单(三重循环) | 中等(需多次调用Dijkstra) |
七、工程优化技巧
-
无穷大取值技巧
避免使用
sys.maxsize等过大值,防止加法溢出。可设置为1e9等合理值(需大于图中最大可能路径长度)。 -
路径还原优化
若无需具体路径,可省略前驱矩阵,仅维护距离矩阵,节省 O(V2)O(V^2)O(V2) 空间。
-
稀疏图优化
对于稀疏图,优先选择多源Dijkstra,时间效率远高于Floyd-Warshall。
八、常见应用场景
- 小规模图的任意两点最短路径(如社交网络小圈子的最短好友链)。
- 带负权边的图的全局路径规划(如物流成本计算中的负奖励)。
- 图的负权环检测(如金融套利机会识别)。
- 状态压缩DP中的预处理(如状态间的转移代价计算)。
Floyd-Warshall算法的核心价值在于简单易实现、支持负权边和负权环检测、能一次性求出所有节点对路径,是小规模图全局最短路径问题的首选方案。