对树链剖分的爱 题解

前言

题目链接:洛谷

题意简述

给出一棵 \(n\) 个节点以 \(1\) 为根的有根树。对于第 \(2\leq i\leq n\) 个节点,其父亲 \(f_i\)\([l_i,r_i]\) 中均匀随机。每个树的边有边权,初始为 \(0\)

现在有 \(m\) 次操作,第 \(i\) 次操作表示将 \((u_i,v_i)\) 的路径上所有的边的权值统一加上 \(w_i\)\(m\) 次操作结束后,对于所有 \(i=2\sim n\),求 \((i,f_i)\) 边上权值的期望,对 \(998244353\) 取模。

\(1\leq l_i\leq r_i<i\)\(n \leq 5 \times 10^3\)\(m \leq 10^5\)。(原题 \(m \leq 5 \times 10^3\)。)

题目分析

这道题树的形态十分鬼畜,平常树上那一套都不管用了,于是变得无从下手。似乎能下手的只有随机等概率跳父亲这一操作。

修改之间独立,考虑某一次修改 \((u_i, v_i, w_i)\),我们要对 \(u_i \sim v_i\) 上,除了 \(\operatorname{lca}(u_i, v_i)\) 处的答案都加上 \(w_i\) 与这种树的形态的概率之积,即期望。联想到跳 \(\operatorname{lca}\) 的过程,在 \(u = v\) 之前,每次选取深度较深的结点,往上等概率地跳一次父亲。

对于本题,深度不确定,但是我们只用保证不会跳过 \(\operatorname{lca}\) 即可。什么情况下会跳过呢?就是 \(u\) 跳到了可能成为 \(\operatorname{lca}\) 的点,下一步不能让它往上跳。显然,可能成为 \(\operatorname{lca}\) 的点是 \(u\)\(v\) 当中较小的一个。

即,假设 \(u < v\)\(ans_v \gets ans_v + w\)\(v \gets f_v \in [l_v, r_v]\)\(w \gets \cfrac{w}{r_v - l_v + 1}\)

发现,一对 \((u, v)\) 会被多次访问,与其多次重复操作,不妨先将其记下来,然后一起往上更新。数据范围允许我们设 \(f_{u, v}\) 表示当前已经跳到了 \((u, v)\),对 \(u \sim v\) 这条树链修改的期望。类比上面的转移,有:

\[\forall k \in [l_v, r_v], f_{u, k} \gets f_{u, k} + \cfrac{f_{u, v}}{r_v - l_v + 1} \]

初始 \(f_{u_i, v_i} = w_i\)。我们要保证转移时刻有意义,即保证 \(u < v\)。上式若出现 \(k = u\) 的状态应舍弃;若出现 \(k < u\),要把 \(f_{u, k}\) 变成 \(f_{k, u}\)。由于修改独立,我们在 \(m\) 次操作后一起转移。

至此,时间复杂度是 \(\Theta(n^3 + m)\),考虑优化。

发现加上的数都是相同的,设 \(C = \cfrac{f_{u, v}}{r_v - l_v + 1}\),对于 \(k < u\) 的情况和 \(k > u\) 的情况分别拎出来观察:

\[\forall k \in [l_v, \min \lbrace r_v, u - 1 \rbrace], f_{k, u} \gets f_{k, u} + C \]

\[\forall k \in [\max \lbrace l_v, u + 1 \rbrace, r_v], f_{u, k} \gets f_{u, k} + C \]

很明显的区间加操作,使用二维前缀和维护即可。

时间复杂度:\(\Theta(n ^ 2 + m)\)

代码

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;

const int N = 5020;
const int mod = 998244353;

inline int add(int a, int b) {
    return a + b >= mod ? a + b - mod : a + b;
}

inline int sub(int a, int b) {
    return a - b < 0 ? a - b + mod : a - b;
}

inline int mul(int a, int b) {
    return 1ll * a * b % mod;
}

int Inv[N];

int n, m, L[N], R[N];
int f[N][N];
int ans[N];

inline void add(int x1, int y1, int x2, int y2, int v) {
    if (x1 > x2 || y1 > y2) return;
    f[x2][y2] = add(f[x2][y2], v);
    f[x1 - 1][y1 - 1] = add(f[x1 - 1][y1 - 1], v);
    f[x1 - 1][y2] = sub(f[x1 - 1][y2], v);
    f[x2][y1 - 1] = sub(f[x2][y1 - 1], v);
}

signed main() {
    scanf("%d", &n);
    Inv[1] = 1;
    for (int i = 2; i <= n; ++i)
        Inv[i] = mul(mod - mod / i, Inv[mod % i]);
    for (int i = 2; i <= n; ++i)
        scanf("%d%d", &L[i], &R[i]);
    scanf("%d", &m);
    for (int _ = 1, u, v, w; _ <= m; ++_) {
        scanf("%d%d%d", &u, &v, &w);
        if (u == v) continue;
        if (u > v) swap(u, v);
        add(u, v, u, v, w);
    }
    for (int u = n; u >= 1; --u) {
        for (int v = n; v >= u; --v) {
            f[u][v] = add(f[u][v], f[u + 1][v]);
            f[u][v] = add(f[u][v], f[u][v + 1]);
            f[u][v] = sub(f[u][v], f[u + 1][v + 1]);
            if (v > u) {
                int val = mul(Inv[R[v] - L[v] + 1], f[u][v]);
                add(u, max(u + 1, L[v]), u, R[v], val);
                add(L[v], u, min(u - 1, R[v]), u, val);
                ans[v] = add(ans[v], f[u][v]);
            }
            // for (int o = max(u + 1, L[v]); o <= R[v]; ++o) {
            //     f[u][o] = add(f[u][o], mul(Inv[R[v] - L[v] + 1], f[u][v]));
            // }
            // for (int o = L[v]; o <= min(u - 1, R[v]); ++o) {
            //     f[o][u] = add(f[o][u], mul(Inv[R[v] - L[v] + 1], f[u][v]));
            // }
        }
    }
    for (int i = 2; i <= n; ++i)
        printf("%d ", ans[i]);
    return 0;
}

后记 & 反思

遇到期望 / 概率的题目,要往 DP 的方向思考,而不是像无头苍蝇一样只会写一个爆搜。

遇到宏观情况很鬼畜的问题,往往微观上一次操作是可控的,那么着眼于微观,每次迈出一小步即可。

posted @ 2024-08-18 10:54  XuYueming  阅读(11)  评论(0编辑  收藏  举报