多源最短路与最小环(Floyd 算法图论解析)

引言

新赛季备考进入图论基础攻坚阶段,最短路是考频最高、变形最多的板块之一。最短路家族有三个核心成员: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。

二、核心考点

  1. 中转点松弛(DP 思想) :i 到 j 的最短路要么直达,要么经过某个中转点 k,即 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
  2. 循环顺序铁律:k 必须在最外层,先固定"中转站",再批量更新全图。顺序写错结果全错。
  3. 邻接矩阵初始化 :对角线 dist[i][i]=0,其余初始化为无穷大;无向图两边同时赋值;重边取最小值
  4. 最小环探测技巧 :在把 k 当作中转点"引入"之前,用尚未经过 k 的 dist[i][j] 加上直连边 g[j][k]g[k][i] 凑成一个环,遍历所有 i、j、k 取最小。
  5. 不可达处理 :用 dist[a][b] >= INF/2 判连通(避免 INF+INF 溢出后误判),不连通输出 -1。
  6. 复杂度边界:时间 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→kk→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()

五、易错点(扣分重灾区)

  1. 循环顺序写反 :k 没放最外层,中转点枚举顺序错,结果全错。务必 k → i → j
  2. INF 选太小 :用 1e9 做加法可能溢出成负数,松弛出错误结果;本题 w 可达 10⁹、路径累加易超 int,务必用 long long / Python 大整数,INF 取 1e18
  3. 忘记初始化对角线dist[i][i] 没设 0,自己到自己的距离变成 INF,环和路径都会算错。
  4. 重边没取 min:多条同向边直接覆盖,可能丢掉最短那条。
  5. 不连通判据错误 :用 dist==INF 判断,但 INF+INF 溢出后可能变小误判为连通;用 >= INF/2 更安全。
  6. 最小环顺序错 :先松弛再统计环,会导致 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 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
PiaoKe___43 分钟前
从端云解耦到可编程 Android 实例:云手机架构解析与 Python 批量管控实战
arm开发·python·架构·自动化
6Hzlia1 小时前
【Classic 150 刷题计划】 LeetCode 228. 汇总区间 | C++ 锚点游标与断点检测法
c++·算法·leetcode
计算机源码社1 小时前
基于Hadoop+Spark的商家优惠券营销效果数据分析与可视化-基于Python的商家优惠券营销效果检测与评估分析系统
大数据·hadoop·python·数据挖掘·spark·毕业设计·数据可视化
Severus_black1 小时前
【C++初阶】模板初阶——为何C++能和C语言拉开差距?
c++
Edward The Bunny1 小时前
Leetcode Hot 100
数据结构·算法
hans汉斯1 小时前
数据挖掘|基于BP神经网络的少数民族村寨文化型旅游体验产品潜在游客挖掘
深度学习·神经网络·算法·yolo·软件工程·bp·汉斯出版社
艾莉丝努力练剑3 小时前
【AI大模型接入SDK】LLM会话管理模块设计
网络·c++·人工智能·学习·大模型
小此方3 小时前
「C++AI大模型接入SDK」(一) API接入与本地两种方式对比、API Key获取、API报文详解与简单API的构建
开发语言·c++·人工智能
vicky05173 小时前
解决 ModuleNotFoundError: No module named ‘torch‘ 笔记
python