跳过并跳转到主要内容
BigO

Floyd-Warshall 算法

Floyd‑Warshall 算法是基于动态规划的全源最短路径算法,可求解图中任意两点最短距离,支持负权边,适合小规模稠密图,也能够检测图中的负权环。

基础算法,最短路径问题3分钟阅读
Floyd‑Warshall 算法扁平化科普插画,对比原始网络与处理后的路径
Floyd‑Warshall 算法扁平化科普插画,对比原始网络与处理后的路径

Floyd-Warshall 算法(简称 Floyd 算法)是一种基于动态规划的经典图算法,专门用于求解全源最短路径问题(All-Pairs Shortest Path, APSP),即一次性计算出图中任意两点之间的最短距离。

Floyd 算法的核心逻辑是:逐步扩展允许作为中转点的节点集合,尝试寻找更短的路径。

我们将图中的顶点编号为 1,2,,n1, 2, \dots, n

定义 d(k)[i][j]d^{(k)}[i][j] 表示:在允许使用前 kk 个节点 {1,2,,k}\{1, 2, \dots, k\} 作为中转点的前提下,从节点 ii 到节点 jj 的最短路径长度。

关键理解:集合 {1,2,,k}\{1, 2, \dots, k\} 是中转点的白名单候选集,并不要求路径必须经过这 kk 个节点中的每一个(甚至可以一个都不用)。

对于当前考虑的第 kk 个节点,从 iijj 的最短路径只有两种可能:

  1. 不使用节点 kk 作为中转:路径距离维持上一阶段的取值,即 d(k1)[i][j]d^{(k-1)}[i][j]
  2. 使用节点 kk 作为中转:路径被拆分为 iki \to kkjk \to j 两段,总距离为 d(k1)[i][k]+d(k1)[k][j]d^{(k-1)}[i][k] + d^{(k-1)}[k][j]

因此,转移方程为:

d(k)[i][j]=min(d(k1)[i][j],  d(k1)[i][k]+d(k1)[k][j])d^{(k)}[i][j] = \min\left(d^{(k-1)}[i][j], \; d^{(k-1)}[i][k] + d^{(k-1)}[k][j]\right)

设初始给定的 4 节点有向带权图如下:

图中包含 4 个顶点,边权分别为:121 \to 2 (8), 141 \to 4 (1), 232 \to 3 (1), 313 \to 1 (4), 424 \to 2 (2), 434 \to 3 (9)。

首先建立初始矩阵 D0D_0(即允许 00 个中转点,纯直达路径)。对角线为 00,无直接相连的边设为 \infty

D0=[0810140290]D_0 = \begin{bmatrix} 0 & 8 & \infty & 1 \\ \infty & 0 & 1 & \infty \\ 4 & \infty & 0 & \infty \\ \infty & 2 & 9 & 0 \end{bmatrix}

固定 第 1 行第 1 列,观察是否能通过节点 1 缩短路径:

  • 更新 D1[3][2]D_1[3][2]
    • 原直达距离:D0[3][2]=D_0[3][2] = \infty
    • 经由节点 1:D0[3][1]+D0[1][2]=4+8=12D_0[3][1] + D_0[1][2] = 4 + 8 = 12
    • 更新:min(,12)=12\min(\infty, 12) = \mathbf{12}
  • 更新 D1[3][4]D_1[3][4]
    • 原直达距离:D0[3][4]=D_0[3][4] = \infty
    • 经由节点 1:D0[3][1]+D0[1][4]=4+1=5D_0[3][1] + D_0[1][4] = 4 + 1 = 5
    • 更新:min(,5)=5\min(\infty, 5) = \mathbf{5}

得到矩阵 D1D_1

D1=[0810141205290]D_1 = \begin{bmatrix} 0 & 8 & \infty & 1 \\ \infty & 0 & 1 & \infty \\ 4 & \mathbf{12} & 0 & \mathbf{5} \\ \infty & 2 & 9 & 0 \end{bmatrix}

固定 第 2 行第 2 列,观察是否能通过节点 2 缩短路径:

  • 更新 D2[1][3]D_2[1][3]
    • 原距离:D1[1][3]=D_1[1][3] = \infty
    • 经由节点 2:D1[1][2]+D1[2][3]=8+1=9D_1[1][2] + D_1[2][3] = 8 + 1 = 9
    • 更新:min(,9)=9\min(\infty, 9) = \mathbf{9}

得到矩阵 D2D_2

D2=[08910141205290]D_2 = \begin{bmatrix} 0 & 8 & \mathbf{9} & 1 \\ \infty & 0 & 1 & \infty \\ 4 & 12 & 0 & 5 \\ \infty & 2 & 9 & 0 \end{bmatrix}

固定 第 3 行第 3 列,观察是否能通过节点 3 缩短路径:

  • 更新 D3[2][1]D_3[2][1]D2[2][3]+D2[3][1]=1+4=5<D_2[2][3] + D_2[3][1] = 1 + 4 = \mathbf{5} < \infty
  • 更新 D3[2][4]D_3[2][4]D2[2][3]+D2[3][4]=1+5=6<D_2[2][3] + D_2[3][4] = 1 + 5 = \mathbf{6} < \infty
  • 更新 D3[4][1]D_3[4][1]D2[4][3]+D2[3][1]=9+4=13D_2[4][3] + D_2[3][1] = 9 + 4 = 13,但由于 D2[4][2]+D2[2][1]D_2[4][2]+D_2[2][1] 进一步组合,图中此处显示更新为 7\mathbf{7}(路径 42314 \to 2 \to 3 \to 1 权重 2+1+4=72+1+4=7)。
  • 更新 D3[4][3]D_3[4][3]D2[4][2]+D2[2][3]=2+1=3<9D_2[4][2] + D_2[2][3] = 2 + 1 = \mathbf{3} < 9

得到矩阵 D3D_3

D3=[08915016412057230]D_3 = \begin{bmatrix} 0 & 8 & 9 & 1 \\ \mathbf{5} & 0 & 1 & \mathbf{6} \\ 4 & 12 & 0 & 5 \\ \mathbf{7} & 2 & \mathbf{3} & 0 \end{bmatrix}

固定 第 4 行第 4 列,完成最终计算:

  • 重点更新 D4[1][2]D_4[1][2]
    • 原距离:D3[1][2]=8D_3[1][2] = 8
    • 经由节点 4:D3[1][4]+D3[4][2]=1+2=3D_3[1][4] + D_3[4][2] = 1 + 2 = 3
    • 因为 3<83 < 8,更新 D4[1][2]=3D_4[1][2] = \mathbf{3}(即路径 1421 \to 4 \to 2)。
  • 更新 D4[3][2]D_4[3][2]
    • 原距离:D3[3][2]=12D_3[3][2] = 12
    • 经由节点 4:D3[3][4]+D3[4][2]=5+2=7D_3[3][4] + D_3[4][2] = 5 + 2 = 7
    • 因为 7<127 < 12,更新 D4[3][2]=7D_4[3][2] = \mathbf{7}(即路径 31423 \to 1 \to 4 \to 2)。

最终得到任意两点间的全源最短路径矩阵 D4D_4

D4=[0341501647057230]D_4 = \begin{bmatrix} 0 & \mathbf{3} & 4 & 1 \\ 5 & 0 & 1 & 6 \\ 4 & \mathbf{7} & 0 & 5 \\ 7 & 2 & 3 & 0 \end{bmatrix}

在实际计算中,第 kk 阶段的状态仅依赖于第 k1k-1 阶段。因此,我们可以省去状态定义中的第一维 kk,改用二维数组 d[i][j]d[i][j] 进行原地更新

d[i][j]=min(d[i][j],  d[i][k]+d[k][j])d[i][j] = \min(d[i][j], \; d[i][k] + d[k][j])

以下提供兼顾防数值溢出路径还原功能的完整 C++ 实现:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 使用 1e9 代表 INF,防止两数相加时超出 int 范围引发溢出
const int INF = 1e9;
/**
* @brief Floyd-Warshall 算法实现
* @param n 节点数量(假设节点编号为 0 到 n-1)
* @param edges 边列表,元素格式为 {u, v, weight}
*/
void floydWarshall(int n, const vector<vector<int>>& edges) {
// 1. 初始化距离矩阵与路径记录矩阵
vector<vector<int>> dist(n, vector<int>(n, INF));
vector<vector<int>> parent(n, vector<int>(n, -1));
for (int i = 0; i < n; ++i) {
dist[i][i] = 0; // 节点到自身的距离为 0
}
// 填入图的初始边权
for (const auto& edge : edges) {
int u = edge[0];
int v = edge[1];
int w = edge[2];
// 处理可能存在的重边,取权值最小值
if (w < dist[u][v]) {
dist[u][v] = w;
parent[u][v] = u; // 记录路径的前驱节点
}
}
// 2. 核心动态规划(最外层必须是中转节点 k)
for (int k = 0; k < n; ++k) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
// 安全检查:只有当 i->k 和 k->j 均可达时才更新,双重保险防溢出
if (dist[i][k] != INF && dist[k][j] != INF) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
parent[i][j] = parent[k][j]; // 更新路径前驱
}
}
}
}
}
// 3. 检查负权环
for (int i = 0; i < n; ++i) {
if (dist[i][i] < 0) {
cout << "警告:图中存在负权环!最短路径无意义。" << endl;
return;
}
}
// 4. 打印结果示例:点对 (0, n-1) 的距离与路径
int src = 0, dst = n - 1;
if (dist[src][dst] == INF) {
cout << "节点 " << src << " 到节点 " << dst << " 不可达。" << endl;
} else {
cout << "节点 " << src << " 到节点 " << dst << " 的最短距离为: " << dist[src][dst] << endl;
// 还原具体路径
vector<int> path;
for (int curr = dst; curr != -1; curr = parent[src][curr]) {
path.push_back(curr);
if (curr == src) break;
}
reverse(path.begin(), path.end());
cout << "最短路径路线: ";
for (size_t i = 0; i < path.size(); ++i) {
cout << path[i] << (i + 1 == path.size() ? "" : " -> ");
}
cout << endl;
}
}
int main() {
int n = 4; // 节点个数 (0, 1, 2, 3)
// 边结构:{起点, 终点, 权重}
vector<vector<int>> edges = {
{0, 1, 5},
{0, 3, 10},
{1, 2, 3},
{2, 3, 1}
};
floydWarshall(n, edges);
return 0;
}

特性 说明
时间复杂度 O(V3)\mathcal{O}(V^3),三层嵌套循环,其中 VV 为顶点数量。
空间复杂度 O(V2)\mathcal{O}(V^2),仅需要一个二维矩阵存储邻接矩阵及最短距离。
负权边支持 支持包含负权边的图(比 Dijkstra 算法更具通用性)。
负权环检测 算法执行结束后,若存在 d[i][i]<0d[i][i] < 0,说明图中包含负权环(无最短路解)。
适用场景 顶点数较少(通常 V500V \le 500)的稠密图,或需要计算所有点对距离的场景。

© 2026 五五开 · 一方通行,只管向前。

旅途由 Astro 驱动 · 主题 Chirping Astro