一文彻底搞懂Floyd算法

一文彻底搞懂Floyd算法(弗洛伊德算法)

1. 算法本质

Floyd算法(又称Floyd-Warshall算法)是解决所有顶点对之间最短路径的动态规划算法。
由Robert Floyd于1962年提出(基于Stephen Warshall的传递闭包思想)。
核心思想:动态规划 + 中间点松弛,逐步允许更多顶点作为中转点。


2. 适用场景与前提

  • 适用:带权有向图或无向图(边权可为负,但不能有负权环)。
  • 不适用:存在负权环的图(可检测但无法求最短路径)。
  • 目标:求任意两点 (i, j) 之间的最短路径长度。

3. 核心思想(动态规划定义)

设 dp[k][i][j] 表示从 i 到 j,只允许经过前 k 个顶点(编号 0~k-1)作为中转点的最短路径长度。

状态转移方程:

dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])
  • 不经过顶点 k:保持原路径 dp[k-1][i][j]
  • 经过顶点 k:拆分为 i→k 和 k→j 两段

空间优化:只需一个二维矩阵 dist[][],原地更新即可:

dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

4. 算法步骤(三层循环)

  1. 初始化距离矩阵 dist[][]:
    • dist[i][i] = 0
    • 若存在边 i→j,则 dist[i][j] = weight(i,j)
    • 否则 dist[i][j] = INF
  2. 三重循环:
    for k in range(n):          # 中转点
        for i in range(n):      # 起点
            for j in range(n):  # 终点
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    
  3. 检查负权环:若存在 dist[i][i] < 0,则图中有负权环。

5. 图解示例(手动推演)

图:
顶点:0, 1, 2
边:0→1(3), 0→2(8), 1→2(2), 2→1(-1)(注意负权但不构成负环)

初始距离矩阵:

    0   1   2
0 [ 0,  3,  8 ]
1 [ ∞,  0,  2 ]
2 [ ∞, -1,  0 ]

逐步更新(k=0,1,2):

中转点k 更新后 dist[1][2] 更新后 dist[2][1] 其他变化
初始 2 -1 -
k=0 min(2, ∞+∞)=2 min(-1, ∞+∞)=-1 无
k=1 min(2, 0+2)=2 min(-1, -1+0)=-1 无
k=2 min(2, -1+2)=1 min(-1, 2-1)=-1 dist[1][2] 更新为 1

最终距离矩阵:

    0   1   2
0 [ 0,  3,  5 ]   # 0→2 经过 1:3+2=5
1 [ ∞,  0,  1 ]   # 1→2 经过 2→1→2:-1+2=1
2 [ ∞, -1,  0 ]

6. 代码实现

Python 实现(基础版)

def floyd_warshall(n, edges):
    INF = float('inf')
    # 初始化距离矩阵
    dist = [[INF] * n for _ in range(n)]
    for i in range(n):
        dist[i][i] = 0
    
    # 填充边权
    for u, v, w in edges:
        dist[u][v] = w   # 若有重边可取 min
    
    # 三重循环核心
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    
    # 检测负权环
    for i in range(n):
        if dist[i][i] < 0:
            print("存在负权环!")
            return None
    
    return dist

# 示例使用
edges = [
    (0, 1, 3), (0, 2, 8),
    (1, 2, 2), (2, 1, -1)
]
result = floyd_warshall(3, edges)
for row in result:
    print(row)
# 输出:
# [0, 3, 5]
# [inf, 0, 1]
# [inf, -1, 0]

Python 实现(带路径还原)

def floyd_with_path(n, edges):
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
    nxt = [[-1] * n for _ in range(n)]
    
    for i in range(n):
        dist[i][i] = 0
        nxt[i][i] = i
    
    for u, v, w in edges:
        if w < dist[u][v]:  # 处理重边
            dist[u][v] = w
            nxt[u][v] = v
    
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
                    nxt[i][j] = nxt[i][k]   # 关键:路径经过 k
    
    # 检测负权环
    for i in range(n):
        if dist[i][i] < 0:
            print("存在负权环!")
            return None, None
    
    return dist, nxt

def print_path(nxt, u, v):
    if nxt[u][v] == -1:
        return "无路径"
    path = [str(u)]
    while u != v:
        u = nxt[u][v]
        if u == -1:
            return "路径断裂"
        path.append(str(u))
    return " -> ".join(path)

# 使用示例
dist, nxt = floyd_with_path(3, edges)
print(print_path(nxt, 0, 2))  # 0 -> 1 -> 2

C++ 实现(基础版)

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int INF = 1e9;

vector<vector<int>> floydWarshall(int n, vector<vector<int>>& edges) {
    // 初始化距离矩阵
    vector<vector<int>> dist(n, vector<int>(n, INF));
    for (int i = 0; i < n; i++) {
        dist[i][i] = 0;
    }
    
    // 填充边权
    for (auto& edge : edges) {
        int u = edge[0], v = edge[1], w = edge[2];
        dist[u][v] = min(dist[u][v], w);  // 处理重边
    }
    
    // 三重循环核心
    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                }
            }
        }
    }
    
    // 检测负权环
    for (int i = 0; i < n; i++) {
        if (dist[i][i] < 0) {
            cout << "存在负权环!" << endl;
            return {};
        }
    }
    
    return dist;
}

int main() {
    int n = 3;
    vector<vector<int>> edges = {
        {0, 1, 3}, {0, 2, 8},
        {1, 2, 2}, {2, 1, -1}
    };
    
    vector<vector<int>> result = floydWarshall(n, edges);
    
    // 输出结果
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (result[i][j] == INF)
                cout << "INF ";
            else
                cout << result[i][j] << " ";
        }
        cout << endl;
    }
    // 输出:
    // 0 3 5
    // INF 0 1
    // INF -1 0
    
    return 0;
}

C++ 实现(带路径还原)

#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
using namespace std;

const int INF = 1e9;

pair<vector<vector<int>>, vector<vector<int>>> 
floydWithPath(int n, vector<vector<int>>& edges) {
    vector<vector<int>> dist(n, vector<int>(n, INF));
    vector<vector<int>> nxt(n, vector<int>(n, -1));
    
    for (int i = 0; i < n; i++) {
        dist[i][i] = 0;
        nxt[i][i] = i;
    }
    
    for (auto& edge : edges) {
        int u = edge[0], v = edge[1], w = edge[2];
        if (w < dist[u][v]) {
            dist[u][v] = w;
            nxt[u][v] = v;
        }
    }
    
    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    nxt[i][j] = nxt[i][k];
                }
            }
        }
    }
    
    // 检测负权环
    for (int i = 0; i < n; i++) {
        if (dist[i][i] < 0) {
            cout << "存在负权环!" << endl;
            return {{}, {}};
        }
    }
    
    return {dist, nxt};
}

string getPath(vector<vector<int>>& nxt, int u, int v) {
    if (nxt[u][v] == -1) return "无路径";
    
    vector<int> path;
    path.push_back(u);
    while (u != v) {
        u = nxt[u][v];
        if (u == -1) return "路径断裂";
        path.push_back(u);
    }
    
    string result = to_string(path[0]);
    for (int i = 1; i < path.size(); i++) {
        result += " -> " + to_string(path[i]);
    }
    return result;
}

int main() {
    int n = 3;
    vector<vector<int>> edges = {
        {0, 1, 3}, {0, 2, 8},
        {1, 2, 2}, {2, 1, -1}
    };
    
    auto [dist, nxt] = floydWithPath(n, edges);
    
    cout << "0 -> 2 的最短路径: " << getPath(nxt, 0, 2) << endl;
    // 输出: 0 -> 2 的最短路径: 0 -> 1 -> 2
    
    return 0;
}

7. 复杂度分析

指标 复杂度 说明
时间复杂度 O(V³) 三重循环,V 为顶点数
空间复杂度 O(V²) 需要存储距离矩阵
优化可能性 较低 适合稠密图,V ≤ 200~300

对于稀疏图,可考虑对每个顶点运行 Dijkstra(堆优化),复杂度 O(V·(E log V))。


8. 正确性证明(简要)

  • 归纳假设:第 k 轮循环后,dist[i][j] 表示从 i 到 j,只允许经过前 k 个顶点(编号 0~k-1)的最短路径。
  • 归纳步骤:考虑第 k 轮,新路径要么不经过 k(旧值),要么经过 k(拆分为 i→k 和 k→j,均只使用前 k-1 个顶点)。
  • 由动态规划最优子结构性质,可保证最终结果正确。

9. 与Dijkstra算法的对比

特性 Floyd算法 Dijkstra算法
目标 多源(所有点对) 单源(一个起点)
算法类型 动态规划 贪心
时间复杂度 O(V³) O((V+E) log V)(堆优化)
负权边支持 ✅ 支持(无负权环) ❌ 不支持
负权环检测 ✅ 可检测 ❌ 不能
适用场景 稠密图、所有点对最短路 稀疏图、单源最短路
空间 O(V²) O(V+E)

10. 常见误区与注意事项

  • 中转点循环必须在外层:k 在外层是核心,若交换循环顺序会导致错误。
  • INF 选择:必须足够大,防止 INF + INF 溢出(C++ 中 1e9 较安全)。
  • 有向图与无向图:无向图看作两条方向相反的有向边处理。
  • 负权环检测:若最终 dist[i][i] < 0,表示存在负权环,最短路径无定义(可无限减小)。
  • 不可达点:保持 INF,表示无路径。
  • 重边处理:初始化时取 min 权重。

11. 实际应用场景

  • 交通网络:求任意两个城市之间的最短行车距离。
  • 社交网络:计算用户之间的最短「关系距离」。
  • 路由协议:OSPF 中计算全拓扑最短路径(需考虑链路状态)。
  • 传递闭包:求有向图的连通性(将边权设为 1,判断可达性)。
  • 游戏开发:计算地图中所有点对之间的最短路径。

12. 优化技巧

  1. 剪枝优化:若 dist[i][k] == INF,跳过内层循环。
  2. 对称矩阵优化:无向图只需计算 j >= i 部分。
  3. 使用 short 或 int:根据权重范围选择合适数据类型。
  4. 并行化:k 外层无法并行,但 i,j 内层可并行化。
// 剪枝优化示例(C++)
for (int k = 0; k < n; k++) {
    for (int i = 0; i < n; i++) {
        if (dist[i][k] == INF) continue;  // 剪枝
        for (int j = 0; j < n; j++) {
            if (dist[k][j] == INF) continue;  // 剪枝
            if (dist[i][k] + dist[k][j] < dist[i][j]) {
                dist[i][j] = dist[i][k] + dist[k][j];
            }
        }
    }
}

13. 总结

特点 说明
算法类型 动态规划
适用图 有向图/无向图,可含负权边(无负权环)
时间复杂度 O(V³)
空间复杂度 O(V²)
优势 实现简单、支持负权边、多源全路径
劣势 对大规模图(V>500)运行较慢

一句话记忆:

三层循环 k,i,j,每次尝试用 k 当中转点来松弛 i→j 的距离,最终得到所有点对的最短路径。

posted @ 2026-08-29 19:26  PHKLSA  阅读(13)  评论(0)    收藏  举报