Codeforces 295B 从逆序加点到 Floyd 的中间点状态
先明确状态与不变量,再对照手算、证明、边界和实现。
章节速览7 节 · 展开选择
删掉一个顶点,经过它的最短路可能全部失效。仅从原来的最短距离里减去一些数,并不能找回绕行路线。若所有删除顺序提前给出,就可以反过来问:每次加入一个顶点,哪些路径第一次变得可用?
这篇承接 Dijkstra 的路径代价与动态规划的状态定义,把 Floyd 的外层循环解释成“逐个放开中间点”。这里不是在线维护任意增删图,而是利用已经知道的完整操作顺序。
官方题目与朴素代价
295B · Greg and Graph 的官方难度为 1700,标签为 dp、graphs、shortest paths。输入是完全有向图的邻接矩阵,1≤n≤500,非对角边权 1…100000,对角为零;两个方向的权重可以不同。删除序列是顶点的一个排列。每次删除之前,输出当前所有有序点对的最短距离之和。元信息及范围核对于 2026-10-21。
如果对每个剩余图重新运行 Floyd,总时间为 O(n⁴)。若只保存最初的全图距离,删除后又无法知道最短路是否偷偷使用了已经消失的点。难点是限制路径的内部顶点,而非只限制求和的端点。
反转操作,先明确允许什么
删除顺序记为 x₁,…,xₙ。逆序依次加入 xₙ,…,x₁;当刚加入 xₜ 时,活动点集合恰好等于正序第 t 次删除之前的剩余集合。
定义 d[i][j]:从 i 到 j、内部顶点只允许取自已加入集合 S 的最短距离。i、j 本身可以尚未加入。端点与内部顶点是两种不同权限。
初始 S 为空,没有内部顶点,故 d=a。加入 k 后,对所有 i、j 执行:
d[i][j] = min(d[i][j], d[i][k] + d[k][j])。
更新完成后,只对 i、j 都在 S 中的有序点对求和。对角项为零,包含它们也不影响结果。
flowchart LR A[已知全部删除顺序] --> B[逆序加入一个顶点 k] B --> C[允许 k 成为内部顶点] C --> D[更新全部端点对] D --> E[只累计活动端点对] E --> F[存到原删除位置]
为什么一次松弛足够
归纳假设:加入 k 前,所有 d[i][j] 都已是内部顶点限制在 S 的最优距离。
加入 k 后,最短路径分两类:不经过 k 时,旧距离已经最优;经过 k 时,因为权重为正,可以去掉环并取一条简单最短路径,k 只出现一次。将路径在 k 切开,两段内部顶点都在旧 S 内,因此代价至少为旧 d[i][k]+d[k][j]。反过来,两段最短路拼接给出可行行走,若有重复顶点可去环,不会增加代价。所以两类取最小恰好得到新状态。
原地更新也成立:d[k][k]=0 且无负环,经过 k 不会让第 k 行或第 k 列变得更短。因此本轮使用的 d[i][k]、d[k][j] 保持旧值,等价于分层转移。k 必须是最外层循环;i、j 的先后可交换。
这个不变量覆盖未激活端点。等到一个端点真正加入时,它已经积累了此前所有中间点的路径信息,无需补做旧层。
三个顶点的完整手算
自拟矩阵如下,行是起点、列是终点:
| a | 1 | 2 | 3 |
|---|---|---|---|
| 1 | 0 | 2 | 10 |
| 2 | 8 | 0 | 3 |
| 3 | 4 | 9 | 0 |
删除顺序为 [1,3,2],加入顺序是 [2,3,1]。
| 加入点 | 活动集合 | 本轮发生的缩短 | 活动有序点对距离和 | 答案位置 |
|---|---|---|---|---|
| 2 | {2} | d₁₃:10→2+3=5 | 0 | 第 3 项 |
| 3 | {2,3} | d₂₁:8→3+4=7 | 3+9=12 | 第 2 项 |
| 1 | {1,2,3} | d₃₂:9→4+2=6 | 2+5+7+3+4+6=27 | 第 1 项 |
输出为 27 12 0。第二项的 3→2 距离仍是 9;路径 3→1→2 虽然只花 6,但那时点 1 已经被删除,不能使用。
为什么不能只更新活动端点
上例首次加入 2 时,1 与 3 都未激活,却必须把 d[1][3] 从 10 更新到 5。若跳过它,之后加入 3 时仍拿着旧的 10;同样还会漏掉经由 3 对 d[2][1] 的更新。最后加入 1 的转移只能尝试经由 1,无法重新补上错过的中间点,最终错误得到总和 33。
flowchart TD A[距离矩阵:全部端点] --> B[中间点权限由 S 限制] A --> C[求和对象:活动端点] B --> D[每轮 n 平方次松弛] C --> E[只计当前剩余图] D --> F[别把求和筛选搬到更新循环] E --> F
自检:为何最终全图距离不能直接用于中间答案?
最终距离可能经过尚未加入的中间点。例子里最终 d₃₂=6,但只剩 {2,3} 时应为 9。端点存在并不表示路径内部点存在。
完整 C++17 实现
1 |
|
每轮一次 n² 松弛和至多 n² 求和,共 O(n³) 时间,矩阵 O(n²) 空间。完全图保证两点间总有直接边,所以有限距离不超过 100000,代码无需无穷大哨兵;有序点对之和可达 500×499×100000=24950000000,必须用 long long。这里连矩阵也统一用 64 位,便于安全相加。
边界与验证
| 检查 | 应有行为 | 常见错误 |
|---|---|---|
| n=1 | 唯一答案为 0 | 漏掉最后单点状态 |
| 非对称边权 | 两个方向分别计算 | 擅自把矩阵对称化 |
| 删除前求和 | 加入 xₜ 后存 answer[t] | 少算一个顶点 |
| 尚未加入的端点 | 仍然参与距离更新 | 只更新活动端点 |
| 500 个点、最大权重 | 总和 24950000000 | 使用 int 累加 |
| 任意在线删除 | 本文方法不直接适用 | 忽略离线前提 |
验证程序 tests/verify-reverse-floyd-article.cjs 提取正文代码实际编译,用每个正序剩余图上重新运行的逐源稠密 Dijkstra 独立核对全部答案;同时检查上面的手算与“只更新活动端点”反例、单点、非对称、全部删除排列的小图和 n=500 的 64 位边界。测试覆盖的范围以程序实际输出为准。
继续比较 0-1 BFS:它利用边权限制加速单源搜索;本题则利用操作顺序,让全点对动态规划只增加状态权限。二者节省工作的依据不同。