1. 程式人生 > >LOJ #2135. 「ZJOI2015」幻想鄉戰略遊戲(點分樹)

LOJ #2135. 「ZJOI2015」幻想鄉戰略遊戲(點分樹)

題意

給你一顆 \(n\) 個點的樹,每個點的度數不超過 \(20\) ,有 \(q\) 次修改點權的操作。

需要動態維護帶權重心,也就是找到一個點 \(v\) 使得 \(\displaystyle \sum_{v} w_v \times \mathrm{dist}(u, v)\) 最小。

\(n \le 10^5, q \le 10^5\)

題解

首先了解一個重心的重要性質:

對於一條邊 \(x \to y\) 如果 \(x\) 側子樹和 \(>\) \(y\) 側子樹和,那麼重心一定在 \(x\) 側。

值得一提的是這個結論對於 \(\mathrm{dist}^k(x, i)\) 都是成立的,一般情況下都需要快速求出樹的帶權重心。

利用這個結論可以快速求出 帶權重心

暴力求的話,在隨機資料下表現非常優秀,但是我們明顯可以用一些資料結構來優化這個過程,此時不難想到 點分樹

因為對於上面那個找 帶權重心 的過程,我們可以考慮分治解決。

  1. 首先先到整棵樹的重心,當做分治操作的起點。
  2. 每次考慮列舉它所有點分樹上的兒子,然後向著 \(sumchild_y * 2 > Sum\) 的方向去走,也就是向著子樹權值和大於總權值一半的方向。
  3. 然後我們接下來就可以到這個子樹所對應的分治中心,繼續進行分治操作。
  4. 直到這個點在點分樹上不存在一個兒子滿足 \(sumchild_y * 2 > Sum\) 的要求,此時這個點就是重心。

這樣我們最多重複 \(\log n\) 次操作就停下來,接下來我們就是需要動態求一個子樹的 \(sum_y\)

這個顯然可以用樹剖線段樹等資料結構進行維護,但我們有了點分樹顯然這樣是多餘的。

我們考慮對於每個點分樹上每個點,維護它子樹所有點的 \(w_i\) 的和,記為 \(\displaystyle sum_i = \sum_{v \in child(i)} w_v\)

我們每次從 \(x \to y\) 向下分治的時候,令 \(v\)\(x \to y\)原樹 路徑上除 \(x\) 外第一個點。

我們考慮把 \(v \to y\) 在點分樹路徑上的所有 \(sum\)

加上 \(sum_x - sum_y\) 也就是 \(x\) 部分的點權。

至於這樣為什麼是對的。簡單說明下,這樣就會對接下來所有需要算上 \(x\) 部分貢獻的分治重心進行貢獻。這樣我們就保證了所有要算上的點都是算上的正確的答案。

注意做完後需要減回來。

然後我們找到了重心,考慮計算答案。

有兩種方法。

  1. 第一種是類似於 「HNOI2015」開店 其中一個做法,利用滿足差分的性質。

    也就是 \(\displaystyle \sum _{i=1}^{n} w_i \mathrm{dist}(x, i) = \sum_{i=1}^{n} w_i(d_i + d_x) - 2 \sum_{i=1}^{n} w_i d_{lca(i, x)}\) 的特性。(此處 \(d_i\)\(i\) 的深度)

    對於每個點將其到根路徑鏈上的點 \(w_i\) ,然後詢問 \(x\) 到根的路徑點權和 \(res\)

    \(\displaystyle \sum_{i=1}^{n} w_id_i + (\sum_{i=1}^{n}w_i)d_x\) 減去 \(res\) 就行了。然後用樹剖後,利用線段樹就可以動態維護了。

  2. 但顯然此處,我們還是有著點分樹這個強大的樹上結構,可以考慮換一種方式來維護。

    我們在之前維護 \(sum_u\) 的基礎上,多維護兩個東西。

    • \(tot_u\) :點分樹上 \(u\) 的子樹裡所有點的到 \(u\) 的帶權距離和 \(\displaystyle \sum_{v \in child(u)} w_v \times \mathrm{dist}(v, u)\)
    • \(totfa_u\) :點分樹上\(u\) 的子樹裡所有點的到 \(u\) 在點分樹上的父親 \(fa\) 的帶權距離和 \(\displaystyle \sum_{v \in child(u)} w_v \times \mathrm{dist}(v, fa)\)

    然後詢問 \(pos\) 節點的時候。我們考慮每次在點分樹向上跳,並計算貢獻。

    假設當前從 \(v \to u\) ,把 \(ans\) 加上 \((sum_{u} - sum_{v}) \times \mathrm{dist}(pos,fa)\) ,這個意思就是把 \(v\) 外面所有的點加上這條邊權的答案。

    但是這樣顯然算少了,因為 \(v\) 外面所有點到 \(u\) 的距離沒有算上,所以還要加上 \(totf_v - tot_v\) 這部分貢獻就行了。

    注意 \(ans\) 一開始的時候初值是 \(tot_{pos}\)

然後為了程式碼沒有那麼毒瘤,對於此處的樹上距離,我們可以預處理出每個點到它點分樹上的祖先的距離。

因為我們只需要用上這些點對的距離。

總結

對於一些動態有關樹上距離的問題,我們可以考慮點分樹之類的強大資料結構。

然後帶權重心都可以滿足之前那個性質,可以用點分樹上去找。

程式碼

強烈建議看看我的程式碼!! 寫的真的優秀!!

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
    freopen ("2135.in", "r", stdin);
    freopen ("2135.out", "w", stdout);
#endif
}

const int N = 1e5 + 1e3, M = N << 1;

typedef long long ll;

int Head[N], Next[M], to[M], val[M], e;
inline void add_edge(int u, int v, int w) {
    to[++ e] = v; Next[e] = Head[u]; Head[u] = e; val[e] = w;
}
inline void Add(int u, int v, int w) {
    add_edge(u, v, w); add_edge(v, u, w);
}

#define Travel(i, u, v) for(register int i = Head[u], v = to[i]; i; v = to[i = Next[i]])

bitset<N> vis;
int sz[N], maxsz[N], rt, nodesum;
void Get_Root(int u, int fa = 0) {
    sz[u] = maxsz[u] = 1;
    Travel(i, u, v) if (v != fa && !vis[v])
        Get_Root(v, u), sz[u] += sz[v], chkmax(maxsz[u], sz[v]);
    chkmax(maxsz[u], nodesum - sz[u]);
    if (maxsz[u] < maxsz[rt]) rt = u;
}

ll dis[N][20]; int from[N], cur[N];
void Get_Dis(int u, ll dep, int fa, int anc) {
    if (fa) from[u] = anc, dis[u][cur[u] ++] = dep;
    Travel(i, u, v) if (!vis[v] && v != fa) Get_Dis(v, dep + val[i], u, anc);
}

typedef pair<int, ll> PII;
#define fir first
#define sec second
#define mp make_pair
vector<PII> Sub[N];

void Dfs_Div(int u = 1) {
    vis[u] = true; Get_Dis(u, 0, 0, u);
    Travel(i, u, v) if (!vis[v])
        rt = 0, nodesum = sz[v], Get_Root(v), 
           Sub[u].push_back(mp(rt, v)), from[rt] = u, Dfs_Div(rt);
}

ll sum[N], tot[N], tot_fa[N];
inline void Update(int pos, int uv) {
    tot_fa[pos] += dis[pos][0] * uv;
    for (register int u = pos, dep = 0; u; u = from[u], ++ dep) {
        sum[u] += uv;
        tot[from[u]] += dis[pos][dep] * uv;
        tot_fa[from[u]] += dis[pos][dep + 1] * uv;
    }
}

PII cache[N]; int len = 0;

ll Sum;
int Find_Root(int u) {
    for (PII it : Sub[u]) {
        register int v = it.fir;
        if (sum[v] * 2 > Sum) {
            register int pos; ll sumu;
            for(pos = it.sec, sumu = Sum - sum[v]; pos != from[u]; pos = from[pos])
                sum[pos] += sumu, cache[++ len] = mp(pos, sumu);
            return Find_Root(v);
        }
    }
    return u;
}

int bas;
inline ll Query() {
    register int pos = Find_Root(bas);
    For (i, 1, len) sum[cache[i].fir] -= cache[i].sec; len = 0;

    ll res = tot[pos];
    for (register int u = from[pos], Last = pos, dep = 0; u; u = from[Last = u], ++ dep)
        res += tot[u] - tot_fa[Last] + (sum[u] - sum[Last]) * dis[pos][dep];

    return res;
}

signed main () {

    File();

    int n = read(), q = read();
    For (i, 1, n - 1) {
        int u = read(), v = read(), w = read(); Add(u, v, w);
    }

    maxsz[rt = 0] = nodesum = n; Get_Root(1); Dfs_Div(bas = rt);
    For (i, 1, n) if(cur[i]) reverse(dis[i], dis[i] + cur[i]);

    while (q --) {
        int pos = read(), uv = read();
        Sum += uv; Update(pos, uv);
        printf ("%lld\n", Query());
    }

    return 0;
}