P1119 灾后重建 — 增量 Floyd-Warshall

这是第一次做OI题目,问了claude code不少东西,总结了一下:

题目概述

  • 来源: 洛谷 P1119,难度 普及+/提高
  • 题意: N 个村庄 (0..N-1),M 条双向公路。第 i 个村庄在第 t[i] 天修复完成。只有两端村庄均已修复的公路才能使用。Q 次询问 (x, y, T):第 T 天从 x 到 y 的最短路长度。若起终点未修复或无路径,输出 -1。
  • 关键约束: N ≤ 200, Q ≤ 50000, t[i] 单调不减, 询问时间 T 单调不减, 边权 w ≤ 10000

算法分析

为什么是 Floyd

N ≤ 200 允许 O(N³) 预处理。Floyd-Warshall 外层循环”for k in 0..N-1″的语义是:以 k 为中间顶点,松弛所有 (i,j) 对。等价于逐步扩大”允许使用的中间顶点集合” S = {0, 1, …, k-1}。

本题中 S 的扩大不由编号顺序驱动,而由修复时间驱动:

  • 询问时间 T 单调不减
  • t[i] 也单调不减
  • 每当 T 推进,解锁所有 t[i] ≤ T 的村庄,用它们作为中间顶点跑一层 Floyd

DP 状态定义

f(k, i, j) = 从 i 到 j 的最短路径,且路径上所有中间顶点 ∈ {0, 1, …, k-1}

递推式: f(k+1, i, j) = min(f(k, i, j), f(k, i, k) + f(k, k, j))

空间优化(原地覆盖)的可行性:因为 f(k, i, k) = f(k+1, i, k)(k 是终点时,新增 k 作为中间顶点不影响以 k 为端点的路径),所以第 k 轮更新时 dist[i][k] 和 dist[k][j] 是稳定的,可以安全覆盖。

复杂度

O(N³ + Q),N=200 时约 8×10⁶ 次松弛操作。

中间顶点顺序无关性的证明

定理:逐个添加中间顶点的顺序不影响 Floyd-Warshall 的最终结果。

分析学视角:设 Pk = {所有中间顶点来自 {v₀, v₁, …, v{k-1}} 的路径}。Floyd 维护的量是:

$$d_k(i,j) = \inf{\text{length}(P) \mid P \in \mathcal{P}_k, P \text{ 从 } i \text{ 到 } j}$$

关键观察:

  1. 单调性:Pk ⊆ P{k+1},于是 d_{k+1} ≤ d_k(infimum over expanding set 单调不增),且有下界(路径长度非负),序列收敛。
  2. 最终穷尽:当 k = n 时,P_n = P(全体路径),d_n = 全局最短路径。

顺序无关的核心:infimum over a set 只依赖于最终的集合是什么,不依赖于用怎样的嵌套子集序列去逼近它。无论走 {a}→{a,b}→{a,b,c} 还是 {c}→{b,c}→{a,b,c},到达全集的瞬间,考虑的候选路径集合完全相同,infimum 自然相同。

松弛算子的合流性:每步松弛 $$T_k(d)(i,j) $$= $$min(d(i,j), d(i,k)+d(k,j)) $$是单调算子。施加顺序不同的算子序列对应不同的中间状态,但最终收敛到同一个不动点(Kleene 闭包 / tropical semiring 下的传递闭包)。这类似于重写系统中的 Church-Rosser(合流)性质。

P1119 推论:村庄激活顺序按修复时间而非编号,但由上述定理,最终 D[V] 正确。每个查询要求 D[{已修复村庄}][x][y],恰好是算法在查询时维护的 dist[x][y]。

AC 代码

/**
 * P1119 灾后重建 — 增量 Floyd-Warshall
 * =====================================
 * 核心思想:Floyd 外层循环的语义是"逐步扩大允许使用的中间顶点集合"。
 * 本题中集合扩大不由编号顺序驱动,而由修复时间驱动——
 * 询问时间和修复时间均单调不减,两个指针同步向前,
 * 每个村庄恰好被作为中间顶点处理一次。
 *
 * 复杂度:O(N³ + Q),N=200 时约 8×10⁶ 次松弛,稳过。
 */
#include <bits/stdc++.h>
using namespace std;

const int INF = 0x3f3f3f3f;   // 两倍不溢出 int
const int MAXN = 205;          // N ≤ 200

int G[MAXN][MAXN];  // 距离矩阵
int N, M, Q;

// 以顶点 k 为中间点,跑一层 Floyd 松弛
void floyd(int k) {
    for (int i = 0; i < N; i++) {
        if (G[i][k] == INF) continue;
        for (int j = 0; j < N; j++) {
            if (G[k][j] == INF) continue;
            if (G[i][k] + G[k][j] < G[i][j]) {
                G[i][j] = G[i][k] + G[k][j];
            }
        }
    }
}

int main() {
    cin >> N >> M;

    int t[MAXN];
    for (int i = 0; i < N; i++) cin >> t[i];

    // 初始化距离矩阵:全 INF,对角 0
    memset(G, 0x3f, sizeof(G));
    for (int i = 0; i < N; i++) G[i][i] = 0;

    // 读入双向边
    int u, v, w;
    for (int i = 0; i < M; i++) {
        cin >> u >> v >> w;
        G[u][v] = w;
        G[v][u] = w;
    }

    cin >> Q;

    int now = 0;  // 下一个待解锁的村庄编号
    int x, y, T;

    while (Q--) {
        cin >> x >> y >> T;

        // 解锁所有修复时间 ≤ T 的村庄,逐个跑一层 Floyd
        while (now < N && t[now] <= T) {
            floyd(now);
            now++;
        }

        // 回答询问
        if (t[x] > T || t[y] > T)
            cout << -1 << '\n';
        else if (G[x][y] == INF)
            cout << -1 << '\n';
        else
            cout << G[x][y] << '\n';
    }

    return 0;
}

OI 经验总结(首次做题)

暴露的问题

  • 变量命名冲突:局部变量 t 覆盖了数组 t[MAXN],导致 t[now] 编译报错
  • 运算符混淆:cout >> 写成 <<>> 是 cin 的)
  • 逻辑结构:查询输出放在循环外,导致只输出最后一次结果
  • 算法积累不足:虽然理解了 Floyd 的 DP 本质,但第一次见到增量变体需要引导

收获

  • Floyd-Warshall 外层循环 = 逐个激活中间顶点 = expanding set 上的 infimum
  • OI 中 memset(..., 0x3f, ...) 初始化 INF 的技巧及原理
  • 0x3f3f3f3f 的选择:加法不溢出,memset 一步到位
  • OI 赛制特点:允许多次提交但看不到评测数据,需自己造边界数据测试
  • 变量名不要和类型名/函数名/已声明数组冲突

适用场景

  • 动态加点 + 全源最短路查询
  • 顶点按某种单调顺序逐步可用
  • N 较小 (≤ 500),允许 O(N³) 预处理
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇