引言
新赛季备考进入图论基础攻坚阶段,最短路是考频最高、变形最多的板块之一。最短路家族有三个核心成员:Floyd(多源/全源) 、Dijkstra(单源)、Bellman-Ford/SPFA(负权)。本篇聚焦 Floyd-Warshall ------它用动态规划思想,一次算出任意两点之间的最短距离,核心代码只有短短几行,是 n ≤ 500 小图场景的"万能备胎"。
更重要的是,Floyd 的三重循环稍加改造,还能顺手解决最小环探测 、传递闭包等高频衍生问题。下面用一道原创题,把"多源最短路 + 最小环"一次讲透。
一、题目:城际轻轨连通体检(原创题)
某都市圈规划了 n 座城市(编号 1..n),已建成 m 条双向轻轨线路。每条线路连接城市 u、v,通行时间为 w(正整数,单位:分钟)。
任务一(多源最短路) :轻轨公司收到 q 次乘客咨询,每次问"从城市 a 到城市 b 的最短通行时间是多少"。要求预处理一次,之后每次咨询 O(1) 查表回答;若两城之间不连通,输出 -1。
任务二(最小环) :安全部门要排查"环线"------找出整个线网中经过至少 3 座不同城市的最小总通行时间环(即最小权值和环)。若不存在这样的环,输出 -1。
数据范围:1 ≤ n ≤ 300,1 ≤ m ≤ n×(n-1)/2,1 ≤ w ≤ 10⁹,1 ≤ q ≤ 10⁵。
样例输入
4 5
1 2 2
2 3 1
3 1 4
2 4 5
4 3 3
3
1 4
2 4
3 1
样例输出
6
4
3
min_cycle=7
解释:1→4 最短为 1→2→3→4(2+1+3=6);2→4 最短为 2→3→4(1+3=4);3→1 最短为 3→2→1(1+2=3)。最小环是三角 1-2-3-1,总时长 2+1+4=7。
二、核心考点
- 中转点松弛(DP 思想) :i 到 j 的最短路要么直达,要么经过某个中转点 k,即
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])。 - 循环顺序铁律:k 必须在最外层,先固定"中转站",再批量更新全图。顺序写错结果全错。
- 邻接矩阵初始化 :对角线
dist[i][i]=0,其余初始化为无穷大;无向图两边同时赋值;重边取最小值。 - 最小环探测技巧 :在把 k 当作中转点"引入"之前,用尚未经过 k 的
dist[i][j]加上直连边g[j][k]、g[k][i]凑成一个环,遍历所有 i、j、k 取最小。 - 不可达处理 :用
dist[a][b] >= INF/2判连通(避免INF+INF溢出后误判),不连通输出 -1。 - 复杂度边界:时间 O(n³),空间 O(n²);n ≤ 400~500 放心用,n 上万必须换 Dijkstra/堆优化。
三、解法拆解
3.1 Floyd 主流程
用一个二维数组 dist[i][j] 表示 i 到 j 的当前最短距离。初始时:
-
dist[i][i] = 0; -
有边则
dist[u][v] = dist[v][u] = w(无向图两边同赋,重边取 min); -
其余为
INF。
然后三层循环:外层枚举中转点 k,内层枚举起点 i、终点 j,执行松弛:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
k 从 1 枚举到 n,每轮把"允许经过前 k 个中转点"的最优解逐步推进。枚举结束后,dist[i][j] 就是 i 到 j 的全局最短路。q 次咨询直接查表即可。
3.2 最小环探测(关键技巧)
最小环一定形如 i → ...... → j → k → i,其中 i...j 是不经过 k 的最短路径,j→k、k→i 是两条直连边。因此在引入 k 作为中转点之前 (也就是还没用 k 做松弛前),用当前的 dist[i][j](不含 k)拼上 g[j][k] + g[k][i],就是一个经过 k 的候选环:
for k in 1..n:
# 先统计最小环(此时 dist 还没用 k 松弛)
for i in 1..n:
for j in 1..n:
if i==j or i==k or j==k: continue
if dist[i][j] < INF and g[j][k] < INF and g[k][i] < INF:
best = min(best, dist[i][j] + g[j][k] + g[k][i])
# 再做 k 层正常松弛
for i in 1..n:
for j in 1..n:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
注意顺序:先统计环,再松弛 。这样 dist[i][j] 一定不含 k,保证拼出的环是简单环(至少 3 个互异点)。
四、参考代码
C++ 版本
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
// g 存原始直连边,dist 存最短路
vector<vector<ll>> g(n + 1, vector<ll>(n + 1, INF));
vector<vector<ll>> dist(n + 1, vector<ll>(n + 1, INF));
for (int i = 1; i <= n; i++) {
g[i][i] = 0;
dist[i][i] = 0;
}
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
// 无向图两边同赋,重边取最小
if (w < g[u][v]) { g[u][v] = w; dist[u][v] = w; }
if (w < g[v][u]) { g[v][u] = w; dist[v][u] = w; }
}
ll best = INF; // 最小环权值和
for (int k = 1; k <= n; k++) {
// 第一步:先统计经过 k 的最小环(此时 dist 尚未用 k 松弛)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j || i == k || j == k) continue;
if (dist[i][j] < INF && g[j][k] < INF && g[k][i] < INF)
best = min(best, dist[i][j] + g[j][k] + g[k][i]);
}
}
// 第二步:k 层正常松弛
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dist[i][k] < INF && dist[k][j] < INF)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
int q;
cin >> q;
while (q--) {
int a, b;
cin >> a >> b;
if (dist[a][b] >= INF / 2) cout << -1 << "\n";
else cout << dist[a][b] << "\n";
}
if (best >= INF / 2) cout << "min_cycle=-1\n";
else cout << "min_cycle=" << best << "\n";
return 0;
}
Python 版本
python
import sys
def solve() -> None:
data = sys.stdin.read().strip().split()
if not data:
return
it = iter(data)
n = int(next(it))
m = int(next(it))
INF = 10 ** 18
g = [[INF] * (n + 1) for _ in range(n + 1)]
dist = [[INF] * (n + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
g[i][i] = 0
dist[i][i] = 0
for _ in range(m):
u = int(next(it)); v = int(next(it)); w = int(next(it))
if w < g[u][v]:
g[u][v] = w; dist[u][v] = w
if w < g[v][u]:
g[v][u] = w; dist[v][u] = w
best = INF
for k in range(1, n + 1):
# 先统计经过 k 的最小环
for i in range(1, n + 1):
for j in range(1, n + 1):
if i == j or i == k or j == k:
continue
if dist[i][j] < INF and g[j][k] < INF and g[k][i] < INF:
cand = dist[i][j] + g[j][k] + g[k][i]
if cand < best:
best = cand
# 再松弛
for i in range(1, n + 1):
dik = dist[i][k]
if dik >= INF:
continue
row_i = dist[i]
row_k = dist[k]
for j in range(1, n + 1):
if row_k[j] < INF:
nd = dik + row_k[j]
if nd < row_i[j]:
row_i[j] = nd
out = []
q = int(next(it))
for _ in range(q):
a = int(next(it)); b = int(next(it))
out.append("-1" if dist[a][b] >= INF // 2 else str(dist[a][b]))
out.append("min_cycle=-1" if best >= INF // 2 else "min_cycle=" + str(best))
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
solve()
五、易错点(扣分重灾区)
- 循环顺序写反 :k 没放最外层,中转点枚举顺序错,结果全错。务必
k → i → j。 - INF 选太小 :用
1e9做加法可能溢出成负数,松弛出错误结果;本题 w 可达 10⁹、路径累加易超int,务必用long long/ Python 大整数,INF 取1e18。 - 忘记初始化对角线 :
dist[i][i]没设 0,自己到自己的距离变成 INF,环和路径都会算错。 - 重边没取 min:多条同向边直接覆盖,可能丢掉最短那条。
- 不连通判据错误 :用
dist==INF判断,但INF+INF溢出后可能变小误判为连通;用>= INF/2更安全。 - 最小环顺序错 :先松弛再统计环,会导致
dist[i][j]已经包含 k,拼出的"环"退化或重复计数;必须先统计、后松弛。
六、进阶拓展
6.1 传递闭包(可达性)
只关心"两点间是否连通",不关心距离。把 min 换成逻辑或 |,+ 换成 &:
python
# 可达性闭包:reach[i][j] 表示 i 是否能到达 j
for k in range(1, n + 1):
for i in range(1, n + 1):
for j in range(1, n + 1):
reach[i][j] = reach[i][j] or (reach[i][k] and reach[k][j])
n 较大时可用 bitset 把内层压成位运算,常数大幅降低。
6.2 负权边与负环
Floyd 天然支持负权边(只要无负环)。检测负环只需跑完 Floyd 后看对角线:dist[i][i] < 0 说明存在从 i 出发回到 i 的负权环。
6.3 路径还原
需要输出具体路径时,开一个 nxt[i][j] 数组记录 i→j 最短路上的"下一跳"。松弛成功 dist[i][j] > dist[i][k]+dist[k][j] 时令 nxt[i][j]=nxt[i][k],再从起点沿 nxt 走即可还原整条路径。
6.4 与 Dijkstra 的选型对比
| 算法 | 适用 | 复杂度 | 限制 |
|---|---|---|---|
| Floyd | 多源/全源最短路 | O(n³) | n ≤ 400~500 |
| Dijkstra | 单源最短路 | O(m log n) | 边权非负 |
| Bellman-Ford/SPFA | 单源、负权、判负环 | O(nm) | 效率不稳定 |
选题原则:点数小、要查很多点对 → Floyd 一把梭;大图、单源、边权非负 → Dijkstra 堆优化。
七、小结与互动
Floyd-Warshall 用"枚举中转点松弛"的动态规划思想,一次性解决全源最短路,并顺手拿下最小环、传递闭包、负环判定等衍生问题。记住三条命门:k 在外层、INF 要够大且用长整型、最小环先统计后松弛,竞赛小图题基本稳了。
互动时间:你在新赛季图论练习中,最常被哪类最短路题卡住?是负权环判定、路径还原,还是最小环的边界情况?欢迎在评论区留言,下一篇我们可以深入讲解"最短路树与路径还原"实战。也欢迎分享你用 Floyd 解过的最巧妙的一道题!
本文为原创算法讲解,配套练习资料见文末网盘模块,建议配合模板题(如全源最短路、最小环)动手敲一遍,加深理解。
📚 免费少儿编程资料(夸克网盘领取)
以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:
- 全国青少年信息素养大赛复赛集训题目Python&C++.docx
https://pan.quark.cn/s/93995d3cb150 - 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
https://pan.quark.cn/s/da97b5dbf75d - Python背记手册.pdf
https://pan.quark.cn/s/7568ae9ca92b - Python课程
https://pan.quark.cn/s/a94bf02d00c6 - 2024信息素养大赛图形化复赛集训题答案3-9
https://pan.quark.cn/s/6ccab7ec3cbc - 2025年03月份电子学会考级真题
https://pan.quark.cn/s/4403c4228912 - 2025全国青少年信息素养大赛赛项说明
https://pan.quark.cn/s/d9d0df4a9f29 - 青少儿信息素养大赛编程资料
https://pan.quark.cn/s/4ab6bd83be8a
资料持续更新,关注获取最新分享。