P7516 [省选联考 2021 A/B 卷] 图函数 解题报告


P7516 [省选联考 2021 A/B 卷] 图函数 解题报告

〇、写在前面

大家好!这篇解题报告将带你一步步攻克这道看似复杂的图论问题。

初看题目,f(u, G) 的定义非常绕:它在一个循环里检查点对的连通性,同时又会动态地删除图中的点,这让模拟变得异常困难。更棘手的是,我们还需要在不断删边的过程中反复求解。

遇到这种“动态维护+复杂查询”的题目,我们的第一反应通常是:这个问题有没有更简单的等价描述? 这正是解题的突破口。

一、核心思想:化繁为简,寻找等价条件

让我们先忘记删边,只专注于如何计算一次 \(h(G) = \sum_{u=1}^n f(u, G)\)

\(h(G)\) 是所有 \(f(u, G)\) 的总和。我们可以不按 \(u\) 来计算,而是考虑每一对点 \((u, v)\) 对总答案的贡献。

\(v\) 会在计算 \(f(u, G)\) 时被计入贡献(并被删除),当且仅当在轮到检查 \(v\) 时,图 \(G'\) 中存在 \(u \to v\)\(v \to u\) 的双向路径。这里的 \(G'\) 是 G 删掉了一些在 \(v\) 之前被检查并删除的点(即编号小于 \(v\) 的点)后的图。

经过一番(非常复杂的)推理和大胆猜想,我们可以得出一个惊人的结论:

\(h(G)\) 的值,等于图中满足特定条件的“点对”的数量。

这个特定条件是什么呢?一个无序点对 \(\{u, v\}\)\(u\)\(v\) 可以是同一个点)被称为“有效点对”,当且仅当:
在原图 \(G\) 中,存在 \(u \to v\)\(v \to u\) 的双向路径,且这两条路径上的所有中间节点(不含起点和终点)的编号都必须大于等于 \(\min(u, v)\)

让我们来验证一下这个结论:

  1. 对于任意点 \(u\),点对 \(\{u, u\}\) 显然满足条件(不需要中间节点),所以天然就是有效点对。这对应了在计算 \(f(u, G)\) 时,当检查到 \(v=u\) 时,\(u\) 自身总是和自身连通的,会被计入贡献。这部分总共有 \(n\) 对。
  2. 对于点对 \(\{u, v\}\)\(u \ne v\)(假设 \(u < v\)),它成为有效点对的条件是:在图中,\(u\)\(v\) 互相连通,且路径的中间节点编号都 \(\ge u\)
    • 这恰好对应了在计算 \(f(u,G)\) 时,检查 \(v\) 时,\(u,v\) 互通;以及在计算 \(f(v,G)\) 时,检查 \(u\) 时,\(v,u\) 互通。经过严谨证明可以发现,这两种情况的贡献可以合并,最终等价于我们定义的“有效点对”。

所以,原问题被转化成了:计算图中“有效点对”的总数。

二、解法:倒序加边与“Floyd”思想

现在问题清晰多了。但别忘了,我们还要处理 \(m\) 次删边操作。

“删边”操作通常很难维护。一个经典的技巧是时间倒流:把删边看作是倒序加边。我们从一个空图开始,按 \(m, m-1, \dots, 1\) 的顺序把边加回去。这样,问题就从“每次删一条边求答案”变成了“每次加一条边求答案”。

加边是单调的:如果一对点在某个时刻成为了“有效点对”,那么在后续加入更多边后,它们仍然是“有效点对”。

这启发我们,可以对每一个可能的点对 \(\{u, v\}\),计算出它最早在哪条边加入后,成为了“有效点对”。

比如说,点对 \(\{u, v\}\) 在加入第 \(k\) 条边后首次成为“有效点对”,那么它将对 \(G_0, G_1, \dots, G_{k-1}\) 的答案都产生 1 的贡献。

我们可以用一个差分数组 count[k] 记录“在加入第 \(k\) 条边时,新增了多少个有效点对”。最后,通过后缀和就能求出所有时刻的答案。
ans[i] = ans[i+1] + count[i]

现在,唯一的挑战就是如何高效地计算出每个点对 \(\{u, v\}\) 首次成为“有效点对”的时刻。

三、算法实现:带权 Floyd

如何找到点对 \(\{u, v\}\) (假设 \(u<v\))成为“有效点对”的时刻?我们需要找到这样两条路径:

  1. 一条 \(u \to v\) 的路径,和一条 \(v \to u\) 的路径。
  2. 两条路径上的中间节点编号都 \(\ge u\)
  3. 组成这两条路径的所有边中,编号最小的边的编号要尽可能大(因为我们是倒序加边,编号越大意味着时间越早)。

我们把每条边的“边权”定义为它的输入顺序(即时间)。我们想找的路径,就是所谓的“瓶颈路”或“最大瓶颈路”。

这个问题和经典的 Floyd-Warshall 算法非常相似。Floyd 算法通过枚举中间点 k 来更新点对之间的最短路。我们可以稍作修改:

  1. 定义 dist[i][j] 为:从 \(i\)\(j\) 的所有路径中,路径上“最小边权”的最大值。

    • dist[i][j] 的更新法则为:dist[i][j] = max(dist[i][j], min(dist[i][k], dist[k][j]))。意思是,从 \(i\)\(j\) 可以直接走,也可以经过 \(k\)。如果经过 \(k\),那么整条路径的瓶颈取决于 \(i \to k\)\(k \to j\) 这两段中较差的那一段。
  2. 为了处理“中间节点编号 \(\ge \min(u,v)\)”的限制,我们让 Floyd 的中转点 kn1 倒序枚举

完整的算法流程如下:

  1. 初始化

    • dist[i][j] 初始为 0。对于输入的第 t 条边 x -> y,我们设置 dist[x][y] = t
    • count 数组全部初始化为 0。
  2. 核心计算 (倒序 Floyd)
    我们从 k = n1 循环:

    • 在更新 dist 之前dist[i][j] 的值代表了从 \(i\)\(j\),只使用编号 \(>k\) 的点作为中间节点的最大瓶颈。
    • 此时,我们可以计算所有与 k 相关的点对 \(\{i, k\}\)(其中 \(i>k\))的“有效时刻”。
      • 点对 \(\{i, k\}\) 成为“有效点对”的条件是:\(i, k\) 互通,且中间节点编号 \(\ge k\)。由于我们此时只允许 \(>k\) 的中间节点,这个条件是满足的。
      • 它们互通的时刻(即最早加入的边的编号)就是 min(dist[i][k], dist[k][i])
      • t = min(dist[i][k], dist[k][i])。我们在 count[t] 上加一,表示在 t 时刻,有一个新的点对 \(\{i, k\}\) 达标了。
    • 更新 dist 矩阵
      • for i = 1 to n, for j = 1 to n:
      • dist[i][j] = max(dist[i][j], min(dist[i][k], dist[k][j]))
      • 这样,dist 矩阵现在就包含了允许使用 k 作为中转点的信息,为下一轮循环(k-1)做好了准备。
  3. 统计答案

    • 初始时,空图(\(G_m\))有 \(n\) 个有效点对(所有 \(\{i, i\}\))。所以 ans[m+1] = n
    • i = m1 倒序计算:ans[i] = ans[i+1] + count[i]
    • ans[1] 就是完整图 \(G\) 的答案。

这个算法的时间复杂度是 \(O(n^3 + m)\),其中 \(O(n^3)\) 来自于 Floyd 算法, \(O(m)\) 来自于初始化和最后的统计,可以通过所有数据点。

四、代码解析

这里附上题解中简洁的代码,并加上注释方便理解。

#include <stdio.h>
#include <algorithm> // 为了使用 max 和 min

using namespace std;

const int N = 1003;  // 点数上限
const int M = 200003; // 边数上限

int n, m;
int u[M], v[M]; // 存储输入的边
int f[N][N]; // 即报告中的 dist 矩阵,f[i][j] 记录 i->j 路径的最大瓶颈

int g[M];    // 即报告中的 count 数组,g[t] 记录在 t 时刻新增的有效点对数量
long long ans[M]; // 答案数组

int main() {
    scanf("%d %d", &n, &m);
    // 初始化 f 矩阵,读入边,记录边的“时刻”
    for (int i = 1; i <= m; i++) {
        scanf("%d %d", &u[i], &v[i]);
        f[u[i]][v[i]] = i; 
    }

    // Floyd 核心部分,中转点 k 从 n 到 1 倒序枚举
    for (int k = n; k >= 1; k--) {
        // 1. 计算与 k 相关的点对的贡献
        // 对于任意 i > k,检查点对 {i, k}
        // 此时 f[i][k] 和 f[k][i] 代表只经过 >k 的中转点的路径瓶颈
        for (int i = k + 1; i <= n; i++) {
            // 点对 {i, k} 成为有效点对的时刻是 min(f[i][k], f[k][i])
            // 如果二者有一个为0,表示不连通,min下来也是0,不产生贡献
            g[min(f[i][k], f[k][i])]++;
        }
        
        // 2. 用 k 作为中转点,更新所有点对的路径瓶颈
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                // i->j 的新路径可以经过 k
                // 新路径的瓶颈是 i->k 和 k->j 两段中较小的值
                // 我们在原有路径和新路径中取一个瓶颈更大的
                f[i][j] = max(f[i][j], min(f[i][k], f[k][j]));
            }
        }
    }

    // 3. 倒序统计最终答案
    // ans[m+1] 对应的是删掉所有边(m条)后的图 Gm
    // 此时只有 n 个 {i,i} 点对是有效的
    ans[m + 1] = n;
    for (int i = m; i >= 1; i--) {
        // ans[i] 是图 Gi-1 的答案(删了 1~i-1 条边)
        // 它等于图 Gi 的答案 ans[i+1] 加上在第 i 时刻新产生的有效点对数 g[i]
        ans[i] = ans[i + 1] + g[i];
    }

    // 输出所有答案
    for (int i = 1; i <= m + 1; i++) {
        printf("%lld ", ans[i]);
    }
    puts("");

    return 0;
}

五、总结

这道题的精髓在于两步漂亮的转化:

  1. 将原问题中复杂的、带动态删除的函数求和问题,转化为一个静态的、组合计数的“有效点对”问题。
  2. 将多次删边查询,通过“时间倒流”转化为单次加边过程中的贡献累加问题。

最终,我们用一个巧妙的、倒序枚举中转点的 Floyd 算法,在 \(O(n^3)\) 的时间内解决了所有子问题。希望这篇报告能帮助你理解这道题的巧妙之处!

posted @ 2025-07-10 19:43  surprise_ying  阅读(19)  评论(0)    收藏  举报