Appearance
Floyd 算法
2026 大纲 五(四)图的基本应用 2. 最短路径 · 所有顶点对(无权图见《BFS 最短路径》,单源见《Dijkstra》)。
换一个问法:让谁当中转站
Dijkstra 的思路是"每轮确定一个离源点最近的顶点",那是贪心。Floyd 换了个完全不同的角度,它问的是:
如果只允许拿前
个顶点当中转站, 到 最短能走多远?
把这个量记作
这个定义一给出,两个端点就明确了:
:一个中转站都不许用,那就只能走直连边——它就是邻接矩阵。 :人人都可以当中转站,没有任何限制——它就是最终答案。
于是问题变成:怎么从
顶点编号从 0 开始时,初始矩阵记作
(因为 已经表示"允许经过 "了);若顶点从 1 编号,初始矩阵就记作 。编号习惯不同,上标随之平移,含义不变。
转移方程:按"经不经过 "分类
从
- 不经过
:那它的中间顶点序号其实都不超过 ,长度就是 。 - 经过
:在 处把路径切成 和 两段。最短路径上顶点不重复(无负权回路时),所以两段的中间顶点序号都不超过 ,且各自必须最短,长度为 。
两类取小者:
这是动态规划,不是贪心——这一点与 Dijkstra 是本质区别。
落成代码就是那三重循环:
c
for (int k = 0; k < V; k++) // ① 允许的中转顶点上界
for (int i = 0; i < V; i++) // ② 起点
for (int j = 0; j < V; j++) // ③ 终点
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
path[i][j] = path[k][j]; // 注意不是 = k
}先动手看一眼
盯
为什么必须在最外层
这是 Floyd 唯一真正的坑。
for i → for j → for k,就变成了"对每一格分别去试各种中转点",而试的时候别的格子还没算好。
具体反例:4 个顶点、3 条弧,构成一条编号递减的链——
错误写法 for i → for j → for k 的执行:
:试所有 —— 时 ; 时 。无更新。( 此刻还没算出来,因为 排在后面。) : 时更新 ——可惜晚了, 已经过去,本轮不回头。 : 时更新 ——同样晚了, 已经过去。
最终
而正确写法 for k → for i → for j:
⚠️ 为什么反例要造成"编号递减"的链? 若链是
这种编号递增的形状, i-j-k的扫描次序恰好顺着链的方向,误打误撞也能算对。"某些图上碰巧对"正是错误循环序最危险的地方——不构造逆序的图,你在纸上试几个例子可能永远发现不了它是错的。
另外说明:
path[i][j] = path[k][j],不是 = k
path[i][j] 存的是"
新路径是 path[k][j]。只有当 = k 就把中间顶点丢了,回溯出来的路径是断的。
初始化配套:
c
#define INF 0x3f3f3f3f // 取这个值是为了 INF+INF 不溢出 int
int dist[V][V]; // 最短距离矩阵
int path[V][V]; // 前驱矩阵:path[i][j] 是 i→j 最短路径上 j 的前一个顶点
void init(int graph[V][V]) { // 直接由邻接矩阵得到 D^(-1)
for (int i = 0; i < V; i++)
for (int j = 0; j < V; j++) {
dist[i][j] = graph[i][j]; // 无边处为 INF,对角线为 0
path[i][j] = (i != j && graph[i][j] < INF) ? i : -1;
}
}负权:能处理负权边,不能有负权回路
这是 Floyd 与 Dijkstra 的分水岭:
✅ Floyd 能处理负权边。 上面的正确性推导全程只用了"经不经过
"的分类讨论,一次都没用到"边权非负"——而 Dijkstra 的贪心恰恰卡在那一步上。
🔴 但不能有负权回路。 沿着负权回路绕一圈总权就变小,可以无限减下去,最短路径根本不存在——这不是算法不行,是问题无解。判断依据:跑完后若存在
,就说明有负权回路。
时间
和"邻接矩阵的幂"别搞混
邻接矩阵那一篇讲过
| 矩阵幂 | Floyd | |
|---|---|---|
| 内层做什么运算 | 乘法 + 加法 | 加法 + 取 |
| 结果的含义 | 路径条数 | 路径长度 |
| 下标 | 路径长度恰好为 | 允许的中转点上界 |
结构同源、语义完全不同。 问"
四顶点有向图的逐轮全表推演(想手动模拟一遍就展开)
初始邻接矩阵(
| 0 | 5 | ∞ | 7 | |
| ∞ | 0 | 4 | ∞ | |
| 3 | ∞ | 0 | 2 | |
| ∞ | ∞ | 1 | 0 |
: ,更新为 8 : ,不更新 - 其余格子的
或 为 ,均不更新
| 0 | 5 | ∞ | 7 | |
| ∞ | 0 | 4 | ∞ | |
| 3 | 8 | 0 | 2 | |
| ∞ | ∞ | 1 | 0 |
: ,更新为 9 : ,不更新 、 : ,均不更新
| 0 | 5 | 9 | 7 | |
| ∞ | 0 | 4 | ∞ | |
| 3 | 8 | 0 | 2 | |
| ∞ | ∞ | 1 | 0 |
: ,不更新 : ,不更新 : ,更新为 7 : ,更新为 6 : ,更新为 4 : ,更新为 9
| 0 | 5 | 9 | 7 | |
| 7 | 0 | 4 | 6 | |
| 3 | 8 | 0 | 2 | |
| 4 | 9 | 1 | 0 |
: ,更新为 8 - 其余格子均无改进
最终
| 0 | 5 | 8 | 7 | |
| 7 | 0 | 4 | 6 | |
| 3 | 8 | 0 | 2 | |
| 4 | 9 | 1 | 0 |
最终前驱矩阵与路径还原:
| −1 | 0 | 3 | 0 | |
| 2 | −1 | 1 | 2 | |
| 2 | 0 | −1 | 2 | |
| 2 | 0 | 3 | −1 |
读法是从终点往回倒推。以
再验一个 path[3][2] 赋来的,而 = k 立刻出错。
每还原一条路径都要验算长度,这是手算里唯一可靠的自检。
只用一个矩阵就地更新为什么安全(想弄清代码里为什么不需要两份 dist 就展开)
代码里只有一个 dist[][],第 dist[i][k]、dist[k][j] 会不会已经在本轮被改过?
不会。要改 dist[i][k] 就得满足 dist[i][k] + dist[k][k] < dist[i][k],即 dist[k][k] < 0,而这只有存在负权回路时才可能。
所以:无负权回路 ⇒ 对角线恒为 0 ⇒ 第
何时该改用 n 次 Dijkstra(要在两者之间选型时展开)
对每个顶点各跑一次朴素 Dijkstra 是
- 代码量:Floyd 只有三重循环加一个
if;Dijkstra 要维护visited与选点循环。 - 负权边:Floyd 支持,Dijkstra 不支持。
- 稀疏图:
次堆优化 Dijkstra 是 ,稀疏时优于 。
所以"全源一律用 Floyd"是不对的:稀疏图上跑
| 一组概念 | 差别 | 判别依据 |
|---|---|---|
| 支持负权边 vs 不支持负权回路 | 前者是能力,后者是问题本身无解 | 问"最短路径存不存在":负回路下不存在 |
| Floyd vs | 朴素同为 | 先问有没有负权边,再比 |
| Floyd vs Dijkstra 的思想 | 动态规划 vs 贪心 | 前者枚举中转点做分类讨论,后者每轮"确定"一个顶点 |
考点速记
三条结论:
是"允许的中转点上界",所以必须在最外层;写到内层,在编号逆序的链上会给出 。 path[i][j] = path[k][j],因为path记的是终点的前驱,不是中转点。时间、 空间;支持负权边,不支持负权回路(判断依据: )。
这一节在 408 真题里至今不单独成题——下方的「真题练习」是空的,这不是漏挂。Floyd 目前出现的位置有两处,都是"作为对照项":
- 最短路径算法的选型判断:题目要求"求每一对顶点之间的最短路径"或图中含负权边时,正确答案就是它。选型的第一问是"边权是否全相等"(是则 BFS),第二问是"单源还是全源、有没有负权"(单源非负用 Dijkstra,全源或含负权用 Floyd)。
- 与邻接矩阵幂的区分:
那道大题问的是"长度恰好为 的路径条数",与 Floyd 的三重循环形状相同但语义无关。
复习优先级上,它排在 Dijkstra 与 Prim/Kruskal 之后:把三重循环的次序、path 的更新式、以及"支持负权边不支持负权回路"这三条记住即可,不必花大量时间练手算全表。
易错:
必须在最外层。 挪到内层在有些图上碰巧还是对的,所以自己出题验证时要构造编号递减的链才能暴露问题。
易错:
path[i][j] = path[k][j],不是= k。 只有是直连边时两者才碰巧相同。
易错:Floyd 与"邻接矩阵的幂"结构像、含义不同。 一个取
算长度,一个做乘加算条数。
教材出处
- 严蔚敏《数据结构(C 语言版)》(第 2 版)印刷 p174,§6.6.2 之 2: "求解每一对顶点之间的最短路径有两种方法:其一是分别以图中的每个顶点为源点共调用
次 迪杰斯特拉算法;其二是采用下面介绍的弗洛伊德 (Floyd) 算法。 两种算法的时间复杂度均为 ,但后者形式上较简单。" 同页给出两个辅助数组: Path[i][j]为"最短路径上顶点的前一顶点的序号", D[i][j]记录与 之间的最短路径长度。 - 同书印刷 p174:算法步骤逐层描述"在
和 间加入顶点 …… 取其中较短者作为 到 的中间顶点序号不大于 的最短路径", 并给出方阵序列 与递推式 —— 本篇的状态定义与转移方程即以此为准。 - 同书印刷 p175:
ShortestPath_Floyd的完整实现, 其中更新时写的正是Path[i][j] = Path[k][j],与本篇一致; 同页并给出从Path逐级回溯读出路径的示范。
相关知识
Dijkstra 算法(单源、贪心、禁负权,与本篇的对照贯穿全文)| BFS 最短路径(无权图,三算法选型表在那一篇)| 邻接矩阵(输入输出都是矩阵;矩阵幂与本篇的区分也在那一篇)| 图的基本概念(带权路径长度、回路)| 矩阵的存储(二维数组的地址计算与遍历代价)