【数据结构与算法】图 Floyd算法
相关题目:
1334. 阈值距离内邻居最少的城市 - 力扣(LeetCode)
资料 :
Floyd算法原理及公式推导 - 知乎
Floyd 算法是一种经典的动态规划算法,用与求解图中所有顶点之间的最短短路路径。它由Robert Floyd 于1962 年提出,核心思想是通过“中间顶点”逐步松弛路径长度,最终得到任意两点间的最短距离。
算法核心 :
图的类型:无向图 ,有向图(注意边的方向性)
边权特性: 支持负权边,但是不允许“包含负权边的回路”,否则会导致路径长度无线减小,无法收敛。
图的存储:
使用邻接矩阵存储,用于快速访问任意两点间的边权。
算法原理步骤
动态规划 状态定义
定义 dp[k][i][j]=表示只允许使用前k个节点作为中间节点 ,顶点 i 到顶点j 的最短距离。
根据状态转移:
不选择第k个节点 作为中间节点:
选择第k个顶点组委中间节点:路径拆分为i->k->j
.
状态转移方程为:
空间优化(关键)
观察dp[k][...] 仅依赖dp[k-1][...] ,因此无需存储所有k 层状态,可以直接用二维数组dist覆盖更新
(k 作为外层循环,每次更新dist[i]j[ 时复用之前的结果])。
实现步骤
假设 图中有n个顶点,邻接矩阵初始化为dist ,步骤如下:
初始化邻接矩阵
对顶点i, dist[i][i]=0 自身到自身的距离为0
如果顶点i和j 直接有直接的边,权重为w 则dist[i][j]=w
如果顶点i 和j 无直接边, , 表示不可达,通常用一个极大值如10^9
外层循环(枚举中间节点k)
遍历所有可能的中间顶点k(从1到n)
中间循环: 枚举起点i:
遍历所有可能的起点i (从1到n)
内侧循环:枚举终点 j
遍历所有可能的终点j , (1到 n ),执行松弛操作,
(可选)检测负权回路:
算法结束后,若存在 ,则说明图中存在包含顶点 i 的负权回路(因为自身到自身的距离不可能为负)
代码
c++
#include <iostream>
#include <vector>
#include <climits>
using namespace std;const int INF = INT_MAX / 2; // 避免加法溢出int main() {int n, m; // n: 顶点数, m: 边数cin >> n >> m;// 1. 初始化邻接矩阵vector<vector<int>> dist(n + 1, vector<int>(n + 1, INF));for (int i = 1; i <= n; ++i) {dist[i][i] = 0; // 自身到自身距离为0}// 读入边:u -> v,权值 wfor (int i = 0; i < m; ++i) {int u, v, w;cin >> u >> v >> w;dist[u][v] = w; // 若为无向图,需额外添加 dist[v][u] = w}// 2. Floyd 算法核心(k: 中间顶点,i: 起点,j: 终点)for (int k = 1; k <= n; ++k) {for (int i = 1; i <= n; ++i) {for (int j = 1; j <= n; ++j) {// 若 i->k 或 k->j 不可达,则跳过(避免 INF + INF 溢出)if (dist[i][k] != INF && dist[k][j] != INF) {dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);}}}}// 3. 输出所有顶点间的最短距离cout << "所有顶点间的最短距离:" << endl;for (int i = 1; i <= n; ++i) {for (int j = 1; j <= n; ++j) {if (dist[i][j] == INF) {cout << "INF "; // 不可达} else {cout << dist[i][j] << " ";}}cout << endl;}// 4. 检测负权回路bool has_negative_cycle = false;for (int i = 1; i <= n; ++i) {if (dist[i][i] < 0) {has_negative_cycle = true;break;}}if (has_negative_cycle) {cout << "图中存在负权回路!" << endl;}return 0;
}
python
import sys
# 避免类似 C++ 中 INT_MAX 溢出问题,使用一个足够大的数表示“不可达”
INF = float('inf')def main():# 读取输入(顶点数 n,边数 m)# 注:Python 中 input() 读取单行,需拆分字符串获取整数;若输入较大可改用 sys.stdin 提速n, m = map(int, sys.stdin.readline().split())# 1. 初始化邻接矩阵:dist[i][j] 表示顶点 i 到 j 的初始距离# 顶点编号从 1 开始(与 C++ 保持一致,避免索引偏移)dist = [[INF] * (n + 1) for _ in range(n + 1)]# 自身到自身的距离为 0for i in range(1, n + 1):dist[i][i] = 0# 读入 m 条边:u -> v,权值 w(有向边)for _ in range(m):u, v, w = map(int, sys.stdin.readline().split())# 若存在多条从 u 到 v 的边,保留权值最小的一条(与原 C++ 逻辑一致)dist[u][v] = w# 【若为无向图】需添加以下一行(无向边等价于双向有向边):# dist[v][u] = w# 2. Floyd 算法核心:三层循环枚举中间顶点、起点、终点# k:中间顶点(允许经过前 k 个顶点时更新最短路径)for k in range(1, n + 1):# i:路径起点for i in range(1, n + 1):# j:路径终点for j in range(1, n + 1):# 跳过不可达的情况(避免 INF + INF 无意义计算)if dist[i][k] != INF and dist[k][j] != INF:# 松弛操作:更新 i->j 的最短距离if dist[i][j] > dist[i][k] + dist[k][j]:dist[i][j] = dist[i][k] + dist[k][j]# 3. 输出所有顶点间的最短距离(格式与 C++ 一致)print("所有顶点间的最短距离:")for i in range(1, n + 1):row = []for j in range(1, n + 1):if dist[i][j] == INF:row.append("INF")else:row.append(str(dist[i][j]))# 用空格连接一行的元素,与 C++ 输出格式对齐print(' '.join(row))# 4. 检测负权回路:若存在顶点 i 满足 dist[i][i] < 0,说明有负权回路has_negative_cycle = Falsefor i in range(1, n + 1):if dist[i][i] < 0:has_negative_cycle = Truebreakif has_negative_cycle:print("图中存在负权回路!")if __name__ == "__main__":main()
不可达值(INF): Python 中用 float('inf') 表示 “无穷大”,比 C++ 的 INT_MAX/2 更直观,且天然避免整数溢出问题(无需担心 INF + INF 超出范围)。
邻接矩阵初始化: 用 Python 列表推导式 [[INF]*(n+1) for _ in range(n+1)] 创建 (n+1)×(n+1) 的矩阵(顶点编号从 1 开始,与 C++ 一致,减少逻辑偏移)。
输入处理: 使用 sys.stdin.readline() 替代 input(),处理大量输入时效率更高(适配题目中可能的大规模图数据);通过 map(int, ...split()) 拆分输入字符串为整数,与 C++ 的 cin >> 逻辑一致。 核心松弛操作: 保留三层循环的顺序(k→i→j),仅将 C++ 的 min() 函数替换为 Python 的条件判断(if dist[i][j] > ...),逻辑完全等价,且更符合 Python 代码习惯。
输出格式: 用列表 row 收集每行的输出内容,最后通过 ' '.join(row) 用空格连接,确保输出格式与 C++ 一致(如 “INF” 对应不可达,数字对应最短距离)。 负权回路检测: 保留原逻辑 —— 遍历所有顶点 i,若 dist[i][i] < 0,说明存在经过 i 的负权回路(因为 “自身到自身的最短路径” 不可能为负)。
示例
示例1
输入(3 个顶点,4 条有向边):
plaintext
3 4
1 2 2
1 3 6
2 3 1
3 1 -4
输出:
所有顶点间的最短距离: 0 2 3 -3 0 1 -4 -2 0 图中存在负权回路!
该示例中,dist[1][1] = 0、dist[2][2] = 0、dist[3][3] = 0 看似无负,但实际计算过程中会发现循环 1→2→3→1 的总权值为 2+1+(-4) = -1 < 0,最终代码会正确检测出负权回路。
示例2
无负权边的有向图示例
中间顶点如何优化最短路径
示例场景:4 个顶点的有向图
假设我们有一个包含 4 个顶点(编号 1~4)的有向图,边的连接和权值如下(边的方向和权重是核心):
- 1 → 2,权值 3
- 1 → 3,权值 6
- 2 → 3,权值 2
- 2 → 4,权值 5
- 3 → 4,权值 1
- 4 没有出边
第一步:明确输入格式
根据代码的输入要求(先输入顶点数 n
和边数 m
,再输入 m
条边的起点、终点、权值),该示例的输入如下:
plaintext
4 5
1 2 3
1 3 6
2 3 2
2 4 5
3 4 1
第二步:算法执行逻辑拆解(核心步骤)
我们会按 Floyd 算法的三层循环(中间顶点 k
→ 起点 i
→ 终点 j
),逐步展示邻接矩阵 dist
的更新过程,理解 “中间顶点如何缩短路径”。
1. 初始化邻接矩阵
初始时,dist[i][j]
的规则:
- 自身到自身:
dist[i][i] = 0
- 有直接边:
dist[i][j] = 边权
- 无直接边:
dist[i][j] = INF
(用∞
表示)
初始 dist
矩阵(行是起点,列是终点):
起点 \ 终点 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 0 | 3 | 6 | ∞ |
2 | ∞ | 0 | 2 | 5 |
3 | ∞ | ∞ | 0 | 1 |
4 | ∞ | ∞ | ∞ | 0 |
2. 枚举中间顶点 k=1(允许经过顶点 1 优化路径)
此时检查所有 i→j,看是否能通过 i→1→j 缩短路径。 由于顶点 1 的出边只有 1→2、1→3,且大部分 i→1 不可达(如 i=2,3,4 到 1 是 ∞),因此矩阵无更新。
3. 枚举中间顶点 k=2
(允许经过顶点 2 优化路径)
检查所有 i→j
,看是否能通过 i→2→j
缩短路径,关键更新如下:
- 对于
i=1, j=3
:原路径 1→3 权值 6;新路径 1→2→3 权值3+2=5
→ 更新dist[1][3] = 5
- 对于
i=1, j=4
:原路径 1→4 是∞
;新路径 1→2→4 权值3+5=8
→ 更新dist[1][4] = 8
更新后的 dist
矩阵:
起点 \ 终点 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 0 | 3 | 5 | 8 |
2 | ∞ | 0 | 2 | 5 |
3 | ∞ | ∞ | 0 | 1 |
4 | ∞ | ∞ | ∞ | 0 |
4. 枚举中间顶点 k=3
(允许经过顶点 3 优化路径)
检查所有 i→j
,看是否能通过 i→3→j
缩短路径,关键更新如下:
- 对于
i=1, j=4
:原路径 1→4 权值 8;新路径 1→3→4 权值5+1=6
→ 更新dist[1][4] = 6
- 对于
i=2, j=4
:原路径 2→4 权值 5;新路径 2→3→4 权值2+1=3
→ 更新dist[2][4] = 3
更新后的 dist
矩阵:
起点 \ 终点 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 0 | 3 | 5 | 6 |
2 | ∞ | 0 | 2 | 3 |
3 | ∞ | ∞ | 0 | 1 |
4 | ∞ | ∞ | ∞ | 0 |
5. 枚举中间顶点 k=4
(允许经过顶点 4 优化路径)
由于顶点 4 没有出边(所有 4→j
除了自身都是 ∞
),因此矩阵无更新。
第三步:代码输出结果
将上述输入代入 Python 代码,最终输出如下(包含所有顶点间的最短距离,且无负权回路):
plaintext
所有顶点间的最短距离:
0 3 5 6
INF 0 2 3
INF INF 0 1
INF INF INF 0
结果解读
输出矩阵的每一行代表 “从当前起点到所有终点的最短距离”:
- 起点 1 到各终点:1→1(0)、1→2(3)、1→3(5,1→2→3)、1→4(6,1→2→3→4)
- 起点 2 到各终点:2→1(不可达)、2→2(0)、2→3(2)、2→4(3,2→3→4)
- 起点 3 到各终点:3→1/2(不可达)、3→3(0)、3→4(1)
- 起点 4 到各终点:4→1/2/3(不可达)、4→4(0)
这与我们手动拆解的 “最优路径” 完全一致,验证了代码的正确性。
额外测试:含负权边(无负权回路) 若在上述示例中添加一条边 3→1,权值 -2(无负权回路),输入变为: plaintext 4 6 1 2 3 1 3 6 2 3 2 2 4 5 3 4 1 3 1 -2 代码会检测到 “无负权回路”,且更新后的 dist[2][1] 会变为 2→3→1 的权值 2 + (-2) = 0,dist[3][1] = -2 等,进一步体现 Floyd 算法对负权边的支持。