- admin 的博客
弗洛伊德算法 (Floyd-Warshall)
- @ 2026-6-7 19:55:29
一、算法概述
弗洛伊德算法(Floyd-Warshall Algorithm)是一种用于求解任意两点间最短路径的动态规划算法。它可以在一次运行后得到图中所有节点对之间的最短距离,支持负权边(但不能有负权环)。算法由 Robert Floyd 和 Stephen Warshall 分别独立提出。
二、核心思想
Floyd-Warshall 的核心思想是动态规划 + 中转节点:
设 表示"从节点 到节点 ,且只允许经过编号为 到 的节点作为中间节点"的最短距离。
状态转移:
$$dist[k][i][j] = \min\big(dist[k-1][i][j],\ dist[k-1][i][k] + dist[k-1][k][j]\big)$$即:要么不经过 ,维持原路径;要么经过 ,用 和 的最短路径拼接起来。
可以通过滚动数组优化掉第一维,直接在 上进行原地更新:
$$dist[i][j] = \min\big(dist[i][j],\ dist[i][k] + dist[k][j]\big)$$在最外层循环枚举 即可保证正确性。
三、算法步骤/伪代码
FloydWarshall(图 G):
// 初始化距离矩阵
dist[i][j] = 边(i, j) 的权重,若无边则为 +∞
dist[i][i] = 0
// 三重循环:枚举中转节点 k
for k = 0 to V-1:
for i = 0 to V-1:
for j = 0 to V-1:
if dist[i][k] != +∞ and dist[k][j] != +∞:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
// (可选)检测负环
for i = 0 to V-1:
if dist[i][i] < 0:
存在负环
return dist
路径还原
额外维护一个 next[i][j] 矩阵记录 到 路径上的下一个节点。当 dist[i][j] 因 而被更新时,设置 next[i][j] = next[i][k]。
四、C++ 代码实现
完整可运行的 Floyd-Warshall 实现,包含距离计算、路径还原和传递闭包。
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll INF = LLONG_MAX / 2; // 防溢出
class FloydWarshall {
private:
int V;
vector<vector<ll>> dist;
vector<vector<int>> next; // 路径还原
public:
FloydWarshall(int n) : V(n),
dist(n, vector<ll>(n, INF)),
next(n, vector<int>(n, -1)) {
// 自己到自己的距离为 0
for (int i = 0; i < V; ++i) {
dist[i][i] = 0;
next[i][i] = i;
}
}
// 添加边
void addEdge(int u, int v, ll w) {
dist[u][v] = min(dist[u][v], w); // 有重边时取最小
next[u][v] = v;
}
// 执行 Floyd-Warshall 算法
// 返回值:是否存在负环
bool compute() {
// 枚举中转节点 k
for (int k = 0; k < V; ++k) {
for (int i = 0; i < V; ++i) {
if (dist[i][k] == INF) continue; // 剪枝
for (int j = 0; j < V; ++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];
next[i][j] = next[i][k]; // 路径经过 k
}
}
}
}
// 检测负环:对角线出现负数
for (int i = 0; i < V; ++i) {
if (dist[i][i] < 0) return true;
}
return false;
}
// 获取 i 到 j 的最短距离
ll getDist(int i, int j) { return dist[i][j]; }
// 还原 i 到 j 的最短路径
vector<int> getPath(int i, int j) {
vector<int> path;
if (dist[i][j] == INF) return path; // 不可达
int cur = i;
while (cur != j) {
if (cur == -1) return {}; // 异常
path.push_back(cur);
cur = next[cur][j];
}
path.push_back(j);
return path;
}
// 打印距离矩阵
void printDist() {
cout << "距离矩阵:" << endl;
for (int i = 0; i < V; ++i) {
for (int j = 0; j < V; ++j) {
if (dist[i][j] == INF) cout << "INF\t";
else cout << dist[i][j] << "\t";
}
cout << endl;
}
}
// ====== 传递闭包 (Transitive Closure) ======
// 返回 reachable[i][j]:i 是否可达 j
vector<vector<bool>> transitiveClosure() {
vector<vector<bool>> reach(V, vector<bool>(V, false));
// 初始化
for (int i = 0; i < V; ++i) {
reach[i][i] = true;
for (int j = 0; j < V; ++j) {
if (dist[i][j] != INF && i != j) {
reach[i][j] = true;
}
}
}
// Floyd 求传递闭包
for (int k = 0; k < V; ++k) {
for (int i = 0; i < V; ++i) {
for (int j = 0; j < V; ++j) {
reach[i][j] = reach[i][j] || (reach[i][k] && reach[k][j]);
}
}
}
return reach;
}
};
// ==================== 主函数 ====================
int main() {
cout << "========== Floyd-Warshall 任意两点最短路 ==========" << endl;
FloydWarshall fw(5);
// 构建示例图(5 节点 0~4)
fw.addEdge(0, 1, 3);
fw.addEdge(0, 2, 8);
fw.addEdge(0, 4, -4); // 负权边
fw.addEdge(1, 3, 1);
fw.addEdge(1, 4, 7);
fw.addEdge(2, 1, 4);
fw.addEdge(3, 0, 2);
fw.addEdge(3, 2, -5); // 负权边
fw.addEdge(4, 3, 6);
bool hasNegCycle = fw.compute();
if (hasNegCycle) {
cout << "图中存在负环!" << endl;
} else {
fw.printDist();
cout << "\n路径还原示例:" << endl;
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 5; ++j) {
if (i == j) continue;
auto path = fw.getPath(i, j);
if (!path.empty()) {
cout << i << " -> " << j << " (dist=" << fw.getDist(i, j) << "): ";
for (int k = 0; k < (int)path.size(); ++k) {
if (k > 0) cout << " -> ";
cout << path[k];
}
cout << endl;
}
}
}
}
// ====== 传递闭包演示 ======
cout << "\n========== 传递闭包 (可达性) ==========" << endl;
FloydWarshall tc(4);
tc.addEdge(0, 1, 1);
tc.addEdge(1, 2, 1);
tc.addEdge(2, 3, 1);
// 0 -> 1 -> 2 -> 3 形成链,但没有从 0 直接到 3 的边
tc.compute();
auto reach = tc.transitiveClosure();
cout << "可达性矩阵:" << endl;
cout << " ";
for (int j = 0; j < 4; ++j) cout << j << " ";
cout << endl;
for (int i = 0; i < 4; ++i) {
cout << i << " : ";
for (int j = 0; j < 4; ++j) {
cout << (reach[i][j] ? "T" : "F") << " ";
}
cout << endl;
}
// 0 可达 1,2,3(通过传递)
// 3 不可达 0(单向链)
return 0;
}
五、复杂度分析
-
时间复杂度:
- 三重循环,每重最多 次,共 次迭代
- 传递闭包(用 bitset 优化后可达 ,其中 为机器字长)
-
空间复杂度:
dist矩阵:next矩阵(路径还原):
与其他算法的对比
| 算法 | 源点数 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| Floyd-Warshall | 全源 | 较小(≤ 500),求所有点对 | |
| Dijkstra(V 次) | 非负权, 较小 | ||
| Johnson | 含负权,比 Floyd 更快(稀疏图) |
时 Floyd 是很好的选择,实现极简。
六、应用场景
| 场景 | 说明 |
|---|---|
| 任意两点最短路 | 全源最短路径的经典解法 |
| 传递闭包 | 判断有向图中任意两点之间是否存在路径 |
| 最小环 | 在 Floyd 过程中维护环的权重最小值 |
| 图的直径 | 所有点对最短距离的最大值 |
| 中心点选取 | 到所有点距离之和最小的节点 |
| 负环检测 | 检查 |
七、经典例题
例题1:传递闭包
题目描述:给定一个有向图,对每个节点对 ,判断从 是否能到达 。
解法:使用 Floyd 算法求传递闭包。用布尔矩阵 reach[i][j] 代替距离矩阵,三重循环时取 reach[i][j] = reach[i][j] || (reach[i][k] && reach[k][j])。
上述代码中的 transitiveClosure() 即为完整实现。对于稠密图,还可以用 std::bitset 优化到 。
例题2:任意两点最短路
题目描述:给定一个 个节点、 条边的有向带权图,回答 次询问,每次询问给出 ,求 到 的最短距离。
解法:直接使用 Floyd-Warshall 算法,预处理所有点对距离,然后 回答每次询问。
上述代码中的 FloydWarshall 类即为通用模板。核心步骤:
- 读入 并建图
- 调用
compute()预处理 - 对每次询问调用
getDist(a, b)即可
以下是完整模板:
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
typedef long long ll;
const ll INF = LLONG_MAX / 2;
int main() {
int n, m, q;
cin >> n >> m >> q;
vector<vector<ll>> dist(n, vector<ll>(n, INF));
for (int i = 0; i < n; ++i) dist[i][i] = 0;
for (int i = 0; i < m; ++i) {
int u, v; ll w;
cin >> u >> v >> w;
--u; --v;
dist[u][v] = min(dist[u][v], w);
}
// Floyd-Warshall
for (int k = 0; k < n; ++k)
for (int i = 0; i < n; ++i)
if (dist[i][k] != INF)
for (int j = 0; j < n; ++j)
if (dist[k][j] != INF)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
// 处理负环(对角线出现负数)
bool negCycle = false;
for (int i = 0; i < n; ++i)
if (dist[i][i] < 0) { negCycle = true; break; }
while (q--) {
int a, b;
cin >> a >> b;
--a; --b;
if (negCycle) {
cout << "存在负环" << endl;
} else if (dist[a][b] == INF) {
cout << "不可达" << endl;
} else {
cout << dist[a][b] << endl;
}
}
return 0;
}