灾后重建

GitHub跳转原题关系图返回列表

题意

村庄按时间修复,询问某天只经过已修复村庄的最短路。

思路

直接做法:每个询问重算一次。

对每个询问 tt,只保留重建时间不晚于 tt 的村庄与它们之间的公路,跑一次 Floyd,读出 d[x][y]。这个做法完全符合题意,但代价是 Q⋅N3Q \cdot N^3:

Q⋅N3=5×104×8×106=4×1011Q \cdot N^3 = 5\times 10^4 \times 8\times 10^6 = 4\times 10^{11}

必超时。卡点在于:相邻两次询问之间图几乎没变——村庄只会被修复,不会被摧毁,所以“已开放的点集”只增不减,全额重算把 99.9%99.9\% 相同的计算又做了一遍。

转折:看清 Floyd 的 middle 到底是什么

Floyd 的三层循环通常会写成一个纯粹的循环变量,但它有明确语义:

d(k)[i][j]=只允许经过前 k 个点作为中转时,i 到 j 的最短路d^{(k)}[i][j] = \text{只允许经过前 } k \text{ 个点作为中转时,} i \text{ 到 } j \text{ 的最短路}

也就是说,middle 不是在“迭代次数”,而在控制允许使用的中转点集合。跑完第 kk 轮后,矩阵里就是“只用前 kk 批点中转”的最短路。

把这两件事对照起来,就会发现它们长得一模一样:

时间轴 Floyd 的 middle
时间 tt,已修复的村庄集合 已跑完的 k 轮,已允许的中转点集合
只增不减 只增不减
下个时刻新增一批村庄 下轮 k 新增一个中转点

于是 middle 可以跟着时间走:把村庄按重建时间排好,询问按时间排好,用一个指针记录“当前已开放到哪个村庄”;每开放一个新村庄就让它当一次 middle,做一轮加点转移。

几个容易搞混的点

middle 必须“从小到大”吗? 不一定。Floyd 的 k 顺序其实是任意的,只要每个点恰好当一次 k,最终结果都是正确的全源最短路(中间状态不同,不动点相同)。

那为什么这里没选编号顺序?因为我们要在扫描过程中直接回答询问,所以中间状态必须对应一个真实时刻。选时间序,第 kk 轮后的矩阵恰好就是第 tkt_k 天的答案;选别的顺序,中间状态并无现实含义,就问不了。

复用旧矩阵为什么不会出错? 这是必须说清的一点,不是“看起来应该没问题”。新增中转点只会让最短路变短或不变(多一条可选绕路),所以旧矩阵里每一格都是新矩阵的上界,不会成为错的答案。随后那轮 middle 转移会把可以变短的格子全部修正好。

这个“单调收紧⇒复用安全”的论证一旦失效,整道题的做法就崩,见后文边界。

总代价为什么降到 N3N^3? 因为所有询问共享同一条 middle 序列。N 个点在整个程序里各只当一次 k,所以 k 循环总计只跑 NN 轮,而不是每个询问跑 NN 轮。这是均摊:每个询问只花 O(1)O(1) 查表。

边界

  • 端点未修复:tx>tt_x > t 或 ty>tt_y > t 时直接输出 −1-1,哪怕矩阵里已经有值(那个值可能经过了尚未修复的点)。
  • 不连通:d[x][y] 仍为 INF 时输出 −1-1。
  • 为什么依赖 tt 不降:单向推进的指针要求询问时间非降。若询问乱序,需要先离线排序再回答(本题题面已保证非降,直接用即可)。
  • 若村庄能反复摧毁重建:已开放集合不再单调,旧矩阵可能变得过于乐观,这套增量做法立即失效,必须换成分治或线段树分治等结构。

Python 知识

  • 单调询问让一个 activated 指针代替每次重新计算。
  • 缓存矩阵行引用降低三重循环开销。
  • 10**18 作为整数无穷大,无浮点比较问题。
  • 多层条件用布尔与短路求值压成一行:ready[start] <= time and ready[end] <= time and value < infinity,写入前就能把 −1-1 的三种情形全筛掉。

代码

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-07-17 01:40
# update_at: 2026-10-20 16:20
#
# main.py:灾后重建。村庄按时间修复,询问某天只经过已修复村庄的最短路。
# 朴素做法是对每个询问重建图再跑一次 Floyd,代价 Q*N^3 必超时。
# 关键:Floyd 的中间点 middle 不是在“迭代次数”,而是在控制“允许使用的中转点集合”,
# 已经跑完 k 轮就代表允许经过前 k 批点中转。而村庄只会修复不会摧毁,
# 所以已开放集合只增不减 —— 这两件事结构相同,就把 middle 交给时间轴:
#   村庄按修复时间排好、询问按时间排好,用指针 activated 记录开放到哪,
#   每开放一个新村庄就让它当一次 middle,做一轮加点转移。
# N 个点全程各只当一次中间点,总代价 O(N^3 + Q),而不是 O(Q*N^3)。
# 复用旧矩阵的前提是单调收紧:新增中转点只会让最短路变短或不变,
# 所以旧值恒为新值的上界,不会成为错误答案。

import sys

INF = 10**18  # 整数哨兵,比任何合法路径都大,无浮点比较问题


def solve() -> None:
    # 一次性读入全部整数再顺序消费:比逐行 input() 少大量 IO 与解析开销
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n, m = next(data), next(data)

    ready = [next(data) for _ in range(n)]  # 题面保证 ready 非降序

    dist = [[INF] * n for _ in range(n)]
    for i in range(n):
        dist[i][i] = 0

    for _ in range(m):
        u, v, w = next(data), next(data), next(data)
        # 公路是双向的;题面保证任意两点之间只有一条路,不必取 min
        dist[u][v] = dist[v][u] = w

    activated = 0  # 已经放开到哪个村庄,值就是“下一次要用的 middle”
    answers: list[str] = []

    Q = next(data)
    for _ in range(Q):
        start, end, day = next(data), next(data), next(data)

        # 把重建时间不晚于今天、但还没当中间点用过的村庄依次放开。
        # 每放开一个就做一轮 Floyd 加点转移:允许经过它的路径可能更短。
        while activated < n and ready[activated] <= day:
            middle = activated
            through = dist[middle]  # dist[middle][*],缓存行引用
            for x in range(n):
                row = dist[x]
                base = row[middle]  # dist[x][middle]
                if base == INF:
                    continue  # x 到 middle 不可达,这一轮对 x 没有改善
                for y in range(n):
                    candidate = base + through[y]
                    if candidate < row[y]:
                        row[y] = candidate
            activated += 1

        # 三种输出 -1 的情形合成一个判断:端点未修复 / 距离仍无穷
        value = dist[start][end]
        reachable = ready[start] <= day and ready[end] <= day and value < INF
        answers.append(str(value if reachable else -1))

    print("\n".join(answers))


if __name__ == "__main__":
    solve()

原有 C++ 版本仍保留:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-17 01:40
 * update_at: 2026-10-20 15:45
 */
// 灾后重建:村庄按时间修复,询问某天只经过已修复村庄的最短路。
// 朴素做法是对每个询问重建图再跑一次 Floyd,代价 Q*N^3 必超时。
// 关键:Floyd 的中间点 k 不是在"迭代次数",而是在控制"允许使用的中转点集合",
// 跑完 k 轮就代表允许经过前 k 批点中转。而村庄只会修复不会摧毁,
// 所以已开放集合只增不减 —— 这两件事结构相同,就把 k 交给时间轴:
//   题面保证 t_i 非降、询问的 t 也非降,所以可以边读询问边往前推进指针,
//   每放开一个新村庄就让它当一次 k,做一轮 Floyd 加点转移。
// N 个点全程各只当一次中间点,总代价 O(N^3 + Q),而不是 O(Q*N^3)。
// 复用旧矩阵的前提是单调收紧:新增中转点只会让最短路变短或不变,
// 所以旧值恒为新值的上界,不会成为错误答案。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 205;              // 村庄数上界 N <= 200
const long long INF = (1LL << 60);

int n, m;
int ready_day[MAXN];               // ready_day[i]:i 号村庄第几天修复完成
long long dist_node[MAXN][MAXN];   // dist_node[i][j]:只用已开放村庄中转的最短路

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> ready_day[i];  // 题面保证非降序

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

    for (int i = 0; i < m; i++) {
        int u, v;
        long long w;
        cin >> u >> v >> w;
        // 公路双向;题面保证任意两点间只有一条路,直接赋值即可
        dist_node[u][v] = w;
        dist_node[v][u] = w;
    }

    int activated = 0;  // 已经放开到哪个村庄,它同时也是"下一次要用的 k"
    int query_cnt;
    cin >> query_cnt;

    for (int i = 0; i < query_cnt; i++) {
        int start, end, day;
        cin >> start >> end >> day;

        // 把修复时间不晚于今天、但还没当过中间点的村庄依次放开。
        // 每放开一个就做一轮 Floyd 加点转移:允许经过它的路径可能更短。
        while (activated < n && ready_day[activated] <= day) {
            int middle = activated;
            for (int x = 0; x < n; x++) {
                if (dist_node[x][middle] == INF) continue;  // x 到 middle 不可达,这一轮对 x 无改善
                long long base = dist_node[x][middle];
                for (int y = 0; y < n; y++) {
                    long long candidate = base + dist_node[middle][y];
                    if (candidate < dist_node[x][y]) {
                        dist_node[x][y] = candidate;
                    }
                }
            }
            activated++;
        }

        // 三种输出 -1 的情形:起点未修复、终点未修复、仍然不可达
        if (ready_day[start] > day || ready_day[end] > day || dist_node[start][end] == INF) {
            cout << -1 << '\n';
        } else {
            cout << dist_node[start][end] << '\n';
        }
    }

    return 0;
}

复杂度

Floyd 的 middle 一共只被当作中转点 NN 次,所以总时间 O(N3+Q)O(N^3 + Q),空间 O(N2)O(N^2)。

对比未优化的 O(Q⋅N3)O(Q \cdot N^3),差距是 QQ 倍(本题 5×1045\times 10^4 倍)。实测 N=200N=200 完全图、Q=50000Q=50000 的最坏数据,Python 版约 1.7 秒。

总结

Floyd 的 middle 层不是在“迭代次数”,而是在控制“允许使用的中转点集合”。己知这个集合只增不减,就把 middle 交给时间轴:N 个点全程各当一次中间点,总代价从 Q⋅N3Q \cdot N^3 降到 N3N^3。

复用旧矩阵的前提是单调收紧:新增中转点只会让距离变短或不变。一旦集合不再单调(比如村庄可以反复摧毁),这个前提就不成立。