【图论】Floyd-Warshall 算法

Floyd-Warshall算法详解

Floyd-Warshall 算法(简称 Floyd 算法)是一种用于求解所有顶点对之间的最短路径的经典动态规划算法。它适用于带权有向图或无向图,可以处理正权边、负权边,但不能处理包含负权回路(负权环)的图

一、算法目标

给定一个带权图 G=(V,E)G = (V, E)G=(V,E),其中 VVV 是顶点集合,EEE 是边集合,每条边有权重(可以为负)。Floyd 算法的目标是计算出图中任意两个顶点 iiijjj 之间的最短路径长度,并可选择性地记录下路径本身。

二、核心思想:动态规划

Floyd 算法的核心思想是动态规划。它通过逐步引入“中间顶点”来更新任意两点间的最短距离。

1. 定义状态

定义 dist[i][j][k] 为:从顶点 iii 到顶点 jjj,且路径中只允许使用前 kkk 个顶点作为中间顶点时的最短路径长度

这里的“前 kkk 个顶点”通常指顶点编号为 000k−1k-1k1(如果顶点从 0 开始编号)。

2. 状态转移方程

考虑是否使用第 kkk 个顶点(即顶点 k−1k-1k1)作为中间点:

  • 不使用顶点 k−1k-1k1 作为中间点:那么最短路径就是 dist[i][j][k-1]
  • 使用顶点 k−1k-1k1 作为中间点:路径可以分解为 i→k−1i \to k-1ik1k−1→jk-1 \to jk1j,这两段路径也都只允许使用前 k−1k-1k1 个顶点作为中间点,因此长度为 dist[i][k-1][k-1] + dist[k-1][j][k-1]

取两者中的最小值:

dist[i][j][k]=min⁡(dist[i][j][k−1], dist[i][k−1][k−1]+dist[k−1][j][k−1]) \text{dist}[i][j][k] = \min(\text{dist}[i][j][k-1],\ \text{dist}[i][k-1][k-1] + \text{dist}[k-1][j][k-1]) dist[i][j][k]=min(dist[i][j][k1], dist[i][k1][k1]+dist[k1][j][k1])

3. 空间优化:滚动数组

注意到 dist[i][j][k] 只依赖于 dist[...][...][k-1],因此我们可以省略第三维,直接在二维数组上原地更新。即:

dist[i][j]=min⁡(dist[i][j], dist[i][k]+dist[k][j]) \text{dist}[i][j] = \min(\text{dist}[i][j],\ \text{dist}[i][k] + \text{dist}[k][j]) dist[i][j]=min(dist[i][j], dist[i][k]+dist[k][j])

这里 k 是当前允许使用的最大编号的中间顶点。我们按 kkk000n−1n-1n1 的顺序进行迭代。

三、算法步骤

1. 初始化距离矩阵 dist

  • 对于每对顶点 (i,j)(i, j)(i,j)
    • 如果 i=ji = ji=j,则 dist[i][j] = 0(到自身的距离为 0)。
    • 如果存在从 iiijjj 的边,权重为 www,则 dist[i][j] = w
    • 否则,dist[i][j] = \infty(表示不可达)。

2. 三重循环迭代

  • 外层循环 kkk:从 000n−1n-1n1,表示当前允许使用的中间顶点是 kkk
    • 中层循环 iii:从 000n−1n-1n1,表示起点。
    • 内层循环 jjj:从 000n−1n-1n1,表示终点。
    • 更新:dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

3. (可选)路径重建

  • 为了能够输出最短路径本身,需要维护一个 next 矩阵。
    • next[i][j] 存储从 iiijjj 的最短路径上,iii 的下一个顶点。
    • 在初始化时,如果存在边 i→ji \to jij,则 next[i][j] = j;否则为 nullptr-1
    • 在更新 dist[i][j] 时,如果通过 kkk 的路径更短,则更新 next[i][j] = next[i][k]

4. (可选)负权环检测

  • 算法结束后,检查 dist[i][i] < 0 的顶点 iii。如果存在,则说明图中存在负权环(因为从 iiiiii 的路径长度为负,意味着存在一个负权环)。

四、 算法复杂度

  • 时间复杂度O(V3)O(V^3)O(V3)。三重循环,每层循环 VVV 次。
  • 空间复杂度O(V2)O(V^2)O(V2)。主要存储距离矩阵 dist(和 next 矩阵)。

五、 优缺点

  • 优点
    • 代码简洁,易于实现。
    • 可以求出所有顶点对的最短路径。
    • 可以处理负权边。
  • 缺点
    • 时间复杂度较高,对于稀疏图不如 Dijkstra 或 Bellman-Ford 高效。
    • 不能处理负权环(会得到错误结果)。

六、C++ 实现

下面是一个完整的 C++ 实现,包含距离计算、路径重建和负权环检测。

#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
#include <stack>

using namespace std;

// 常量:表示无穷大(不可达)
const int INF = INT_MAX / 2; // 使用 INT_MAX/2 防止加法溢出

class FloydWarshall {
private:
    int n; // 顶点数量
    vector<vector<int>> dist; // 距离矩阵
    vector<vector<int>> next; // 路径重建矩阵:next[i][j] 表示 i 到 j 最短路径上 i 的下一个顶点
    bool hasNegativeCycle; // 标记是否存在负权环

public:
    // 构造函数
    FloydWarshall(int vertices) : n(vertices), hasNegativeCycle(false) {
        // 初始化距离矩阵
        dist.assign(n, vector<int>(n, INF));
        for (int i = 0; i < n; ++i) {
            dist[i][i] = 0; // 自身到自身的距离为 0
        }

        // 初始化 next 矩阵
        next.assign(n, vector<int>(n, -1)); // -1 表示无下一个顶点
    }

    // 添加有向边
    void addEdge(int from, int to, int weight) {
        // 注意:这里假设没有重边,如果有重边,应保留最小权重
        if (weight < dist[from][to]) {
            dist[from][to] = weight;
            next[from][to] = to; // from 到 to 的下一个顶点是 to
        }
    }

    // 执行 Floyd-Warshall 算法
    void computeShortestPaths() {
        // 三重循环:k 是中间顶点,i 是起点,j 是终点
        for (int k = 0; k < n; ++k) {
            for (int i = 0; i < n; ++i) {
                for (int j = 0; j < n; ++j) {
                    // 避免 INF 相加导致溢出
                    if (dist[i][k] < INF && dist[k][j] < INF) {
                        if (dist[i][j] > dist[i][k] + dist[k][j]) {
                            dist[i][j] = dist[i][k] + dist[k][j];
                            next[i][j] = next[i][k]; // 更新路径:i 到 j 的下一个点是 i 到 k 的下一个点
                        }
                    }
                }
            }
        }

        // 检测负权环:检查 dist[i][i] < 0
        for (int i = 0; i < n; ++i) {
            if (dist[i][i] < 0) {
                hasNegativeCycle = true;
                break;
            }
        }
    }

    // 查询从 i 到 j 的最短距离
    int getDistance(int i, int j) const {
        if (hasNegativeCycle) {
            cout << "图中存在负权环,结果无效!" << endl;
            return INF;
        }
        if (dist[i][j] == INF) {
            return INF; // 不可达
        }
        return dist[i][j];
    }

    // 获取从 i 到 j 的最短路径(顶点序列)
    vector<int> getPath(int i, int j) const {
        if (hasNegativeCycle) {
            cout << "图中存在负权环,路径无效!" << endl;
            return {};
        }
        if (dist[i][j] == INF) {
            cout << "从 " << i << " 到 " << j << " 不可达。" << endl;
            return {};
        }

        vector<int> path;
        path.push_back(i);
        // 使用 next 矩阵重建路径
        while (i != j) {
            i = next[i][j];
            if (i == -1) { // 理论上不应发生,因为已检查可达
                cout << "路径重建错误!" << endl;
                return {};
            }
            path.push_back(i);
        }
        return path;
    }

    // 打印所有顶点对的最短距离
    void printAllDistances() const {
        if (hasNegativeCycle) {
            cout << "图中存在负权环,无法打印有效距离。" << endl;
            return;
        }

        cout << "所有顶点对之间的最短距离:" << endl;
        cout << "     ";
        for (int j = 0; j < n; ++j) {
            cout << "   " << j << "   ";
        }
        cout << endl;

        for (int i = 0; i < n; ++i) {
            cout << "  " << i << " | ";
            for (int j = 0; j < n; ++j) {
                if (dist[i][j] == INF) {
                    cout << " INF  ";
                } else {
                    cout << " " << dist[i][j] << " ";
                    if (dist[i][j] < 10) cout << " "; // 对齐
                }
            }
            cout << endl;
        }
    }

    // 打印从 i 到 j 的最短路径
    void printPath(int i, int j) const {
        auto path = getPath(i, j);
        if (path.empty()) return;

        cout << "从 " << i << " 到 " << j << " 的最短路径: ";
        for (size_t idx = 0; idx < path.size(); ++idx) {
            cout << path[idx];
            if (idx < path.size() - 1) cout << " -> ";
        }
        cout << " (距离: " << getDistance(i, j) << ")" << endl;
    }

    // 检查是否存在负权环
    bool hasNegativeCycleDetected() const {
        return hasNegativeCycle;
    }
};

// 测试函数
int main() {
    // 示例 1: 一个简单的正权图
    cout << "=== 示例 1: 正权图 ===" << endl;
    FloydWarshall fw1(4);

    // 添加边 (from, to, weight)
    fw1.addEdge(0, 1, 5);
    fw1.addEdge(0, 3, 10);
    fw1.addEdge(1, 2, 3);
    fw1.addEdge(2, 3, 1);
    fw1.addEdge(3, 1, 2); // 注意这个反向边

    fw1.computeShortestPaths();

    if (fw1.hasNegativeCycleDetected()) {
        cout << "检测到负权环!" << endl;
    } else {
        fw1.printAllDistances();
        fw1.printPath(0, 3);
        fw1.printPath(0, 2);
    }

    cout << "\n" << endl;

    // 示例 2: 包含负权边但无负权环的图
    cout << "=== 示例 2: 负权边图(无负权环)===" << endl;
    FloydWarshall fw2(3);

    fw2.addEdge(0, 1, 4);
    fw2.addEdge(0, 2, 3);
    fw2.addEdge(1, 2, -2); // 负权边
    fw2.addEdge(2, 1, 1);  // 另一条边

    fw2.computeShortestPaths();

    if (fw2.hasNegativeCycleDetected()) {
        cout << "检测到负权环!" << endl;
    } else {
        fw2.printAllDistances();
        fw2.printPath(0, 1);
        fw2.printPath(0, 2);
    }

    cout << "\n" << endl;

    // 示例 3: 包含负权环的图
    cout << "=== 示例 3: 负权环图 ===" << endl;
    FloydWarshall fw3(3);

    fw3.addEdge(0, 1, 1);
    fw3.addEdge(1, 2, -3);
    fw3.addEdge(2, 0, 1); // 形成环 0->1->2->0,权重和为 1-3+1 = -1 < 0

    fw3.computeShortestPaths();

    if (fw3.hasNegativeCycleDetected()) {
        cout << "检测到负权环!" << endl;
        // 此时距离矩阵可能无效
        cout << "顶点 0 到自身的距离: " << fw3.getDistance(0, 0) << endl; // 应为负数
    } else {
        fw3.printAllDistances();
    }

    return 0;
}

代码说明

  1. INF 常量:使用 INT_MAX / 2 而不是 INT_MAX,是为了在 dist[i][k] + dist[k][j] 计算时防止整数溢出(如果 dist[i][k]dist[k][j]INT_MAX,相加会溢出)。
  2. addEdge 函数:处理重边,只保留最小权重的边。
  3. computeShortestPaths:核心三重循环,包含溢出检查和负权环检测。
  4. next 矩阵:用于路径重建。next[i][j] 指向从 iiijjj 的最短路径上 iii 的直接后继。
  5. getPath 函数:利用 next 矩阵,从起点开始,一步步跳到下一个顶点,直到到达终点,构建完整路径。
  6. 负权环检测:在算法结束后检查 dist[i][i] < 0

七、总结

Floyd-Warshall 算法是一个优雅而强大的算法,特别适合于需要频繁查询任意两点间最短距离的场景,或者顶点数量较少的图。虽然其 O(V3)O(V^3)O(V3) 的时间复杂度在大图上不占优势,但其实现简单、逻辑清晰,是图论中最基础和重要的算法之一。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值