Floyd-Warshall 算法(简称 Floyd 算法)是一种用于求解所有顶点对之间的最短路径的经典动态规划算法。它适用于带权有向图或无向图,可以处理正权边、负权边,但不能处理包含负权回路(负权环)的图。
一、算法目标
给定一个带权图 G=(V,E)G = (V, E)G=(V,E),其中 VVV 是顶点集合,EEE 是边集合,每条边有权重(可以为负)。Floyd 算法的目标是计算出图中任意两个顶点 iii 和 jjj 之间的最短路径长度,并可选择性地记录下路径本身。
二、核心思想:动态规划
Floyd 算法的核心思想是动态规划。它通过逐步引入“中间顶点”来更新任意两点间的最短距离。
1. 定义状态
定义 dist[i][j][k] 为:从顶点 iii 到顶点 jjj,且路径中只允许使用前 kkk 个顶点作为中间顶点时的最短路径长度。
这里的“前 kkk 个顶点”通常指顶点编号为 000 到 k−1k-1k−1(如果顶点从 0 开始编号)。
2. 状态转移方程
考虑是否使用第 kkk 个顶点(即顶点 k−1k-1k−1)作为中间点:
- 不使用顶点 k−1k-1k−1 作为中间点:那么最短路径就是
dist[i][j][k-1]。 - 使用顶点 k−1k-1k−1 作为中间点:路径可以分解为 i→k−1i \to k-1i→k−1 和 k−1→jk-1 \to jk−1→j,这两段路径也都只允许使用前 k−1k-1k−1 个顶点作为中间点,因此长度为
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][k−1], dist[i][k−1][k−1]+dist[k−1][j][k−1])
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 是当前允许使用的最大编号的中间顶点。我们按 kkk 从 000 到 n−1n-1n−1 的顺序进行迭代。
三、算法步骤
1. 初始化距离矩阵 dist:
- 对于每对顶点 (i,j)(i, j)(i,j):
- 如果 i=ji = ji=j,则
dist[i][j] = 0(到自身的距离为 0)。 - 如果存在从 iii 到 jjj 的边,权重为 www,则
dist[i][j] = w。 - 否则,
dist[i][j] = \infty(表示不可达)。
- 如果 i=ji = ji=j,则
2. 三重循环迭代:
- 外层循环 kkk:从 000 到 n−1n-1n−1,表示当前允许使用的中间顶点是 kkk。
- 中层循环 iii:从 000 到 n−1n-1n−1,表示起点。
- 内层循环 jjj:从 000 到 n−1n-1n−1,表示终点。
- 更新:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
3. (可选)路径重建:
- 为了能够输出最短路径本身,需要维护一个
next矩阵。next[i][j]存储从 iii 到 jjj 的最短路径上,iii 的下一个顶点。- 在初始化时,如果存在边 i→ji \to ji→j,则
next[i][j] = j;否则为nullptr或-1。 - 在更新
dist[i][j]时,如果通过 kkk 的路径更短,则更新next[i][j] = next[i][k]。
4. (可选)负权环检测:
- 算法结束后,检查
dist[i][i] < 0的顶点 iii。如果存在,则说明图中存在负权环(因为从 iii 到 iii 的路径长度为负,意味着存在一个负权环)。
四、 算法复杂度
- 时间复杂度: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;
}
代码说明
INF常量:使用INT_MAX / 2而不是INT_MAX,是为了在dist[i][k] + dist[k][j]计算时防止整数溢出(如果dist[i][k]或dist[k][j]是INT_MAX,相加会溢出)。addEdge函数:处理重边,只保留最小权重的边。computeShortestPaths:核心三重循环,包含溢出检查和负权环检测。next矩阵:用于路径重建。next[i][j]指向从 iii 到 jjj 的最短路径上 iii 的直接后继。getPath函数:利用next矩阵,从起点开始,一步步跳到下一个顶点,直到到达终点,构建完整路径。- 负权环检测:在算法结束后检查
dist[i][i] < 0。
七、总结
Floyd-Warshall 算法是一个优雅而强大的算法,特别适合于需要频繁查询任意两点间最短距离的场景,或者顶点数量较少的图。虽然其 O(V3)O(V^3)O(V3) 的时间复杂度在大图上不占优势,但其实现简单、逻辑清晰,是图论中最基础和重要的算法之一。
Floyd-Warshall算法详解
1484

被折叠的 条评论
为什么被折叠?



