對樹鏈剖分的愛 題解

XuYueming發表於2024-08-18

前言

題目連結:洛谷

題意簡述

給出一棵 \(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 的方向思考,而不是像無頭蒼蠅一樣只會寫一個爆搜。

遇到宏觀情況很鬼畜的問題,往往微觀上一次操作是可控的,那麼著眼於微觀,每次邁出一小步即可。

相關文章