← 目录 / 算法文档 · 模块十五 图论基础 / 15.2 Floyd 最短路

15.2 Floyd 最短路

一次性算出图中任意两点之间的最短距离——三重循环,本质上是一个动态规划,思路和 13 模块的框架完全相通。

本页目录
① 为什么需要 Floyd:任意两点之间的最短路

14.1、14.4 节的 BFS、A* 解决的都是"从一个起点出发,到某个终点(或所有点)的最短路"——这类问题统称单源最短路。但有些场景需要知道任意两点之间的最短距离(比如"任意两个城市之间开车最快多久能到"),如果对每个点都单独跑一次单源最短路,需要跑 V 次。Floyd 算法能用一次性的三重循环,直接算出所有点对之间的最短距离,代价是要求图的规模不能太大(见 ⑤)。

② 核心思想:这其实是一个动态规划

Floyd 算法看起来只是三层 for 循环,但它的本质是一个动态规划——用的正是 13 模块讲过的那套框架。设 dp[k][i][j] 表示:只允许经过编号 1~k 的点作为中转站时,ij 的最短距离。

要素Floyd 算法里对应什么
状态定义dp[k][i][j]:只允许经过 1~k 号点中转时,i 到 j 的最短距离
转移方程dp[k][i][j] = min( dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j] )
初始化dp[0][i][j] = 原图中 i、j 之间直接的边权(没有边就是 INF),dp[0][i][i]=0
填表顺序k 从 1 到 n 依次增大(第几个中转点被"解锁")

转移方程的含义很直白:ij,允许经过 1~k 号点中转时的最短距离,要么压根不经过 k 号点(沿用 dp[k-1][i][j]),要么经过 k 号点中转(先从 ik,再从 kj,两段都只用 1~k-1 号点中转),取两者较小的一个。

💡
为什么代码里看不到三维数组?观察转移方程会发现,算 dp[k][*][*] 只需要用到 dp[k-1][*][*]——和 13.2 节背包问题"压缩成一维数组"是同样的道理,这里可以把"第几轮中转点"这一维原地滚动,直接在同一个二维数组 dist[i][j] 上更新,不需要真的开一个三维数组。这也是为什么 Floyd 算法最终代码看起来只是"三层循环 + 一行 min",背后其实是省略了一维的动态规划。
③ 图解:中转点一个个加入,矩阵逐步被更新

沿用 15.1 节的示例图(顶点 A B C D,边 A-B=4A-C=1B-C=2B-D=5C-D=8),初始的邻接矩阵(INF 表示没有直接的边):

初始矩阵(只有直接相连的边)
ABCD
A041
B4025
C1208
D580
A、D 之间没有直接的边,暂时是 (不可达)。接下来依次让 A、B、C、D 轮流当"中转点",看看能不能借道缩短某些距离。
以 B 为中转点后(k=B)
ABCD
A0419
B4025
C1207
D9570
检查"经过 B 中转"能不能缩短距离:A→D 原本是 ,经过 B 中转 A→B→D = 4+5 = 9,比 小,更新为 9C→D 原本是 8,经过 B 中转 C→B→D = 2+5 = 7,比 8 小,更新为 7
再以 C 为中转点后(k=C,最终结果)
ABCD
A0318
B3025
C1207
D8570
再检查"经过 C 中转":A→B 原本是 4,经过 C 中转 A→C→B = 1+2 = 3,更新为 3A→D 上一轮刚更新成 9,这一轮经过 C 中转 A→C→D = 1+7 = 8(这里用的 C→D=7 是上一轮 B 中转之后的最新值),比 9 更小,再次更新为 8。之后再让 A、D 当中转点检查一遍,矩阵不再变化——最终 AD 的最短距离是 8,对应路径 A→C→B→D1+2+5=8)。
📌
注意 A→D 被更新了两次:第一次借道 B 从"不可达"降到 9,第二次借道 C(这时候 C→D 已经是借道 B 优化过的 7)又降到 8。这正体现了动态规划"状态转移建立在之前已经算好的结果之上"——dp[k][i][j] 依赖的 dp[k-1][i][k]dp[k-1][k][j],本身也可能是前面几轮中转优化过的结果,中转点的效果会像滚雪球一样累积。
④ 完整代码
C++ · Floyd 最短路
1const int INF = 0x3f3f3f3f;
2int dist[MAXN][MAXN], n; // dist[i][j]:15.1 节邻接矩阵初始化后,就是 dp[0][i][j]
3
4void Floyd()
5{
6 for (int k = 1; k <= n; k++) // ★ k(中转点)必须放在最外层,见 ⑥ 陷阱
7 for (int i = 1; i <= n; i++)
8 for (int j = 1; j <= n; j++)
9 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
10}
💡
代码只有三行,但每一行对应的含义都很明确:第 6 行的 k 对应 dp 的"第几轮中转点"这一维(已经被压缩省略,直接原地滚动更新 dist);第 7、8 行的 i、j 枚举所有点对;第 9 行就是转移方程本身——dist[i][j](不经过 k)和 dist[i][k] + dist[k][j](经过 k 中转)取较小值。循环结束后,dist[i][j] 就是 ij 的最短距离。
⑤ 复杂度与适用场景

三层循环,每层都是 O(n),总时间复杂度 O(n³);空间上只需要一个 n×n 的矩阵,O(n²)

点数 nO(n³) 大致运算量是否适合用 Floyd
100约 100 万非常适合,毫秒级完成
1,000约 10 亿勉强可以,但接近超时边界
10,000约 1 万亿不适合,会严重超时
🎯
什么时候选 Floyd?点数不多(一般在几百这个量级),并且确实需要任意两点之间的最短距离时,Floyd 代码最短、最不容易写错,是最优选择。如果点数较多、但只需要"从某一个点出发到所有点"的最短距离(单源最短路),应该用 15.3 节的 Dijkstra——复杂度更低,能应对大得多的图。
⑥ 常见陷阱
三层循环的顺序写反,k 没有放在最外层:这是 Floyd 最容易犯的错误——转移方程要求"算 dp[k] 这一轮时,dp[k-1] 必须已经完全算好",也就是说必须先把 k=1 这一轮的所有 i、j 都更新完,再进入 k=2。如果把 k 放在最内层循环,会在"上一个中转点还没枚举完所有点对"的情况下就提前使用了还没更新完整的数据,算出来的最短路可能是错的(哪怕看起来数值上"差不多",某些点对的结果会不正确)。
用 0 表示"不可达",和"距离恰好是 0"混淆:和 15.1 节邻接矩阵的陷阱一样,初始化"没有边"要用足够大的 INF,不能用 0
INF + INF 可能整数溢出:第 9 行 dist[i][k] + dist[k][j],如果 i、kk、j 都不连通,两边都是 INF,相加会超过 int 能表示的范围,溢出后可能变成一个很小甚至负数的值,被误判成"更短的路径"。解决办法是把 INF 取一个"足够大,但加两次也不会溢出"的值(比如 0x3f3f3f3f 而不是 INT_MAX),这也是本节代码里 INF 取这个特定数值的原因。
图中存在负权环时,Floyd 会算出错误的负数距离:如果图里有一个总权值为负的环,理论上"最短路"可以无限绕环、无限变小,是没有意义的。Floyd 算法本身不会报错,但可以用来检测负环:算完之后检查对角线 dist[i][i],如果某个 dist[i][i] < 0,说明存在经过 i 的负权环。
🏆
接下来:Floyd 解决的是"任意两点"的最短路,代价是 O(n³)、只能应付较小的图。15.3 节的 Dijkstra 会回到"单源最短路"这个更常见的问题,用贪心 + 优先队列把复杂度降到 O((V+E) log V),能处理大得多的图。