[APIO2018] 铁人两项

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

圆方树 + 赋权 + 树上路径贡献计数:方点权=BCC大小,圆点-1,树DP统计每点被(s,f)路径经过次数

OJ: luogu

题目 ID: P4630

难度:提高+/省选-

标签:圆方树点双连通分量树形DP路径计数

创建: 2026-10-23 12:51

更新: 2026-10-23 13:26

形式化题目

给定无向简单图 G=(V,E)G=(V,E),∣V∣=n≤105|V|=n\le 10^5,∣E∣=m≤2×105|E|=m\le 2\times 10^5。

统计有序三元组 (s,c,f)(s,c,f) 的数量,满足:

  • s,c,fs,c,f 两两互不相同;
  • 存在一条从 ss 到 ff 的简单路径(不重复顶点),且 cc 在该路径上。

正解

思路

在一个大小 ≥3\ge 3 的点双连通分量(BCC)中,对于任意两个不同顶点 x,yx,y 及块内任意 z≠x,yz\ne x,y,存在简单路径 x→z→yx\to z\to y。

这一性质允许我们把「所有 ss-ff 简单路径上可作为 cc 的顶点」按 BCC 聚合。

圆方树建模

  1. 用 Tarjan 求出原图所有点双连通分量。
  2. 每个 BCC 新建一个方点(若 ∣BCC∣≥2|BCC|\ge 2)。
  3. 圆点 == 原图顶点 1..n1..n,方点编号 n+1…n+1\dots。
  4. 每个 BCC 的圆点都与对应方点连边。

赋权规则:

  • 方点权值 w=BCC 内圆点数w=\text{BCC 内圆点数};
  • 圆点权值 w=−1w=-1。

在圆方树上,任意两圆点 u,vu,v 之间路径的点权和,恰好等于原图中 uu 到 vv 所有简单路径上可作为 cc 的顶点总数(已自动扣除端点与割点重复)。

树上贡献计数

答案即所有圆点对 (s,f)(s,f) 的路径权值之和。

转化为对树上每个节点 uu,统计「有多少对圆点 (s,f)(s,f) 的路径经过 uu」,记 cnt[u]cnt[u]。

用两次 DFS 实现:

  • 第一次:计算子树内圆点数 size[u]size[u]。
  • 第二次:对每个 uu 统计
    • 两端都在子树的贡献:∑size[v]×(size[u]−size[v])\sum size[v]\times(size[u]-size[v])
    • 跨过 uu 的贡献:size[u]×(N−size[u])size[u]\times(N-size[u])
    • 其中 NN 为当前连通分量内圆点总数。

最终答案:ans=∑uw[u]⋅cnt[u]⋅2\displaystyle ans=\sum_u w[u]\cdot cnt[u]\cdot 2(两个方向)。

代码

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-10-23 12:51
 * update_at: 2026-10-23 13:26
 */
// P4630 [APIO2018] 铁人两项
// 圆方树 + 树上路径贡献计数
// 核心:点双性质 → 方点赋权 BCC 大小,圆点 -1 → 树 DP 统计每点被 (s,f) 路径经过次数
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 400010;  // n + bcc 约 2n ~ 3n 安全

int n, m;
vector<int> g[MAXN];      // 原图邻接表,1..n

// Tarjan 求 BCC + 同时建圆方树
int dfn[MAXN], low[MAXN], timer;
int stk[MAXN], top;
int tot;                  // 当前节点编号,初始 = n,方点从 n+1 开始
vector<int> e[MAXN];      // 圆方树邻接表(复用 1..tot)
int w[MAXN];              // 权值:圆点 -1,方点 = BCC 内圆点数
int f[MAXN];              // 树 DP:子树圆点数
ll ans;
int sz;                   // 当前连通分量内圆点总数(每次新分量重置)

void tarjan(int u) {
    ++sz;
    dfn[u] = low[u] = ++timer;
    stk[++top] = u;
    for (int v : g[u]) {
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
            if (low[v] == dfn[u]) {
                // 找到一个以 u 为根的 BCC
                ++tot;
                w[tot] = 0;
                int x;
                do {
                    x = stk[top--];
                    e[tot].push_back(x);
                    e[x].push_back(tot);
                    ++w[tot];
                } while (x != v);
                // 割点 u 也要连到这个方点
                e[tot].push_back(u);
                e[u].push_back(tot);
                ++w[tot];
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

void dfs(int u, int fa) {
    f[u] = (u <= n ? 1 : 0);  // 只统计圆点作为 s/f
    for (int v : e[u]) {
        if (v == fa) continue;
        dfs(v, u);
        // 两端都在不同子树的贡献(经过 u)
        ans += 2LL * w[u] * f[u] * f[v];
        f[u] += f[v];
    }
    // 一端在子树、一端在子树外的贡献
    ans += 2LL * w[u] * f[u] * (sz - f[u]);
}

void solve() {
    // 初始化
    timer = 0;
    tot = n;
    top = 0;
    ans = 0;
    for (int i = 1; i <= n; ++i) {
        dfn[i] = low[i] = 0;
        w[i] = -1;          // 圆点初始 -1
        g[i].clear();
        e[i].clear();
    }
    for (int i = n+1; i < MAXN; ++i) {
        e[i].clear();
        w[i] = 0;
        dfn[i] = low[i] = 0;
    }

    // 读边
    for (int i = 0; i < m; ++i) {
        int a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }

    // 对每个连通分量做 Tarjan + DP
    for (int i = 1; i <= n; ++i) {
        if (!dfn[i]) {
            sz = 0;
            tarjan(i);
            if (top > 0) --top;
            dfs(i, 0);
        }
    }

    cout << ans << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    solve();
    return 0;
}

复杂度

时间 O(n+m)O(n+m),空间 O(n+m)O(n+m)。

总结

本题核心是将「简单路径上可作为中转点的顶点」用圆方树聚合:方点代表一个 BCC,权值即块大小;圆点权 −1-1 扣除端点与重复割点。把原问题转化为圆方树上「圆点对路径权值和」,用标准树 DP 在 O(n)O(n) 内完成计数。