CSP-S 提高级课程中心 / 算法进阶 / 第34讲
CSP-S 提高级 · 算法进阶 · s5-12

单源次短路与 Floyd-Warshall

三行循环解决"任意两点最短路",用动态规划的眼光看懂 Floyd;再往前一步,学习"第二好的路"怎么求 —— 次短路是提高组最经典的"最短路变体"。

1 个大纲知识点 Floyd O(n³) 任意两点 次短路 dis1 / dis2 双数组 传递闭包 bitset 优化 12 道洛谷原题
本讲 · 第 34/52 讲单源次短路与Floyd-Warshall
模块 01

情景导航

从"全城地图"和"备用路线"两个场景出发,认识"任意两点最短路"与"次短路"这两件不同的事。

故事全城地图与备用路线

某市交通管理局要为一款导航 App 提供数据支持。工程师小陈拿到的是一张 n 个路口、m 条道路的城市地图,每条路有通行时间。他的任务有两项:

任务一(全城地图):回答"任意两个路口之间的最短通行时间"。用户可能随时查询任意一对起点和终点,总查询次数可能高达上万次。如果每来一个查询就跑一次 Dijkstra,就算 Dijkstra 再快也扛不住 —— 需要的是一张一次算好、永久可用的"全城最短距离表"。这就是 Floyd-Warshall 算法:三重循环,O(n³) 时间,把所有点对的距离一次性算清,之后每次查询都变成 O(1) 的查表。

任务二(备用路线):对某些关键路口,导航需要准备一条"次优路线" —— 当主路线上某条路临时封路时,能立刻给出"第二短的走法"。小陈这才发现,很多题目问的不是"最短是多少",而是"比最短长一点、但尽可能短的那条路是多少",这就是 次短路问题。

小陈在实现次短路时踩了个坑:他本想"先算最短路,然后把最短路上的某条边删掉再跑一次最短路",这样确实能求出与最短路不完全重叠的一条路径,但代码要跑很多次最短路,很慢。后来老张教他一个更优雅的做法:给每个点同时维护两个距离 —— dis1 表示最短距离、dis2 表示次短距离,在 Dijkstra 的松弛语句里同时更新这两个值。一次 Dijkstra 就把两张表全算出来了。

Floyd 管"所有点对",次短路管"第二好" —— 两件事,两套工具。

💡 故事背后的算法思想

Floyd 的本质是动态规划:用 f[k][i][j] 表示"只允许经过前 k 个点作为中转时,i 到 j 的最短距离"。它的状态转移只有两句话:要么不用第 k 个点中转(沿用 f[k−1][i][j]),要么用第 k 个点中转(f[k−1][i][k] + f[k−1][k][j])。取最小值即可。

次短路的本质是"多维护一维最优值":最短路问题里每个点只记"最优值",次短路问题里每个点记"最优值 + 次优值"。松弛时不但要考虑"能否刷新最优",还要考虑"能否刷新次优"。这种"多维护几个候选值"的思想,在"第 K 短路"、"带去除条件的方案"等题目里反复出现。

两者的适用边界:Floyd 只适合 n ≤ 500 左右的小图(n³ 很快超时),但可以处理负权边和判负环;次短路必须保证边权非负(否则"次短"可能不存在)。

①生活类比
🗺️

一本全城距离手册

一次印刷,随时翻查任何两地之间的距离 —— 预处理好,查询变成查表。

→ Floyd 的 O(1) 查询
🔁

中转站可以随便换

"经由 A 中转" 还是 "经由 B 中转",两种方案取更短的那个。

→ f[k][i][j] 的转移
🚧

主路封了走备选

平时走最快的路,一旦封路立刻启用"第二快"的那条。

→ 次短路
🥈

考试只能填一个答案

先记下最高分,再看"比它低的最高分"是多少。

→ dis1 / dis2 双数组
②核心定义

Floyd-Warshall(多源最短路 APSP):用三重循环求出图中任意两点之间的最短距离。时间复杂度 O(n³),空间 O(n²),n 一般在 500 以内。可以处理负权边(不能有负环)。

中转点枚举:Floyd 的外层循环变量 k 表示"允许作为中转的点"的下标。循环结束后 d[i][j] 就是 i 到 j 的最短距离。

传递闭包(Transitive Closure):只关心"i 能否到达 j"(0/1),同样用 Floyd 的三重循环求,把加法和取最小值换成逻辑或 —— 这就是"传递闭包",常用于判断有向图的连通性与偏序关系。

严格次短路:从 s 到 t 的所有路径中,长度严格大于最短路的那些路径里,长度最小的那个值。若不存在(只有一条路),则次短路为无穷大。

非严格次短路:允许"与最短路长度相等"的路径参与竞争,即"第二短的长度值"(可能等于最短路长度,如果存在两条长度相同的最短路)。做题时必须先看清题目要哪一种!

📌 本讲核心知识地图

板块核心内容对应大纲
Floyd 主体dp[k][i][j] 状态定义、三重循环、二维滚动【6】多源最短路
循环顺序k 必须在最外层;O(n³) 复杂度分析【6】Floyd 正确性
初始化d[i][i]=0、无穷大用 0x3f3f3f3f 或 1e18、重边取最小【5】图的基本存储
负环判定d[i][i] < 0 即有负环【6】负环
传递闭包01 矩阵、bitset 优化、路径存在性【5】图的连通性
次短路严格 / 非严格;dis1 + dis2 双数组【6】最短路变体
枚举删边删掉最短路上的边再跑 Dijkstra【6】最短路变体
③核心口诀
🔑 Floyd 三行

for (k) for (i) for (j) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
中转点 k 必须放最外层,否则含义就错了!

🔑 初始化三件事

① d[i][i] = 0;② 其余全设无穷大;③ 读边时用 d[u][v] = min(d[u][v], w) 处理重边。

🔑 判负环

Floyd 跑完后若存在 d[i][i] < 0,说明有负环(i 能经负环回到自己且更短)。

🔑 次短路

每个点多开一个数组:dis1 存最短、dis2 存次短。松弛时先看能不能刷新 dis1,再看能不能刷新 dis2,一次 Dijkstra 全部搞定。

模块 02

知识探究

先把 Floyd 当作一道动态规划来推,看清三重循环的每一层是什么意思;再学习"次短路"这个经典的变体问题。

1Floyd-Warshall:三行循环解决所有点对 核心算法

🔍 从"中转点"这个直觉出发

问:"从 i 到 j 的最短路,会经过哪些点?"答案可能是"什么都不经过,直连",也可能"先到某个点 k,再从 k 到 j"。

Floyd 的想法极其朴素:把中转点一个一个地开放。一开始只允许"直连";然后允许"经过 1 号点中转";再允许"经过 2 号点中转"……直到所有点都可以当中转。

状态定义:d[i][j] 表示"当前允许的中转点集合下,i 到 j 的最短距离"。每开放一个新中转点 k,就尝试用 k 把 i 和 j 拼起来:

d[i][j] = min( d[i][j] , d[i][k] + d[k][j] );

为什么这一行就够了?因为开放 k 之后,i→j 的最短路有两种可能:① 完全不经过 k(那就是旧的 d[i][j]);② 经过 k(那时可以拆成 i→k 和 k→j 两段,而这两段都只用"前 k−1 个中转点",正好就是当前的 d[i][k] 和 d[k][j])。

为什么不用写三维数组?标准的完全背包式优化:d[i][k] 和 d[k][j] 在"用 k 中转"的这一步里不会被改坏(因为 d[i][k] 已经不可能靠"再走一次 k"变得更短,除非有负环),所以可以直接在原地更新,二维滚动即可,空间降到 O(n²)。

C++Floyd 核心三行(背下来)
for (int k = 1; k <= n; k++)          // 中转点(必须最外层)
    for (int i = 1; i <= n; i++)      // 起点
        for (int j = 1; j <= n; j++)  // 终点
            if (d[i][k] + d[k][j] < d[i][j])
                d[i][j] = d[i][k] + d[k][j];

用一个 4 个点的小图走一遍。无向图,边为 1-2:3、1-3:8、2-3:2、2-4:5、3-4:1:

初始距离矩阵(∞ 表示暂时不可达)
—1234
1038∞
23025
38201
4∞510
d[1][1]=0、d[1][2]=3、d[2][3]=2、d[3][4]=1 …其余为 ∞(对称)
▶ Floyd 逐步开放中转点(4 点示例)
0初始化:d[i][i] = 0,直接相连的边填权值,其余为 ∞。此时只允许"直连"。
1k = 1(允许经 1 号点中转):检查所有 (i,j),没有任何组合能通过 1 变得更短 → 矩阵不变。
2k = 2(允许经 1、2 号点中转):d[1][3] 从 8 改为 d[1][2]+d[2][3] = 3+2 = 5;d[1][4] 从 ∞ 改为 3+5 = 8。
3k = 3(允许经 1、2、3 中转):d[1][4] 从 8 改为 d[1][3]+d[3][4] = 5+1 = 6;d[2][4] 从 5 改为 d[2][3]+d[3][4] = 2+1 = 3。
4k = 4(允许经全部 4 个点中转):再检查一遍,没有更优解 → 矩阵不变,算法结束。
5最终全部点对最短距离:d[1][2]=3、d[1][3]=5、d[1][4]=6(1-2-3-4)、d[2][3]=2、d[2][4]=3(2-3-4)、d[3][4]=1。
要点:每开放一个中转点 k,就把"所有点对"都试着用 k 拼一次。i、j 的循环顺序无所谓,但 k 必须最外层。
项目取值说明
时间复杂度O(n³)n = 200 → 8×106,轻松;n = 1000 → 109,超时
空间复杂度O(n²)用 long long d[505][505] 约 2 MB
能否负权✅ 可以但不能有负环
能否判负环✅ 可以跑完后若 d[i][i] < 0 即有负环
适用场景小图 + 多次任意点对查询n ≤ 500 且查询次数多
不适用场景n 很大、单源查询此时用 Dijkstra O(m log n)
💡 记忆技巧:Floyd 就是"枚举中转站"。把它想成"机场换乘"—— k 是"新开的换乘城市",每开一个,就刷新所有城市对之间的最快路线。
2为什么 k 必须在最外层? 必考细节

🔍 两种写法的本质差别

写法 A(正确):for k → for i → for j。含义是"先只允许经 1 中转,把整个矩阵更新一遍;再允许经 1、2 中转,再更新一遍……"

写法 B(错误):for i → for j → for k。含义变成"对每一对 (i,j),逐个尝试各个中转点 k",但此时 d[i][k] 和 d[k][j] 还没有被"更小的中转点集合"充分优化过 —— 相当于只允许一次中转(i→k→j 中间不能再拐弯),自然求不出需要两次以上中转的最短路。

动态规划的解释:Floyd 的状态是"阶段 k"(允许使用的中转点集合大小)。DP 要求按阶段递推,即"第 k 阶段的状态只能由第 k−1 阶段推出"。所以 k 必须是外层循环 —— 这是"阶段",i、j 只是"状态维度"。这和背包问题里"物品在外层、容量在内层"是同一个道理。

结论:i、j、k 三个循环中,只有 k 的位置是硬性要求(必须最外层),i 和 j 谁在前面都无所谓(对称图更是完全一样)。

❌ 错误写法(k 在最内层)

for (i) for (j) for (k)
d[i][j] = min(d[i][j], d[i][k]+d[k][j]);

只能求出"最多一次中转"的路径,长链式最短路会算错。

✅ 正确写法(k 在最外层)

for (k) for (i) for (j)
d[i][j] = min(d[i][j], d[i][k]+d[k][j]);

按"允许的中转点集合"分阶段递推,结果正确。

📐 记牢:k 是阶段,i、j 是状态;阶段必须在最外层 —— 这是 DP 的铁律。
3初始化的三个坑与负环判定

🔧 初始化三件事(缺一不可)

① 对角线置 0:d[i][i] = 0。含义是"自己到自己距离为 0"。这一步是后面 d[i][k] + d[k][j] 能正常工作的基础(例如 i 就是 k 的情况)。

② 其余全设无穷大:常用 0x3f3f3f3f(约 1.06×109)或 1e18。用 0x3f 的好处是可以 memset(d, 0x3f, sizeof d) 一次性搞定,且两个 INF 相加不会溢出 int。若答案可能很大,请用 long long + 1e18。

③ 读入时处理重边:必须写 d[u][v] = min(d[u][v], w),不能写 d[u][v] = w!题目里可能有两条连接同样两点、权值不同的边,后读入的会覆盖掉更优的。

④ 无向图要对称赋值:d[u][v] = d[v][u] = min(..., w)。

C++标准初始化 + 负环判定
const int MAXN = 505;
const long long INF = 1e18;
long long d[MAXN][MAXN];

// ① 全部置 INF,对角线置 0
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++)
        d[i][j] = (i == j) ? 0 : INF;

// ② 读边(无向图示例):重边取最小
for (int i = 1; i <= m; i++) {
    int u, v; long long w;
    cin >> u >> v >> w;
    if (w < d[u][v]) { d[u][v] = w; d[v][u] = w; }   // 有向图去掉 d[v][u]
}

// ③ Floyd 主体
for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++) {
        if (d[i][k] == INF) continue;              // 小剪枝,也防溢出
        for (int j = 1; j <= n; j++) {
            long long nd = d[i][k] + d[k][j];
            if (nd < d[i][j]) d[i][j] = nd;
        }
    }

// ④ 负环判定
bool negCycle = false;
for (int i = 1; i <= n; i++)
    if (d[i][i] < 0) negCycle = true;

🔍 为什么 d[i][i] < 0 就说明有负环?

对角线一开始是 0(就是"原地不动")。如果 i 能沿着一条环回到自己,且这条环的总权为负,那么 Floyd 就会把 d[i][i] 更新成这个负值。反之,如果所有环权都 ≥ 0,那么"从 i 出发绕一圈回到 i"永远不会优于"原地不动",对角线就一直是 0。

推论:如果图中某处有负环,那么"能走到这个负环的点"的对角线也会变成负值(因为可以先走到环上、绕圈、再回来)。所以扫一遍对角线就能发现负环。

⚠️ 三个必查项:对角线 0、其余 INF、重边取 min。这三条错任何一条,Floyd 都会给出难以察觉的错误答案。
4传递闭包:把"最短路"退化成"能不能到" 高频变体

🔍 从距离到可达性

有些问题根本不关心"多远",只关心"能不能到达"。例如:"a 能打败 b、b 能打败 c,那么 a 一定能打败 c 吗?"这就是可达性问题。

把 Floyd 里的 d[i][j] 换成 0/1 矩阵 r[i][j](1 表示 i 能到达 j),转移也从"加法 + 取最小值"换成"逻辑与 + 逻辑或":

r[i][j] |= r[i][k] && r[k][j];

初始化:r[i][i] = 1(自己一定能到自己);有边 u→v 则 r[u][v] = 1。

结果:循环结束后 r[i][j] = 1 当且仅当 i 能沿有向边走到 j(允许经过任意多个中间点)。

1
1
1
1
0
1
1
1
0
0
1
1
0
0
0
1

有向链 1→2→3→4 的传递闭包矩阵(行 = 起点,列 = 终点):1 能到所有点,4 只能到自己

C++传递闭包 · 普通写法与 bitset 优化
/* ===== 写法一:普通三重循环 O(n³) ===== */
bool r[MAXN][MAXN];
for (int i = 1; i <= n; i++) r[i][i] = true;    // 自己到自己
for (int i = 1; i <= m; i++) { int u, v; cin >> u >> v; r[u][v] = true; }

for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++)
        if (r[i][k])
            for (int j = 1; j <= n; j++)
                if (r[k][j]) r[i][j] = true;

/* ===== 写法二:bitset 优化 O(n³ / 64) ===== */
bitset<MAXN> g[MAXN];        // g[i][j] = 1 表示 i 可达 j
for (int i = 1; i <= n; i++) g[i][i] = 1;
for (int i = 1; i <= m; i++) { int u, v; cin >> u >> v; g[u][v] = 1; }

for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++)
        if (g[i][k]) g[i] |= g[k];        // 一次位运算处理 n 个 j
💡 bitset 优化把内层 j 循环压成"一次 64 位的按位或",实际速度提升约 64 倍。n = 1000 时普通写法约 109 次操作会超时,而 bitset 版只要约 1.6×107 次机器字运算,轻松通过。这是提高组非常常用的常数优化技巧。
问题用什么初始化
任意两点最短距离Floyd(加法 + min)d[i][i]=0,重边取 min
任意两点可达性传递闭包(and + or)r[i][i]=1
任意两点可达方案数计数版 Floyd(乘法 + 加法)r[i][i]=1,边 r[u][v]=1
任意两点最小瓶颈变体 Floyd(max + min)d[i][i]=0,边权直接填
任意两点最长路(DAG)拓扑排序 + DP(下一讲)不能用 Floyd,会成环
🔑 一句话

Floyd 的循环骨架是"万能模具":把 min/+ 换成别的运算,就得到一类新算法 —— 可达性(or/and)、计数(+/×)、瓶颈路(min/max)。

5Floyd 适用场景与"加点增量"技巧
✅ 该用 Floyd 的场景
  • n ≤ 500,m 可能很大(接近 n²)
  • 需要回答很多次任意两点的最短路查询
  • 需要求任意两点的可达性 / 计数 / 最小环
  • 图中有负权边但没有负环
  • 需要"逐步加点"的动态连通性
❌ 不该用 Floyd 的场景
  • n > 1000(O(n³) 必超时)
  • 只需要"单源"最短路 → 用 Dijkstra
  • 有负环 → 最短路无定义,只能判环
  • 稀疏图 + 需要多次单源 → 反复 Dijkstra 更快

🏙️ 关键技巧:"加点增量"式 Floyd(灾后重建模型)

有一类题目的特点是:图中的点会按时间顺序陆续"启用"。例如"某些村庄在某时刻才修好,之前不能作为中转"。这时如果每次询问都重跑 Floyd 就太慢了。

做法:因为 Floyd 的外层循环 k 恰好就是"一个一个开放中转点",所以我们只需要 维护一个指针,每当询问时间的上限提高,就把"新修好的点"作为 k 执行一次外层循环。指针只前进、不后退,所以总的 Floyd 工作量仍然是 O(n³)(每对 (i,j) 每个 k 只被处理一次),而所有询问都能在线回答。

为什么正确:Floyd 的 k 循环本身就有"前缀"语义 —— "只允许 1..k 号点作为中转"。把"按时间启用"和"按编号允许"对齐,就完美契合了。

注意:这要求"启用顺序"是固定的,可以用一次排序 + 指针解决;如果启用顺序是任意的,则不能这么做。

🏗️ 灾后重建三类要点:按时间排序点 → 指针前移 → 每前移一次做一轮 k 循环 → 查询时先判"两点是否都已修好"。
6次短路:第二好的那条路 易混概念

🔍 先分清"严格"与"非严格"

严格次短路:在所有路径中,长度严格大于最短路的那些路径里,长度最小的值。若从 s 到 t 只有一条路径,则不存在严格次短路(记为 ∞)。

非严格次短路:不考虑"严格大于",只问"第二短的长度值"。如果存在两条长度相同的最短路,则非严格次短路 = 最短路长度。

怎么区分?看题目原话:"严格大于最短路"、"second shortest longer than the shortest" → 严格;只说"第二短"、"不允许与原最短路完全相同" → 通常是另一种语义。做题前必须把这一句圈出来。

两者的取值关系:设严格次短路为 WS,非严格次短路为 WN,则一定有 WN ≤ WS;当且仅当存在两条等长的最短路时取小于号。

另一个相关概念:"与原最短路不共用某些边的最短路"(例如"封掉某条路后的最短路"),这个话题常常和次短路被放在一起考,但它是第三件事 —— 请求不同,结果也可能不同。

概念定义举例(两条长度 2 的最短路)
最短路最小的路径长度2
严格次短路严格大于最短路的最小长度3(若存在长度 3 的路)
非严格次短路第二短的路径长度值(可等于最短路)2
删边后的最短路禁止使用某条边后的最短路看删的是哪条边
📌 读题三板斧:是求"长度"还是求"路径"?是否要求"严格"?是否允许与最短路重合?
7方法一:Dijkstra 双数组(dis1 + dis2) 推荐

🔍 核心思路:给每个点维护两个值

dis1[u]:从 s 到 u 的最短距离;dis2[u]:从 s 到 u 的次短距离(严格大于 dis1[u])。

关键洞察:从 s 到 v 的次短路,一定是从某个点 u 的一条"到 u 的最短路"或"到 u 的次短路"再走一条边 u→v 得到的。所以松弛时要考虑四种组合:

① 用 dis1[u] + w 去尝试更新 dis1[v];
② 用 dis1[u] + w 去尝试更新 dis2[v];
③ 用 dis2[u] + w 去尝试更新 dis2[v](注意:dis2[u] + w 永远不可能小于 dis1[v]… 其实可能!所以要统一判断)。

统一的写法更清爽:把堆里存的元素设计成 (距离, 点, 类型),类型 = 0 表示"这条候选是某个点的最短路",类型 = 1 表示"次短路"。弹出时如果这个距离已经不是对应的 dis1/dis2(过期),就跳过;否则用它去对每条出边做两种尝试:

· 若 nd < dis1[v]:原来的最短路被"降级"成次短路(dis2[v] = dis1[v]),然后 dis1[v] = nd;两个状态都要入堆。

· 否则若 nd > dis1[v] 且 nd < dis2[v]:只更新 dis2[v] = nd,把 (nd, v, 1) 入堆。

复杂度:每个点最多入堆 2 次,所以总复杂度仍是 O(m log n) —— 和普通 Dijkstra 同阶!这就是它比"枚举删边"优雅的地方。

① 初始化dis1[s]=0,其余全 ∞
② 入堆(0, s, 0)
③ 弹出校验是否过期
④ 刷新 dis1旧值降级给 dis2
⑤ 刷新 dis2落在中间的候选
⑥ 输出dis1[t] 与 dis2[t]

次短路的两个候选来源:要么"某个点的最短路再接一条边",要么"某个点的次短路再接一条边"

C++Dijkstra 双数组求严格次短路(核心框架)
const long long INF = 1e18;
long long dis1[MAXN], dis2[MAXN];       // dis1:最短;dis2:严格次短

typedef pair<int, int> PII;                  // (点, 类型)
typedef pair<long long, PII> PLII;           // (距离, (点, 类型))

void dijkstra2(int s, int n) {
    for (int i = 1; i <= n; i++) { dis1[i] = INF; dis2[i] = INF; }
    dis1[s] = 0;

    priority_queue<PLII, vector<PLII>, greater<PLII>> pq;
    pq.push({0, {s, 0}});                  // 类型 0 = 最短路候选

    while (!pq.empty()) {
        PLII cur = pq.top(); pq.pop();
        long long d = cur.first;
        int u = cur.second.first, t = cur.second.second;

        // 过期副本:与当前记录的 dis 不一致
        if (t == 0 && d != dis1[u]) continue;
        if (t == 1 && d != dis2[u]) continue;

        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].to;
            long long nd = d + e[i].w;

            if (nd < dis1[v]) {                    // ① 刷新最短路
                dis2[v] = dis1[v];               //    旧的最短路降级为次短路
                dis1[v] = nd;
                pq.push({nd, {v, 0}});
                if (dis2[v] < INF) pq.push({dis2[v], {v, 1}});
            } else if (nd > dis1[v] && nd < dis2[v]) {   // ② 刷新次短路
                dis2[v] = nd;
                pq.push({nd, {v, 1}});
            }
        }
    }
}
💡 想让程序求非严格次短路,把条件 nd > dis1[v] 改成 nd >= dis1[v] 即可 —— 这样"与最短路等长的另一条路径"也能成为次短路候选。

🔍 手算演示(严格次短路)

有向图:1→2:2、2→4:2、1→3:1、3→4:4、1→4:6,求 1 到 4 的严格次短路。

① 弹出 1(0):dis1[2] = 2、dis1[3] = 1、dis1[4] = 6。

② 弹出 3(1):3→4 得 nd = 5 < dis1[4] = 6 → dis2[4] = 6、dis1[4] = 5。

③ 弹出 2(2):2→4 得 nd = 4 < dis1[4] = 5 → dis2[4] = 5、dis1[4] = 4。

④ 弹出 4(4):无出边。此时 dis1[4] = 4(路径 1-2-4),dis2[4] = 5(路径 1-3-4)。

答案:最短路 4,严格次短路 5。

8方法二:枚举删边法(思路直观,但有坑)

✂️ 算法步骤

第 1 步:跑一次 Dijkstra,记录最短路路径(用 pre[] 回溯)。

第 2 步:枚举这条路径上的每一条边 e,把它"删掉"(跳过它)再跑一次 Dijkstra,得到 dis[t]。

第 3 步:取所有结果的最小值,就是答案。

复杂度:路径最多 n−1 条边,每次 Dijkstra 是 O(m log n),总复杂度 O(n m log n)。当 n = 105 时显然会超时;只有 n ≤ 1000 左右才勉强可用。

它求的到底是什么?删掉最短路 P 上的某条边 e 后,新最短路一定不使用 e。所以"取最小值"得到的是"与原最短路不共用 e 的路径中最短的"。若图中存在另一条与 P 等长的路径,则答案会等于 P 的长度 → 这是非严格意义上的结果。

结论:枚举删边法不能保证求出严格次短路。要严格,必须在得到候选值后判断它是否 > 最短路长度,或者直接用 dis1/dis2 双数组法。

C++枚举删边法(示意)
// 第一次 Dijkstra,记录前驱(顺便记录"到达 v 用的是哪条边")
dijkstra(s, t, 0);                // ban = 0 表示不删任何边
long long best = INF;

// 顺着 pre 回溯出最短路上的每条边
vector<int> pathEdge;             // 存边在 e[] 中的下标
for (int x = t; x != s; x = e[preEdge[x]].from)
    pathEdge.push_back(preEdge[x]);

// 枚举删掉路径上的一条边,重跑 Dijkstra
for (int id : pathEdge) {
    dijkstra(s, t, id);           // 过程中遇到下标为 id 的边就跳过
    if (dis[t] < best) best = dis[t];
}
cout << best << endl;

💡 两种方法怎么选?

只用一次、需要严格次短路 → dis1/dis2 双数组法(O(m log n),代码稍长但一次搞定)。

题目问的是"去掉某条边后的最短路"(如"封路") → 枚举删边法才是对的语义,它天生就在回答这个问题。

n 很小(≤ 200)而你需要"第 K 短路" → 可以考虑 A* / 可持久化左偏树(超纲,仅作了解)。

9方法对比与选型
方法复杂度能求严格次短路能求"删边后最短路"适用场景
dis1/dis2 双数组O(m log n)✅ 能❌ 不能次短路首选
枚举删边O(n m log n)⚠️ 不保证✅ 能"封路"类问题、n 很小
最短路 + 次短路 FloydO(n³)✅ 能❌ 不能多点对、n ≤ 300
A* / K 短路视实现✅ 能(K=2)❌ 不能第 K 短路(提高+)
题目关键词选什么理由
"任意两点最短路,n ≤ 500,q 次询问"Floyd预处理 O(n³),查询 O(1)
"i 能否到达 j"传递闭包(bitset)Floyd 变体
"严格次短路"dis1/dis2一次 Dijkstra 完成
"封掉一条边后的最短路"枚举删边语义正好匹配
"按时间陆续启用的点"增量 Floydk 循环天然支持"前缀"
"n ≤ 1000 且要任意两点最短路"两次 Dijkstra?再想想n³ = 109 超时,考虑 bitset 或换思路
案例案例1:B3647【模板】Floyd · 任意两点最短路 模板题

题目:给定一张 n 个点、m 条边的无向图(可能有重边、自环,边权为正),再给出 q 次询问,每次询问 u 到 v 的最短距离。

🔍 详细分析过程

第1步:识别模型 —— 多源最短路(APSP),一次预处理、多次查询 → Floyd 的标准场景。

第2步:复杂度核算 —— 若 n ≤ 500,n³ = 1.25×108,在 1 秒内可接受;若每个查询都跑 Dijkstra,q 很大时反而更慢。

第3步:初始化 —— 对角线置 0,其余置 INF,读边时用 min 处理重边。

第4步:Floyd 主体 —— k 在最外层。加一句 if (d[i][k] == INF) continue; 防溢出并加速。

第5步:查询 —— 若 d[u][v] == INF 输出 −1(按题目要求),否则输出 d[u][v]。

易错:自环(u == v)在无向图中读入时要跳过或取 min(自己到自己是 0,不可能更大);d[i][i] 保持 0。

C++B3647 · Floyd 完整代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 505;
const long long INF = 1e18;
long long d[MAXN][MAXN];

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int n, m, q;
    cin >> n >> m >> q;

    // 初始化
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            d[i][j] = (i == j) ? 0 : INF;

    // 读边(无向图,重边取最小)
    for (int i = 1; i <= m; i++) {
        int u, v; long long w;
        cin >> u >> v >> w;
        if (w < d[u][v]) { d[u][v] = w; d[v][u] = w; }
    }

    // Floyd 主体:k 必须最外层
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++) {
            if (d[i][k] == INF) continue;
            for (int j = 1; j <= n; j++) {
                long long nd = d[i][k] + d[k][j];
                if (nd < d[i][j]) d[i][j] = nd;
            }
        }

    // 回答询问
    while (q--) {
        int u, v; cin >> u >> v;
        cout << (d[u][v] == INF ? -1 : d[u][v]) << '\n';
    }
    return 0;
}
运行结果
输入: 4 5 3
      1 2 3
      1 3 8
      2 3 2
      2 4 5
      3 4 1
      1 4
      2 4
      1 2
输出: 6
      3
      3

(1→4 走 1-2-3-4 = 3+2+1 = 6;
  2→4 走 2-3-4 = 2+1 = 3;
  1→2 直连 = 3)

💡 想一想

如果这道题改成"只需要 1 号点到其他所有点的距离",n = 500、m = 5000,用 Floyd 还合适吗?

Floyd 是 O(n³) = 1.25×108;而一次堆优化 Dijkstra 只要 O(m log n) ≈ 5000×9 = 4.5×104,快了三个数量级。单源问题永远优先 Dijkstra,Floyd 只在"要多点对答案"时才划算。

案例案例2:次短路 · Dijkstra 双数组完整实现 核心题型

题目:给定 n 个点、m 条边的有向带权图(边权为正),求从 s 出发到每个点的最短路与严格次短路。

🔍 详细分析过程

第1步:状态设计 —— 每个点有"最短"和"次短"两个状态,总共 2n 个状态。堆里同时存放这两种状态。

第2步:堆元素编码 —— 用 pair<long long, pair<int,int>> 表示 (距离, (点, 类型))。默认 greater 会先比较距离,距离相同时比较点编号、再比较类型,符合"小根堆"的要求。

第3步:过期判定 —— 因为同一个状态可能被多次入堆,弹出时必须判断 d 是否等于对应的 dis1[u] / dis2[u],不等就跳过。

第4步:两种松弛分支 —— nd < dis1[v](刷新最短,旧值降级)和 dis1[v] < nd < dis2[v](刷新次短)。注意 nd == dis1[v] 的情况不处理,这是"严格"的要求。

易错:① 刷新 dis1 时务必把 旧的最短路赋给 dis2(dis2[v] = dis1[v]),这个"降级"操作是最容易漏的一步;② 降级后要把新 dis2 也入堆,否则它会永远停在数据里没机会传播出去。

C++次短路 · 完整代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005, MAXM = 200005;
const long long INF = 1e18;

struct Edge { int to, nxt; long long w; } e[MAXM];
int head[MAXN], cntE = 0;

void add(int u, int v, long long w) {
    e[++cntE] = {v, head[u], w};
    head[u] = cntE;
}

int n, m, s;
long long dis1[MAXN], dis2[MAXN];

typedef pair<int, int> PII;
typedef pair<long long, PII> PLII;

void dijkstra2() {
    for (int i = 1; i <= n; i++) { dis1[i] = INF; dis2[i] = INF; }
    dis1[s] = 0;

    priority_queue<PLII, vector<PLII>, greater<PLII>> pq;
    pq.push({0, {s, 0}});

    while (!pq.empty()) {
        PLII cur = pq.top(); pq.pop();
        long long d = cur.first;
        int u = cur.second.first, t = cur.second.second;

        if (t == 0 && d != dis1[u]) continue;   // 过期副本
        if (t == 1 && d != dis2[u]) continue;

        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].to;
            long long nd = d + e[i].w;

            if (nd < dis1[v]) {                       // ① 刷新最短路
                dis2[v] = dis1[v];                  //    旧最短 → 次短(降级)
                dis1[v] = nd;
                pq.push({nd, {v, 0}});
                if (dis2[v] < INF) pq.push({dis2[v], {v, 1}});
            } else if (nd > dis1[v] && nd < dis2[v]) {  // ② 刷新次短路
                dis2[v] = nd;
                pq.push({nd, {v, 1}});
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n >> m >> s;
    for (int i = 1; i <= m; i++) {
        int u, v; long long w;
        cin >> u >> v >> w;
        add(u, v, w);
    }
    dijkstra2();

    for (int i = 1; i <= n; i++) {
        cout << i << ": ";
        if (dis1[i] == INF) cout << "不可达" << endl;
        else {
            cout << "最短路=" << dis1[i];
            if (dis2[i] == INF) cout << "  次短路=无" << endl;
            else cout << "  次短路=" << dis2[i] << endl;
        }
    }
    return 0;
}
运行结果(严格次短路)
输入: 4 5 1
      1 2 2
      2 4 2
      1 3 1
      3 4 4
      1 4 6
输出: 1: 最短路=0  次短路=无
      2: 最短路=2  次短路=无
      3: 最短路=1  次短路=无
      4: 最短路=4  次短路=5

(到 4 的最短路是 1-2-4 = 2+2 = 4;
  严格次短路是 1-3-4 = 1+4 = 5,比 6(直连 1-4)更短)
运行结果(存在两条等长最短路时)
输入: 4 5 1
      1 2 1
      2 4 1
      1 3 1
      3 4 1
      1 4 3
输出: 4: 最短路=2  次短路=3

(1-2-4 与 1-3-4 都是长度 2,所以【严格】次短路要跳到 3;
  若把条件改成 nd >= dis1[v],输出的次短路就会变成 2)
案例案例3:P1119 灾后重建 · 增量 Floyd 经典应用

题目:有 n 个村庄(编号 0..n−1),第 i 个村庄在第 t[i] 个时刻修好。给出 m 条双向道路。有 q 次询问,第 k 次询问在时刻 T 时从村庄 x 到村庄 y 的最短路长度(只能经过已修好的村庄)。若不可达输出 −1。

🔍 详细分析过程

第1步:为什么不能对每个询问重跑算法 —— n ≤ 200,q ≤ 5×104。每次跑 Floyd 是 8×106,乘 q 直接爆炸。

第2步:关键观察 —— 题目保证修好时间是递增的(t[0] ≤ t[1] ≤ … ≤ t[n−1]),而且询问的 T 也不一定有序(但可以离线排序,或用指针处理)。

第3步:与 Floyd 的 k 循环对应 —— Floyd 的 k 循环含义正是"只允许编号 1..k 的点当中转"。所以只要把"村庄编号"与"修好时间"对齐,就能用一个单调前进的指针维护"当前已启用的中转点集合"。

第4步:处理询问 —— 对每个询问先把指针推到"所有 t[i] ≤ T 的 i 都启用",然后:① 若 x 或 y 尚未修好(t[x] > T 或 t[y] > T)→ 输出 −1;② 若 d[x][y] 仍为 INF → 输出 −1;③ 否则输出 d[x][y]。

第5步:总复杂度 —— 指针总共前进 n 次,每次做一轮 O(n²) 的 k 循环 → 总 O(n³) 预处理;每个询问 O(1),全部询问 O(q)。比"每次重跑"快了 q 倍。

易错:① 别忘了先检查"两端点是否都已修好";② 若询问的 T 不是递增的,要先把询问按 T 离线排序(否则指针不能只前进);③ 修好时间为 0 的村庄(t[i] = 0)在任何 T ≥ 0 的询问中都已启用。

C++P1119 灾后重建 · 完整代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 205;
const long long INF = 1e18;

int t[MAXN];                 // 每个村庄的修好时刻
long long d[MAXN][MAXN];

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> t[i];

    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            d[i][j] = (i == j) ? 0 : INF;

    for (int i = 0; i < m; i++) {
        int u, v; long long w;
        cin >> u >> v >> w;
        if (w < d[u][v]) { d[u][v] = w; d[v][u] = w; }
    }

    int q; cin >> q;
    int cur = 0;                 // 已启用的村庄个数(前缀)

    while (q--) {
        int x, y, T;
        cin >> x >> y >> T;

        // 把所有 t[i] <= T 的村庄作为中转点"开放"
        while (cur < n && t[cur] <= T) {
            int k = cur;
            for (int i = 0; i < n; i++) {
                if (d[i][k] == INF) continue;
                for (int j = 0; j < n; j++) {
                    long long nd = d[i][k] + d[k][j];
                    if (nd < d[i][j]) d[i][j] = nd;
                }
            }
            cur++;
        }

        if (t[x] > T || t[y] > T) cout << -1 << '\n';
        else if (d[x][y] == INF) cout << -1 << '\n';
        else cout << d[x][y] << '\n';
    }
    return 0;
}
运行结果
输入: 4 5
      1 3 5 7
      0 1 4
      0 2 3
      1 2 1
      1 3 8
      2 3 2
      3
      0 3 5
      0 3 7
      0 2 5
输出: -1
      5
      3

(t = [1, 3, 5, 7]:村庄 0/1/2 在时刻 5 已修好,村庄 3 要到时刻 7)
(t=5 时查 0→3:3 号村未修好 → -1)
(t=7 时查 0→3:走 0-2-3 = 3+2 = 5)

💡 想一想

如果题目改成"修好时间不是递增的",这个"单调指针"就失效了。这时该怎么办?

做法是把所有询问按 T 从小到大离线排序,然后同样用指针逐个"开放"村庄 —— 因为我们已经按 T 排好了序,指针仍然是单调前进的。这就是离线处理的典型思路:把"必须按时间顺序"的约束转成"排序后再回答"。

模块 03

实践应用

三道递进练习:先用 Floyd 手推全城距离表,再练严格次短路的双数组思维,最后用传递闭包解决"排名确定"问题。

A1基础应用题

用 Floyd 求出下面无向图的所有点对最短距离:4 个点,边为 1-2:3、1-3:8、2-3:2、2-4:5、3-4:1。

提示:按 k = 1、2、3、4 的顺序逐个开放中转点,每次更新整个矩阵。
查看参考答案

初始矩阵:

  1 2 3 4
1 0 3 8 ∞
2 3 0 2 5
3 8 2 0 1
4 ∞ 5 1 0

k = 1:没有任何组合能变短 → 不变。

k = 2:d[1][3] = 3+2 = 5(原 8);d[1][4] = 3+5 = 8(原 ∞)。

k = 3:d[1][4] = 5+1 = 6(原 8);d[2][4] = 2+1 = 3(原 5)。

k = 4:无变化,算法结束。

最终矩阵:

  1 2 3 4
1 0 3 5 6
2 3 0 2 3
3 5 2 0 1
4 6 3 1 0

✅ 特别关注 d[1][4] = 6,路径是 1-2-3-4(经过两个中转点)。如果误把 k 写在内层,最多只能算出一个中转点,就会得到错误的值 8。

B1进阶应用题

有向图:1→2:1、2→4:1、1→3:1、3→4:1、1→4:3。求 1 到 4 的(1)最短路长度;(2)严格次短路长度;(3)非严格次短路长度。

提示:先把所有从 1 到 4 的路径列出来,再按定义区分。
查看参考答案

列出从 1 到 4 的所有路径:

① 1 → 2 → 4,长度 1 + 1 = 2
② 1 → 3 → 4,长度 1 + 1 = 2
③ 1 → 4,长度 3

答案:

(1)最短路 = 2(有两条,①和②)

(2)严格次短路 = 3(严格大于 2 的最小值,来自路径 ③)

(3)非严格次短路 = 2("第二短的长度值",与最短路相同)

💡 这就是"严格"与"非严格"的分水岭。程序上只要把 nd > dis1[v] 改成 nd >= dis1[v],同一个算法就能求出非严格版本。先看清题目要哪一个,再决定写哪个符号。

⚠️ 另一个易错点:如果把题目读成"从 1 到 4 的第二条不同的路径",那么答案是 2(路径 ②)—— 这是"非严格"的语义。三种理解都可能出现在题目里,务必回到题面确认。

C1综合应用题

5 头奶牛进行了若干场比赛,已知结果:1 胜 2、2 胜 3、3 胜 4、1 胜 5、5 胜 4(胜负关系具有传递性:若 a 胜 b 且 b 胜 c,则 a 必胜 c)。请问有几头奶牛的排名是唯一确定的?

提示:一头奶牛的排名唯一确定 ⟺ 它与其他每一头奶牛之间都有确定的胜负关系。
查看参考答案

建图:用有向边 a→b 表示"a 胜 b",用传递闭包求出所有"能赢"关系。

传递闭包结果(谁能赢谁):

1 能赢:2, 3, 4, 5(1→2→3→4;1→5→4)
2 能赢:3, 4
3 能赢:4
4 能赢:(无)
5 能赢:4

逐个检查"是否和其他每一头都有确定关系":

· 奶牛 1:赢了 2、3、4、5 全部 → 确定(第 1 名) ✅

· 奶牛 4:输给了 1、2、3、5 全部 → 确定(第 5 名) ✅

· 奶牛 2:与 5 之间没有关系 → 不确定 ❌

· 奶牛 3:与 5 之间没有关系 → 不确定 ❌

· 奶牛 5:与 2、3 之间没有关系 → 不确定 ❌

答案:2 头(奶牛 1 和奶牛 4)

✨ 核心判据:对奶牛 i,若"i 能赢的奶牛数 + 能赢 i 的奶牛数 = n − 1",则 i 与所有其他奶牛都有确定关系,排名唯一确定。用传递闭包矩阵实现时,就是统计第 i 行和第 i 列的 1 的个数(不算对角线)。

模块 04

总结提升

①核心要点与万能模板
考点关键结论必须记住的细节
Floyd 主体for k → for i → for jk 必须最外层(它是 DP 的"阶段")
状态定义d[i][j]:只允许前 k 个点中转时的最短路二维滚动省掉一维,空间 O(n²)
复杂度O(n³) 时间 / O(n²) 空间n ≤ 500 可用;n > 1000 必超时
初始化对角线 0、其余 INF、重边取 min三条错一条都会出隐蔽的错误
负环判定d[i][i] < 0 → 有负环因为"绕一圈回到自己"比"原地不动"更短
传递闭包r[i][j] |= r[i][k] && r[k][j];用 bitset 优化到 O(n³/64)
严格次短路dis1 最短,dis2 严格次短刷新 dis1 时要把旧值降级给 dis2
非严格次短路条件从 > 改为 >=允许与最短路等长
枚举删边O(n m log n)求的是"封路后的最短路",不等于严格次短路
增量 Floyd指针 + k 循环前缀适用"按时间陆续启用点"的模型
C++万能模板:Floyd(含负环判定)
const int MAXN = 505;
const long long INF = 1e18;
long long d[MAXN][MAXN];

void init(int n) {
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            d[i][j] = (i == j) ? 0 : INF;
}

void addEdge(int u, int v, long long w, bool undirected = false) {
    if (w < d[u][v]) d[u][v] = w;                  // 重边取最小
    if (undirected && w < d[v][u]) d[v][u] = w;
}

void floyd(int n) {
    for (int k = 1; k <= n; k++)                // 阶段:中转点,必须最外层
        for (int i = 1; i <= n; i++) {
            if (d[i][k] == INF) continue;
            for (int j = 1; j <= n; j++) {
                long long nd = d[i][k] + d[k][j];
                if (nd < d[i][j]) d[i][j] = nd;
            }
        }
}

bool hasNegCycle(int n) {
    for (int i = 1; i <= n; i++)
        if (d[i][i] < 0) return true;
    return false;
}
C++万能模板:严格次短路(dis1 / dis2 双数组)
long long dis1[MAXN], dis2[MAXN];
typedef pair<int, int> PII;
typedef pair<long long, PII> PLII;

void dijkstra2(int s, int n) {
    for (int i = 1; i <= n; i++) { dis1[i] = INF; dis2[i] = INF; }
    dis1[s] = 0;
    priority_queue<PLII, vector<PLII>, greater<PLII>> pq;
    pq.push({0, {s, 0}});
    while (!pq.empty()) {
        PLII cur = pq.top(); pq.pop();
        long long d = cur.first;
        int u = cur.second.first, t = cur.second.second;
        if (t == 0 && d != dis1[u]) continue;
        if (t == 1 && d != dis2[u]) continue;
        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].to;
            long long nd = d + e[i].w;
            if (nd < dis1[v]) {                      // 严格:nd < dis1[v]
                dis2[v] = dis1[v]; dis1[v] = nd;     // 非严格改成 nd >= dis1[v]
                pq.push({nd, {v, 0}});
                if (dis2[v] < INF) pq.push({dis2[v], {v, 1}});
            } else if (nd > dis1[v] && nd < dis2[v]) {
                dis2[v] = nd;
                pq.push({nd, {v, 1}});
            }
        }
    }
}
🎯 选型速判

多次任意两点查询 → Floyd;只要单源 → Dijkstra;只问可达性 → 传递闭包 + bitset;问"第二短" → dis1/dis2;问"封路后" → 枚举删边。

②拓展案例
拓展1Floyd 求最小环:把"环"拆成"一条边 + 一条路径" 提高组经典

问题:求无向图中权值和最小的环。

🔍 核心思路

一个环可以看成 "一条边 (i,k) + 一条从 i 到 k 的、不经过 k 的路径"。而这条路径的最小长度,正是 Floyd 在处理到"k 号点之前的阶段"时矩阵里的值。

做法:在 Floyd 的 k 循环内,先更新答案、再做松弛:

ans = min(ans, d[i][j] + g[i][k] + g[k][j]);   (i, j < k)

其中 g 是原始边权矩阵(不参与 Floyd 更新),d 是当前的 Floyd 距离矩阵。顺序不能颠倒:如果先松弛 d,那么 "i 到 j 的路径"就可能已经用到了 k,d[i][j] + g[i][k] + g[k][j] 就不再是"经过 k 的环",会算错。

为什么要求 i, j < k:这样能保证 d[i][j] 是在"还没有开放 k 作为中转点"的阶段算出来的,路径必然不经过 k。

复杂度:只多了两层枚举,仍是 O(n³)。

C++Floyd 求最小环(核心片段)
long long ans = INF;
for (int k = 1; k <= n; k++) {
    // ① 先更新答案(用"未经过 k"的路径 + 两条与 k 相连的边)
    for (int i = 1; i < k; i++)
        for (int j = i + 1; j < k; j++)
            if (d[i][j] < INF && g[i][k] < INF && g[k][j] < INF)
                ans = min(ans, d[i][j] + g[i][k] + g[k][j]);

    // ② 再做正常的 Floyd 松弛
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (d[i][k] + d[k][j] < d[i][j])
                d[i][j] = d[i][k] + d[k][j];
}

示例:无向图 1-2:1、2-3:1、3-1:1、3-4:1、1-4:10,最小环是 1-2-3-1,权值和 = 1+1+1 = 3(另一个环 1-3-4-1 的和是 1+1+10 = 12,更大)。

🔁 最小环 = 一条边 + 一条"不经过该边端点"的最短路径
拓展2计数版 Floyd:最短路条数与"社交网络" 运算替换

问题:统计任意两点之间最短路的条数(NOI2007 社交网络 P2047 的基础)。

🔍 把 min / + 换成新的运算组合

维护两个矩阵:d[i][j] 表示最短距离,w[i][j] 表示最短路条数。

初始化:d[i][i] = 0, w[i][i] = 1;有边 u→v 权 c 时 d[u][v] = c, w[u][v] = 1;若出现等长的重边,则 w[u][v]++。

转移(三种情况):

① 若 d[i][k] + d[k][j] < d[i][j] → d[i][j] = d[i][k] + d[k][j],条数覆盖:w[i][j] = w[i][k] × w[k][j];

② 若 d[i][k] + d[k][j] == d[i][j] → 条数累加:w[i][j] += w[i][k] × w[k][j];

③ 若更大 → 忽略。

这与"最短路计数"(上一讲的 Dijkstra 计数)本质相同,只是 Floyd 把"所有中转可能"都枚举了一遍,所以天然不需要讨论拓扑序。

易错:① 条数可能极大,必须取模或高精度;② w[i][k] * w[k][j] 要用 long long 甚至 __int128,否则中间乘积会溢出;③ 转移时必须跳过 i == k 或 j == k 的情况(否则会自己乘自己)。

🔢 计数版 Floyd:更短就覆盖、等长就累加,乘法配合加法
拓展3倍增 + Floyd:P1613 跑路这样的"二进制跳跃" 思维拓展

问题:有向图,每条边耗时 1 秒,但有一种"跑路机"可以在 1 秒内走过恰好 2k 条边(k 任意)。求 1 到 n 的最少秒数。

🔍 从"能否跳 2k 步"入手

设 f[k][i][j] = "i 到 j 是否存在一条恰好 2k 条边的路径"。这个状态可以像 ST 表那样倍增转移:

f[k][i][j] = OR over t of ( f[k−1][i][t] AND f[k−1][t][j] )

外层枚举 k,内层就是标准的三重循环 —— 这正是 Floyd 的骨架,只是把 min/+ 换成了 or/and。

之后建一张新图:f[k][i][j] == true 时连一条 i→j、权为 1 的边,在新图上跑一次最短路(BFS 或 Dijkstra)即可。

关键点:k 只需要枚举到 log₂(n) 左右(因为"恰好 2k 条边"的路径至少要 2k 个点,路径不重复点时最多 n−1 条边)。总复杂度 O(n³ log n)。

思维价值:它展示了 Floyd 骨架的可塑性 —— 只要运算满足结合律并且状态定义合适,同一套三重循环可以解决完全不同的问题。

🧬 倍增 + Floyd:把"2k 步可达"当作新的边,再跑一次最短路
③思考与启发

🤔 思考1:把 Floyd 的 min/+ 换成 max/+(最长路),为什么不能用?

Floyd 的骨架看起来很通用,那"最长路"能不能也这么写?

查看参考答案

不能用。原因有两点:

① 正环会让答案发散。如果把 max(d[i][j], d[i][k] + d[k][j]) 迭代下去,只要图中存在正环,就能在环上无限绕行让路径无限变长,最长路变成 +∞,迭代永不收敛(对应到 Floyd 上就是 d[i][i] 会一直变大)。

② 破坏"二维滚动"的前提。Floyd 能用二维数组的前提是 d[i][k] 和 d[k][j] 在"以 k 为中转"这一步不会被改坏。对最长路而言,如果存在正环,这个前提就崩了。

什么时候最长路可做?当图是 DAG(有向无环图) 时,最长路问题可以用"拓扑排序 + DP"在 O(n+m) 内解决 —— 这正是下一讲的主题。

📌 记忆:Floyd 的 min/+ 可以换,但前提是"环不会让答案无限变优或变劣"。最短路对负环敏感、最长路对正环敏感,所以两者都必须先判环。

🤔 思考2:为什么 dis1/dis2 双数组法一定正确?

Dijkstra 原有的正确性依赖"弹出即定稿",现在多了个 dis2,这个性质还成立吗?

查看参考答案

成立,而且证明结构完全一样。

把每个点拆成两个"虚拟状态":状态 Au(最短路状态)和 状态 Bu(次短路状态)。总共 2n 个状态。在原图上做如下改造:

· Au → Av:权 w("用最短路走出 u 的边");
· Au → Bv:权 w("用最短路走出 u,但结果成为 v 的次短路");
· Bu → Bv:权 w("用次短路走")。

在这个新图上跑普通 Dijkstra,Au 的最短路就是 dis1[u],Bu 的最短路就是 dis2[u]。因为所有边权为正,新图的 Dijkstra 正确性由经典定理保证。

结论:双数组法本质上是"把状态空间扩大 2 倍后跑普通 Dijkstra",所以它继承了一切正确性保证。这也解释了为什么复杂度只涨了一个常数倍。

🌟 这个"状态扩展"的思想可以推广到"第 K 短路":把每个点拆成 K 个状态,跑 Dijkstra 得 O(K m log(Kn))。但要注意:这样得到的"路径"允许重复经过点,与"简单路径"的 K 短路不是一回事(后者是真正的难题)。

🤔 思考3:Floyd 的 n³ 什么时候是"瓶颈",什么时候不是?

n ≤ 500 时 Floyd 很快,n = 2000 就必超时。但如果 n 很大、m 却很小(稀疏图),有其他办法吗?

查看参考答案

有,但要分情况讨论:

① 查询次数少、n 大、m 小 → 直接对每个起点跑一遍堆优化 Dijkstra,总复杂度 O(n(m log n))。但 n = 105 时这也太大。

② 图有特殊结构。如果图是一棵树,可以用 LCA / 树上差分把距离查询降到 O(log n);如果是稀疏图 + 特殊性质(如边权为 1、0/1、DAG),可以借 BFS / 拓扑 / 0-1 BFS 加速。

③ 只要"可达性" → 用 bitset 把 Floyd 压到 O(n³/64),n = 5000 时约 2×109 次机器字运算,勉强可行(视时限)。

④ 只要"有向连通分量" → 用 Tarjan 求 SCC(后续讲强连通分量时会学到),复杂度 O(n+m),远快于传递闭包。

⑤ 只要"任意两点最短距离的近似" → 有向图可用"多次从随机点跑 Dijkstra"的方法(如 Thorup-Zwick 的近似距离标签),但这是超纲内容。

📌 结论:Floyd 只是"小图 + 密集查询"的专用工具。遇到大图要先问自己"真的需要全部点对吗"、"图有没有特殊结构",而不是硬上 Floyd。

模块 05

学习检测

先找错,再背口诀——把易错点消灭在考试之前。

⚡高频错误诊断
易错1

k 循环写在了最内层

❌ 错误
for (i) for (j) for (k) d[i][j] = min(d[i][j], d[i][k]+d[k][j]);

后果:只能求出"最多经过一个中转点"的路径。像 1-2-3-4 这样需要两个中转点的最短路会算错,但样例往往看不出来。
✅ 正确
for (k) for (i) for (j) ...

k 是 DP 的"阶段",必须最外层;i、j 的顺序无所谓。检验方法:造一条需要两个中转点的长链,看能不能算对。
口诀:中转点在外层,起点终点在内层!
易错2

初始化漏掉对角线 / 重边没取 min

❌ 错误
① 只把边对应的格子填了权值,忘了 d[i][i] = 0。
② 读边时写 d[u][v] = w;(直接覆盖)。

后果:① 会出现"自己到自己要绕一圈"的荒谬结果;② 输入里两条同端点不同权的边时,后读入的会覆盖掉更优的,答案偏大。
✅ 正确
先整体置 INF、对角线置 0;读边一律用 if (w < d[u][v]) d[u][v] = w;。
无向图再加一句 d[v][u] = d[u][v];。
口诀:对角线归零,其余无穷大,读边必取小!
易错3

无向图忘了对称赋值

❌ 错误
无向图只写了 d[u][v] = w;,没写 d[v][u] = w;。

后果:矩阵变成"有向"的,反向查询全部返回 ∞(不可达)。样例常常只查一个方向,所以不易发现。
✅ 正确
无向图务必两个方向都赋值:if (w < d[u][v]) { d[u][v] = w; d[v][u] = w; }
或有向图时,如果输入给的是"双向边"(比如 P1119 的道路),也要两边都写。
口诀:无向边要写两遍,矩阵才是对称的!
易错4

次短路刷新 dis1 时忘了"降级"旧值

❌ 错误
if (nd < dis1[v]) { dis1[v] = nd; pq.push({nd,{v,0}}); } —— 只更新 dis1,不管 dis2。

后果:原来的最短路(那条"退居第二"的路径)被白白丢掉了,dis2 会偏大甚至一直是 ∞。
✅ 正确
if (nd < dis1[v]) { dis2[v] = dis1[v]; dis1[v] = nd; push 两个状态; }

"旧最短自动降级为次短"是双数组法的灵魂,漏掉它整个算法就废了。同时记得把新的 dis2 也入堆。
口诀:新王登基,旧王退位 —— dis2 接住被挤下来的旧 dis1!
易错5

INF 相加溢出 / INF 取值过小

❌ 错误
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
若 INF 取 INT_MAX(2147483647),两个 INF 相加直接溢出成负数,被当成"更短路"写进矩阵,然后污染全部结果。
✅ 正确
① 用 long long + 1e18,两个 1e18 相加约 2×1018 仍小于 long long 上限 9.2×1018;
② 或者用 0x3f3f3f3f(约 1.06×109),两倍仍小于 int 上限;
③ 最稳的是加判断:if (d[i][k] == INF) continue;。
口诀:无穷大要"大而不溢" —— long long 配 1e18,并加 INF 判断!
📋速查表
算法 / 变体复杂度核心转移解决的问题
Floyd 最短路O(n³)min(d[i][j], d[i][k]+d[k][j])任意两点最短距离
传递闭包O(n³) / O(n³/64)r[i][j] |= r[i][k] && r[k][j]可达性判断
计数版 FloydO(n³)更短覆盖、等长累加 (w×w)最短路条数
最小环O(n³)d[i][j] + g[i][k] + g[k][j]权值和最小的环
最小瓶颈路O(n³)min(d[i][j], max(d[i][k], d[k][j]))路径最大边最小
倍增 FloydO(n³ log n)f[k][i][j] |= f[k−1][i][t] && f[k−1][t][j]2k 步可达性
次短路双数组O(m log n)dis1 降级 / dis2 更新严格次短路
枚举删边O(n m log n)逐条删最短路上的边重跑封路后的最短路
关键动作标准写法注意
初始化d[i][j] = (i==j) ? 0 : INF;INF 用 1e18 + long long
读边if (w < d[u][v]) { d[u][v]=w; d[v][u]=w; }无向图两个方向;重边取 min
Floyd 主体for (k) { if (d[i][k]==INF) continue; for (i) for (j) ... }k 最外层;加 INF 剪枝
负环判定for (i) if (d[i][i] < 0) 有负环;跑完 Floyd 再判
bitset 闭包if (g[i][k]) g[i] |= g[k];g 为 bitset<MAXN> 数组
次短路松弛if (nd < dis1[v]) { dis2[v]=dis1[v]; dis1[v]=nd; } else if (nd > dis1[v] && nd < dis2[v]) dis2[v]=nd;严格用 >,非严格用 >=
增量 Floydwhile (cur < n && t[cur] <= T) { 以 cur 为 k 做一轮; cur++; }要求启用时间递增(或离线排序)
📌笔记卡
🧠 必记概念
  • Floyd 状态:只允许前 k 个点中转时的最短距离
  • k 是阶段,必须最外层
  • 三维转二维:因为 d[i][k]、d[k][j] 在本阶段不会被改坏
  • 负环判据:d[i][i] < 0
  • 严格次短路 > 最短路;非严格次短路可相等
  • 枚举删边 ≠ 求严格次短路
📐 必背模板
  • Floyd 三行:for(k) for(i) for(j) d[i][j]=min(d[i][j], d[i][k]+d[k][j]);
  • 传递闭包:if(r[i][k]) r[i][j] |= r[k][j];
  • bitset 版:if (g[i][k]) g[i] |= g[k];
  • 次短路降级:dis2[v] = dis1[v]; dis1[v] = nd;
  • 最小环:ans = min(ans, d[i][j]+g[i][k]+g[k][j]);
  • 堆元素:pair<long long, pair<int,int>> = (距离, (点, 类型))
⚠️ 考场提醒
  • 先看 n:n ≤ 500 才考虑 Floyd
  • 先看题:严格次短路还是非严格?
  • Floyd 的 k 循环千万别手滑放内层
  • 无向图两个方向都要赋值
  • 次短路记得"降级"旧值 + 新 dis2 也要入堆
  • INF 用 1e18 或 0x3f3f3f3f,别用 INT_MAX
  • 灾后重建类题目注意"两端点是否都已修好"
课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增。C组后附 12 道洛谷原题。

🟢 A组 · 基础(4题)

A1概念题

Floyd 三重循环中,k、i、j 分别代表什么?为什么 k 必须在最外层?Floyd 的时间与空间复杂度是多少?

查看参考答案

k:中转点(阶段变量),表示"目前允许作为中转的点的编号上限"。循环到第 k 轮时,含义是"只允许使用 1..k 号点作为中转点"。

i:路径起点。j:路径终点。d[i][j] 表示当前阶段下 i 到 j 的最短距离。

为什么 k 最外层:Floyd 是动态规划,k 是"阶段"。DP 要求按阶段依次递推 —— 第 k 阶段的状态只能由第 k−1 阶段推出。若把 k 放到内层,含义就变成"对每一对 (i,j),只允许一次中转",那么像 1-2-3-4 这样需要两个中转点的路径就算不出来。

复杂度:时间 O(n³)(三重循环各 n 次),空间 O(n²)(只用一个二维矩阵,完成"三维转二维"的滚动优化)。

易错:i、j 的先后顺序无所谓(对称图甚至完全一样),只有 k 的位置是硬性要求。

A2编程题

用 Floyd 求下面无向图的所有点对最短距离:5 个点,边为 1-2:2、2-3:3、1-3:9、3-4:1、4-5:4、2-5:10。请写出 d[1][5]、d[2][5]、d[1][4] 的值。

查看参考答案

答案:d[1][5] = 10;d[2][5] = 8;d[1][4] = 6。

推导:

· d[1][5]:走 1-2-3-4-5 = 2+3+1+4 = 10,比 1-2-5(2+10 = 12)更短 → 10(经过 2、3、4 三个中转点!)

· d[2][5]:走 2-3-4-5 = 3+1+4 = 8,比 2-5 直连的 10 更短 → 8

· d[1][4]:走 1-2-3-4 = 2+3+1 = 6 → 6(注意 1-3 直连是 9,但 1-2-3 只要 5)

完整矩阵:

  1  2  3  4  5
1  0  2  5  6 10
2  2  0  3  4  8
3  5  3  0  1  5
4  6  4  1  0  4
5 10  8  5  4  0

易错:d[1][5] = 10 这条路径要经过 2、3、4 三个中间点,这正是"k 必须最外层"的最好检验用例 —— k 放内层的错误写法会得到 12。

A3判断题

判断下面说法的正误并说明理由:
(1)Floyd 的 i、j、k 三个循环顺序随便怎么放都行;
(2)Floyd 可以处理负权边;
(3)跑完 Floyd 后如果存在 d[i][i] < 0,说明有负环;
(4)Floyd 的空间复杂度是 O(n³)。

查看参考答案

(1)错。只有 k 必须最外层。i、j 的顺序无所谓,但 k 放到内层含义就完全变了(只允许一次中转),会算错。

(2)对。Floyd 的转移是"取最小值 + 加法",对负权边完全成立。只要没有负环,结果就正确。

(3)对。对角线初始为 0(代表"原地不动")。若 i 能沿一个权值和为负的环回到自己,Floyd 就会把 d[i][i] 更新成这个负值。反之所有环权非负时,对角线必然保持 0。

(4)错。空间是 O(n²)。虽然"理论上的三维 DP"是 O(n³),但 Floyd 可以原地滚动(因为 d[i][k] 和 d[k][j] 在以 k 为中转时不会被改坏),所以只需要一个二维矩阵。

A4编程题

用传递闭包判断可达性:5 个点(编号 1..5),有向边为 1→2、2→3、3→4、1→5、5→4。写出可达矩阵,并回答"哪些点能到达 4 号点"。

查看参考答案

传递闭包矩阵(1 表示行号点能到达列号点):

  1 2 3 4 5
1  1  1  1  1  1
2  0  1  1  1  0
3  0  0  1  1  0
4  0  0  0  1  0
5  0  0  0  1  1

能到达 4 号点的点(看第 4 列):1、2、3、4、5 —— 全部 5 个点。

验证:1→2→3→4 ✅;2→3→4 ✅;3→4 ✅;4 自己 ✅;1→5→4 ✅。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2005;
bitset<MAXN> g[MAXN];
int main(){
    ios::sync_with_stdio(0); cin.tie(0);
    int n = 5, m = 5;
    for (int i = 1; i <= n; i++) g[i][i] = 1;
    int eu[6]={0,1,2,3,1,5}, ev[6]={0,2,3,4,5,4};
    for (int i = 1; i <= m; i++) g[eu[i]][ev[i]] = 1;

    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            if (g[i][k]) g[i] |= g[k];

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) cout << g[i][j] << ' ';
        cout << '\n';
    }
    return 0;
}

输出:

1 1 1 1 1
0 1 1 1 0
0 0 1 1 0
0 0 0 1 0
0 0 0 1 1

易错:① g[i][i] = 1 不能漏(自己一定能到自己);② 用 bitset 时下标从 0 开始,编程时要留意偏移。

🟡 B组 · 提高(4题)

B1编程题

求严格次短路:有向图 4 个点,源点 1,边为 1→2:1、2→3:1、3→4:1、1→4:5、2→4:3。求 1 到 4 的最短路与严格次短路。

查看参考答案

列出从 1 到 4 的所有路径:

① 1→2→3→4 = 1+1+1 = 3
② 1→2→4 = 1+3 = 4
③ 1→4 = 5

答案:最短路 = 3;严格次短路 = 4

双数组算法的执行过程:

弹出 1(0):dis1[2] = 1、dis1[4] = 5
弹出 2(1):2→3 得 dis1[3] = 2;2→4 得 nd = 4 < dis1[4] = 5 → dis2[4] = 5、dis1[4] = 4
弹出 3(2):3→4 得 nd = 3 < dis1[4] = 4 → dis2[4] = 4、dis1[4] = 3
弹出 4(3) 与 4(4):无出边,结束
结果:dis1[4] = 3,dis2[4] = 4 ✅

易错:注意 dis2[4] 经历了 "5 → 4" 的两次更新(先被 1→4 的 5 占据,后被 1→2→4 的 4 刷新)。每次刷新 dis1 时都要先把旧值降级给 dis2,这样才不会丢掉这些候选。

B2纠错题

下面的次短路代码有 3 处错误,请指出并改正:

while (!pq.empty()) {
    PLII cur = pq.top(); pq.pop();
    long long d = cur.first;
    int u = cur.second.first;
    for (int i = head[u]; i; i = e[i].nxt) {
        int v = e[i].to;
        long long nd = d + e[i].w;
        if (nd < dis1[v]) {
            dis1[v] = nd;
            pq.push({nd, {v, 0}});
        }
    }
}
查看参考答案

错误 1:没有处理"降级"。刷新 dis1 时必须先把旧值交给 dis2:dis2[v] = dis1[v];,并把这个新的 dis2 入堆。

错误 2:没有"过期副本"判定。堆里的元素要带"类型"(0 = 最短路候选,1 = 次短路候选),弹出时要检查 d 是否与 dis1[u] 或 dis2[u] 一致,不一致就 continue。

错误 3:缺少"刷新次短路"的分支。少了 else if (nd > dis1[v] && nd < dis2[v]) { dis2[v] = nd; pq.push({nd, {v, 1}}); }。

正确版本:

while (!pq.empty()) {
    PLII cur = pq.top(); pq.pop();
    long long d = cur.first;
    int u = cur.second.first, t = cur.second.second;
    if (t == 0 && d != dis1[u]) continue;      // ← 修复 2
    if (t == 1 && d != dis2[u]) continue;
    for (int i = head[u]; i; i = e[i].nxt) {
        int v = e[i].to;
        long long nd = d + e[i].w;
        if (nd < dis1[v]) {
            dis2[v] = dis1[v];                      // ← 修复 1
            dis1[v] = nd;
            pq.push({nd, {v, 0}});
            if (dis2[v] < INF) pq.push({dis2[v], {v, 1}});
        } else if (nd > dis1[v] && nd < dis2[v]) {   // ← 修复 3
            dis2[v] = nd;
            pq.push({nd, {v, 1}});
        }
    }
}
B3编程题

Floyd 求最小环:无向图 4 个点,边为 1-2:1、2-3:1、3-1:1、3-4:1、1-4:10。求最小环的权值和。

查看参考答案

枚举所有环:

· 1-2-3-1:1+1+1 = 3
· 1-3-4-1:1+1+10 = 12
· 1-2-3-4-1:1+1+1+10 = 13

答案:3(环 1-2-3-1)

算法的关键顺序:在 k 循环内必须先更新答案、再做松弛:

for (int k = 1; k <= n; k++) {
    // ① 先算环(此时 d[i][j] 还没允许经过 k)
    for (int i = 1; i < k; i++)
        for (int j = i + 1; j < k; j++)
            if (d[i][j] < INF && g[i][k] < INF && g[k][j] < INF)
                ans = min(ans, d[i][j] + g[i][k] + g[k][j]);
    // ② 再松弛 d
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (d[i][k] + d[k][j] < d[i][j]) d[i][j] = d[i][k] + d[k][j];
}

易错:① 顺序颠倒是最常见的错误 —— 若先松弛 d,则 d[i][j] 可能已经"经过 k",那么 d[i][j] + g[i][k] + g[k][j] 就不再是一条简单环;② 必须用两份矩阵(g 保持原始边权不参与松弛);③ 无向图要判 i < j 避免重复计算。

B4应用题

奶牛比赛:n = 5,已知结果 1 胜 2、2 胜 3、3 胜 4、1 胜 5、5 胜 4(胜负关系可传递)。用传递闭包求出"哪些奶牛的排名唯一确定"。

查看参考答案

判据:奶牛 i 的排名唯一确定 ⟺ "i 能赢的奶牛数 + 能赢 i 的奶牛数 = n − 1",即 i 与其他每一头奶牛之间都有确定的胜负关系。

传递闭包结果:

· 1 能赢:2, 3, 4, 5(全胜)→ 与其他 4 头都有关系 → 排名确定(第 1 名)

· 2 能赢:3, 4;能被 1 赢 → 与 5 无关系 → 不确定

· 3 能赢:4;能被 1、2 赢 → 与 5 无关系 → 不确定

· 4 能赢:无;能被 1, 2, 3, 5 全部赢 → 排名确定(第 5 名)

· 5 能赢:4;能被 1 赢 → 与 2、3 无关系 → 不确定

答案:2 头(奶牛 1 和奶牛 4)

实现要点:建图时有边 a→b 表示 a 胜 b;r[i][i] = 1;跑传递闭包;对每个 i 统计 cnt = Σj≠i (r[i][j] || r[j][i]),若 cnt == n−1 则答案 +1。

易错:传递闭包的 r[i][i] = 1 不能漏;统计时不要把自己算进去(否则 cnt 永远多 1)。

🔴 C组 · 挑战(4题)

C1综合题

增量 Floyd:n = 4 个村庄,修好时刻分别为 t = [1, 3, 5, 7]。道路(双向)为 0-1:4、0-2:3、1-2:1、1-3:8、2-3:2。请回答查询 (0, 3, 5) 和 (0, 3, 7)。

查看参考答案

查询 (0, 3, T = 5):此时修好的村庄是 0(t=1)、1(t=3)、2(t=5)。村庄 3 的 t=7 > 5,还没修好 → 输出 −1。

查询 (0, 3, T = 7):指针推到所有 t ≤ 7 的村庄都启用(0,1,2,3 全启用)。此时矩阵已被 Floyd 更新完毕:d[0][3] = d[0][2] + d[2][3] = 3 + 2 = 5(走 0-2-3)。注意 0-1-3 是 4+8 = 12,更差 → 输出 5。

关键代码骨架:

int cur = 0;    // 已启用的村庄个数
while (q--) {
    int x, y, T; cin >> x >> y >> T;
    while (cur < n && t[cur] <= T) {          // 开放新村庄作为中转点
        int k = cur;
        for (int i = 0; i < n; i++) {
            if (d[i][k] == INF) continue;
            for (int j = 0; j < n; j++)
                if (d[i][k] + d[k][j] < d[i][j]) d[i][j] = d[i][k] + d[k][j];
        }
        cur++;
    }
    if (t[x] > T || t[y] > T) cout << -1 << '\n';      // 端点未修好
    else if (d[x][y] == INF) cout << -1 << '\n';       // 不可达
    else cout << d[x][y] << '\n';
}

易错:① 忘记"判断两端点是否已修好",直接查 unrebuilt 的点会得到错误结果;② 若询问的 T 不是递增的,必须先把询问按 T 离线排序,否则指针不能只前进;③ 指针只前进不后退,总复杂度 O(n³ + q)。

C2综合题

计数版 Floyd:4 个点,无向边 1-2:1、2-3:1、1-3:1、3-4:1。求任意两点间最短路径的条数,并回答"1 到 3 有几条最短路"。

查看参考答案

直接分析:

· 1 到 2:直连 1-2,长度 1,1 条
· 1 到 3:直连 1-3(长度 1)与 1-2-3(长度 2),最短是 1,只有 1 条
· 1 到 4:1-3-4 = 2,只有 1 条
· 2 到 3:直连长度 1,1 条
· 2 到 4:2-3-4 = 2,1 条
· 3 到 4:直连长度 1,1 条

答案:1 到 3 只有 1 条最短路(长度 1 的直连边)。

核心代码(Floyd 计数版):

// d[i][j] = 最短距离,w[i][j] = 最短路条数
for (int i = 1; i <= n; i++) { d[i][i] = 0; w[i][i] = 1; }
for (int i = 1; i <= m; i++) {
    int u, v; long long c; cin >> u >> v >> c;
    if (c < d[u][v]) { d[u][v] = d[v][u] = c; w[u][v] = w[v][u] = 1; }
    else if (c == d[u][v]) { w[u][v]++; w[v][u]++; }   // 等长重边,条数 +1
}

for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++) {
        if (i == k || d[i][k] == INF) continue;
        for (int j = 1; j <= n; j++) {
            if (j == k || j == i) continue;
            long long nd = d[i][k] + d[k][j];
            if (nd < d[i][j]) {
                d[i][j] = nd;
                w[i][j] = w[i][k] * w[k][j];               // 更短 → 覆盖
            } else if (nd == d[i][j]) {
                w[i][j] += w[i][k] * w[k][j];              // 等长 → 累加
            }
        }
    }

易错:① 必须跳过 i == k、j == k、i == j,否则会"自己乘自己"导致条数虚增;② w[i][k] * w[k][j] 可能溢出 int,要用 long long;③ 条数通常要对大质数取模。

C3变体题

最小瓶颈路(Floyd 版):把 Floyd 的运算改成 d[i][j] = min(d[i][j], max(d[i][k], d[k][j])),它求的是什么?请用一个例子说明。

查看参考答案

它求的是"最小瓶颈路":从 i 到 j 的所有路径中,"路径上最大边权"的最小值。

为什么这样转移是对的:一条经过 k 的路径,它的"最大边权" = max(i→k 上最大边权, k→j 上最大边权)。而总目标是让这个 max 尽可能小,所以就是 min(不用k, max(经k的两段)) —— 与最短路的结构完全一致,只是把"加法"换成"取最大"。

初始化:逻辑上"原地不动不需要任何边",所以 d[i][i] = 0;有边 u-v 权 w 时 d[u][v] = w。这里不能把 d[i][i] 设成 INF(它表示"没有边权的最大值为 0")。

示例:4 个点无向图,边为 1-2:1、2-3:5、3-4:2、1-3:4、1-4:3。求 1→4 的最小瓶颈路:

· 路径 1-4:最大边 3 → 瓶颈 3
· 路径 1-2-3-4:最大边 5 → 瓶颈 5
· 路径 1-3-4:最大边 4 → 瓶颈 4
· 最小值 = 3(直接走 1-4)

与 MST 的关系:最小瓶颈路一定在最小生成树上。用 Kruskal 求出 MST 后,树上两点路径的最大边权就是答案 —— 这是求最小瓶颈路的另一种(通常更快的)方法,多次询问时可以配合 LCA 做到 O(log n) 每次。

易错:① d[i][i] 必须置 0 而不是 INF;② 注意区分"最小瓶颈路"和"最短路"—— 前者优化最大边权,后者优化边权和,两者的答案路径可能完全不同。

C4思维题

倍增 Floyd:有向图 4 个点,边为 1→2、2→3、3→4、1→4。若"跑路机"每次可以走恰好 2k 条边(耗时 1 秒),求 1 到 4 的最少秒数。

查看参考答案

第一步:算出 f[k][i][j](i 到 j 是否存在恰好 2k 条边的路径)

· f[0](恰好 1 条边):直接来自边表 → 1→2 ✅、2→3 ✅、3→4 ✅、1→4 ✅

· f[1](恰好 2 条边):由 f[0] 的两段拼接 → 1→3(1→2→3)✅、2→4(2→3→4)✅

· f[2](恰好 4 条边):由 f[1] 的两段拼接 → 1→4(1→3 与 3→4?需要 2+2 步)… 实际检查后 1→? 没有恰好 4 条边的路径(点数不够)

第二步:建新图并跑最短路

把所有 f[k][i][j] == true 的关系作为"一条权为 1 的边",得到新图:1→2、1→4、1→3、2→3、2→4、3→4。在新图上从 1 到 4 的最短路 = 1(有直连边 1→4)。

答案:1 秒(直接用 1 条边的"跑路机"从 1 跳到 4;或者用 2 条边的跑路机走 1→2→4,也是 1 秒)

核心代码骨架:

// f[k][i][j]:i → j 是否存在恰好 2^k 条边的路径
for (int k = 1; k <= 32; k++)
    for (int t = 1; t <= n; t++)          // 倍增:拆成两段 2^(k-1)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                if (f[k-1][i][t] && f[k-1][t][j])
                    f[k][i][j] = true;

// 建新图:f[k][i][j] 为真 → 边 i→j 权 1,然后跑最短路(BFS 即可,因为权都是 1)
for (int k = 0; k <=32; k++)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (f[k][i][j]) add(i, j, 1);

易错:① 注意"恰好"两个字 —— 不能是"≤ 2k",必须是"正好 2k";② k 只需枚举到 log₂(n) 左右(路径不重复点时最多 n−1 条边);③ 新图的边权全是 1,BFS 即可,不必上 Dijkstra。这就是拓展3里讲的 P1613 跑路的解法。

🔵 洛谷原题精选(12 道)

按难度从易到难排序,建议先做题号 1~6(Floyd 基础与简单应用),再攻 7~12。

1
B3647
【模板】Floyd入门
2
P1346
电车入门
3
P2419
[USACO08JAN] Cow Contest S普及
4
P2910
[USACO08OPEN] Clear And Present Danger S普及
5
P2888
[USACO07NOV] Cow Hurdles S普及
6
P1522
[USACO2.4] 牛的旅行 Cow Tours普及
7
P1119
灾后重建普及
8
P2865
[USACO06NOV] Roadblocks G提高
9
P6175
无向图的最小环问题提高
10
P2047
[NOI2007] 社交网络提高
11
P1613
跑路提高
12
P1073
[NOIP2009 提高组] 最优贸易提高