LOADING

加载过慢请开启缓存 浏览器默认开启

P9584 城市

2023/8/26 OI 题解

P9584 城市 题解。

1. 题意

给出一棵 \(n\) 个点的树,边有边权,每次询问添加一个点 \(n + 1\) 和一条边 \((u, n + 1)\) 后任意两点之间的距离和。

2. 题解

考虑一条边对答案的贡献,即为通过这条边的路径数 \(\times\) 边权。

为了方便计算,我们将边的贡献转为点的贡献:设 \(a_u\) 表示 \(u\) 到父亲的边权,\(f_u\) 表示子树大小,\(g_u = n - f_u\) 表示子树外大小,则点 \(u\) 的贡献为 \(a_u \cdot f_u \cdot g_u\)

然后考虑加了一条边 \((u, n + 1)\) 之后怎么算,不妨设 \(u\)\(n + 1\) 的父亲。于是 \(u\) 到根路径上每个点的子树大小都要 \(+1\),其余点的子树外大小都要 \(+1\),另外答案还要加上新边的贡献。

这样对于每个询问暴力加边计算是 \(O(nm)\) 的,但其实可以直接预处理。发现新边的贡献比较好算,即 \(n \cdot w\),于是考虑预处理每个点除去新边的贡献的答案。

我们先 DFS 预处理出每个点的 \(a_u\)\(f_u\)\(g_u\),另外因为加了一个点所以预处理子树外大小时 \(g_u = n + 1 - f_u\)

然后我们再 DFS 一遍求出每个点的答案,当遍历到一个点 \(u\)\(f_u \gets f_u + 1\)\(g_u \gets g_u - 1\),同时更新当前答案,回溯时再改回来即可。

最后处理询问的时候,将预处理的答案加上新边贡献即可,然后还要 \(\times 2\) 因为每条路径要算两次。

时间复杂度 \(O(n + m)\)

3. Code

#include <iostream>
#include <vector>
using namespace std;
using i64 = long long;
const int N = 2e5 + 5, P = 998244353;
int n, m, a[N], f[N], g[N], ans[N], cur;
vector<pair<int, int>> G[N];
void dfs1(int u, int fa) {
    f[u] = 1;
    for (auto [v, w] : G[u])
        if (v != fa) a[v] = w, dfs1(v, u), f[u] += f[v];
    g[u] = n + 1 - f[u];
    cur = (cur + (i64)a[u] * f[u] * g[u]) % P;
}
void dfs2(int u, int fa) {
    cur = (cur - (i64)a[u] * f[u] * g[u] + P) % P;
    ++f[u], --g[u];
    cur = (cur + (i64)a[u] * f[u] * g[u]) % P;
    ans[u] = cur;
    for (auto [v, w] : G[u])
        if (v != fa) dfs2(v, u);
    cur = (cur - (i64)a[u] * f[u] * g[u] + P) % P;
    --f[u], ++g[u];
    cur = (cur + (i64)a[u] * f[u] * g[u]) % P;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1, u, v, w; i <= n - 1; i++) {
        cin >> u >> v >> w;
        G[u].emplace_back(v, w);
        G[v].emplace_back(u, w);
    }
    dfs1(1, 0);
    dfs2(1, 0);
    for (int i = 1, u, w; i <= m; i++) {
        cin >> u >> w;
        cout << (ans[u] + (i64)n * w) * 2 % P << '\n';
    }
    return 0;
}