最短路径算法:Floyd-Warshall算法

最短路径算法:Floyd-Warshall(弗洛伊德)算法详解

Floyd-Warshall算法是解决带权图中任意两点间最短路径 的经典动态规划算法,核心优势是一次运行即可求出所有节点对的最短路径 ,同时支持检测图中的负权环(无需额外遍历)。其核心思想是:通过中间节点松弛路径 ,即对于每对节点 (i,j),判断是否存在一个中间节点 k,使得 i→k→j 的路径比当前 i→j 的路径更短。

资料:https://pan.quark.cn/s/43d906ddfa1bhttps://pan.quark.cn/s/90ad8fba8347https://pan.quark.cn/s/d9d72152d3cf

一、核心概念
  1. 适用条件

    • 支持有向图/无向图、正权边/负权边(无向图需将边视为双向有向边)。
    • 能检测负权环(若某节点到自身的最短距离小于0,则存在负权环)。
    • 时间复杂度较高,适合节点数较少的图(通常V≤400)。
  2. 动态规划状态定义

    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] 即为 ij 的最短路径。

  3. 负权环检测

    若最终 dist[i][i] < 0,说明节点 i 所在的环是负权环(绕环一圈路径长度会缩短)。

二、算法步骤
  1. 初始化距离矩阵

    • 构建二维数组 distdist[i][j] 表示 ij 的直接边权。
    • i == jdist[i][j] = 0(自身到自身距离为0)。
    • ij 无直接边,dist[i][j] = ∞(无穷大)。
  2. 中间节点松弛迭代

    遍历每个中间节点 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])

  3. 负权环检测

    遍历所有节点 i,若 dist[i][i] < 0,则图中存在负权环。

  4. 路径还原(可选)

    维护一个前驱矩阵 prevprev[i][j] 表示 ij 的最短路径中,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 < 0dist[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×Elog⁡V)O(V \times E \log V)O(V×ElogV)
负权边支持 ✅ 支持 ❌ 仅支持非负权边
负权环检测 ✅ 内置检测 ❌ 无法检测
适用场景 稠密图、节点少、含负权边 稀疏图、节点多、非负权边
实现难度 简单(三重循环) 中等(需多次调用Dijkstra)
七、工程优化技巧
  1. 无穷大取值技巧

    避免使用 sys.maxsize 等过大值,防止加法溢出。可设置为 1e9 等合理值(需大于图中最大可能路径长度)。

  2. 路径还原优化

    若无需具体路径,可省略前驱矩阵,仅维护距离矩阵,节省 O(V2)O(V^2)O(V2) 空间。

  3. 稀疏图优化

    对于稀疏图,优先选择多源Dijkstra,时间效率远高于Floyd-Warshall。

八、常见应用场景
  1. 小规模图的任意两点最短路径(如社交网络小圈子的最短好友链)。
  2. 带负权边的图的全局路径规划(如物流成本计算中的负奖励)。
  3. 图的负权环检测(如金融套利机会识别)。
  4. 状态压缩DP中的预处理(如状态间的转移代价计算)。

Floyd-Warshall算法的核心价值在于简单易实现、支持负权边和负权环检测、能一次性求出所有节点对路径,是小规模图全局最短路径问题的首选方案。

相关推荐
荒诞硬汉2 小时前
数组常见算法
java·数据结构·算法
少许极端2 小时前
算法奇妙屋(二十四)-二维费用的背包问题、似包非包问题、卡特兰数问题(动态规划)
算法·动态规划·卡特兰数·二维费用背包·似包非包
Z1Jxxx2 小时前
日期日期日期
开发语言·c++·算法
万行2 小时前
机器学习&第五章生成式生成器
人工智能·python·算法·机器学习
罗湖老棍子2 小时前
【模板】并查集(洛谷P3367)
算法·图论·并查集
_OP_CHEN2 小时前
【算法基础篇】(四十五)裴蜀定理与扩展欧几里得算法:从不定方程到数论万能钥匙
算法·蓝桥杯·数论·算法竞赛·裴蜀定理·扩展欧几里得算法·acm/icpc
shangjian0072 小时前
AI大模型-机器学习-算法-线性回归
人工智能·算法·机器学习
mjhcsp3 小时前
C++ KMP 算法:原理、实现与应用全解析
java·c++·算法·kmp
lizhongxuan3 小时前
Manus: 上下文工程的最佳实践
算法·架构