一類樹上問題的解決辦法

本文參考自 梁晏成《樹上數據結構》 ,感謝他在雅禮集訓的講解。html

轉化成序列問題

dfs序

按照 \(dfs\) 的入棧順序造成一個序列。node

例如對於這棵樹c++

img

它的 \(dfs\) 序就是 \(1~2~3~4~5~6~7~8\) 。(假設我遍歷兒子是從左到右的)git

樹鏈剖分的運用

對於這個咱們經常配合 樹鏈剖分 來使用。數組

這樣對於一個點,它的子樹編號是連續的一段區間,便於作子樹修改以及查詢問題。數據結構

重鏈上全部節點的標號也是連續的一段區間。app

因此咱們能夠解決大部分鏈或子樹修改以及查詢的問題,十分的優秀。ide

也就是經常把樹上問題轉化成序列問題的通用解法。優化

括號序列

\(dfs\) 時候,某個節點入棧時加入左括號,出棧時加入右括號。

也就是在 \(dfs\) 序旁邊添加括號。

一樣對於上面那顆樹 。

爲了方便觀看,咱們在其中添入一些數字。

它的括號序列就是 \((1(2)(3(4)(5(6(7))))(8))\)

求解樹上距離問題

這個能夠對於一些有關於樹上距離的問題有用,好比 BZOJ1095 [ZJOI2007] Hide 捉迷藏 (括號序列 + 線段樹)

也就是對於樹上兩點的距離,就是他們中間未匹配的括號數量。這個是很顯然的,由於匹配的括號一定不存在於他們之間的路徑上,其餘的都存在於他們的路徑上。

也就是說向上路徑的括號是 \()\) 向下路徑的括號就是 \((\)

樹上莫隊轉化成普通莫隊

\(L_x\)\(x\) 左括號所在的位置,\(R_x\)\(x\) 右括號所在的位置。

咱們查詢樹上一條路徑 \(x \sim y\) 知足 \(L_x \le L_y\) ,考慮:

  • 若是 \(x\)\(y\) 的祖先,那麼 \(x\)\(y\) 的鏈與括號序列 \([L_x, L_y]\) 對應。
  • 若是 \(x\) 不是 \(y\) 的祖先,那麼 \(x\)\(y\) 的鏈除 \(lca\) 部分與括號序列中區間 \([R_x, L_y]\) 對應。

第二點是由於 \(lca\) 的貢獻會在其中被抵消掉,最後暴力算上就好了。

每次移動的時候就修改時候判斷一個點被匹配了沒,匹配減去,沒匹配加上就好了。

SP10707 COT2 - Count on a tree II

題意

屢次詢問樹上一條路徑上不一樣顏色種數。

題解

咱們利用括號序列,把樹上的問題直接拍到序列上來作暴力莫隊就好了,和以前莫隊模板題同樣的作法。

歐拉序列

\(dfs\) 時,某個節點入棧時加入隊列,出棧時將父親加入隊列。

仍是對於上面那顆樹,

它的歐拉序列就是 \(1~2~1~3~4~3~5~6~7~6~5~3~1~8~1\)

這個有什麼用呢qwq 經常用來作 \(lca\) 問題。

具體來講就是,對於歐拉序列每一個點記住它的深度,而後對於任意兩個點的 \(lca\) 就是他們兩個點第一次出現時候的點對之間 深度最小 的那個點。

這就轉化成了一個 \(RMQ\) 問題,用普通的 \(ST\) 表預處理就能夠達到 \(O(n \log n)\) ,詢問就是 \(O(1)\) 的。

若是考慮用約束 \(RMQ\) 來解決,就能夠達到 \(O(n)\) 預處理,\(O(1)\) 詢問的複雜度。

雖然看起來特別優秀,可是並不經常使用qwq

差分思想

  • 對於一對點 \(x, y\) ,假設它們 \(lca\)\(z\) ,那麼這條 \(x\)\(y\) 的鏈能夠用 \(x, y, z, fa[z]\) 的鏈表示。

    例如給一條 \(x \to y\) 的鏈加上一個數 \(v\) ,最後詢問每一個點的權值。

    咱們能夠把 \(x,y\) 處加上 \(v\)\(z, fa[z]\) 處減去 \(v\) ,最後對於每一個點求子樹和就是這個點的權值了。

    注意要特判 \(lca = x ~ or ~ y\) 的狀況。

  • 對於兩條相同的邊上的信息能夠抵消(鏈上全部邊異或的值),能夠直接拆成 \(x, y\) 到根的路徑表示。

單點、鏈、子樹的轉化

在某些狀況下,咱們須要修改和改變查詢的對象來減少維護的難度。

下面我都把鏈當作從底向上的一條,其餘鏈其實均可以拆分紅兩條這種鏈(一條 \(x \to lca\) 向上,另外一條 \(lca \to x\) 向下),也能夠類比接下來的方法進行討論。

  • 單點修改鏈上查詢 \(\Leftrightarrow\) 子樹修改單點查詢

    這個如何理解呢,例如對於這顆樹。

    img

    咱們考慮對於修改 \(x\) 的點權值,不難發現它影響的鏈就是相似 \(y,z \to anc[x]\)\(x\) 本身 以及 它的祖先)的點。

    而後就能夠在 \(x\) 處給子樹修改權值,每次查詢一條鏈就是看它鏈底的權值和減去鏈頂的權值和。

    反過來也是差很少的思路。

  • 鏈上修改單點查詢 \(\Leftrightarrow\) 單點修改子樹查詢

    \(y \to x\) 這條鏈上修改權值,查詢一個點的權值。

    不難發現,這就等價於給 \(x, y\) 處打差分標記,而後每次查詢一顆子樹的信息。

    這樣的話,對於一個點所包含的子樹信息,就是整個全部以前鏈覆蓋它的信息。

    這個經常能夠用於最後詢問不少個點,而後用線段樹合併子樹信息。

  • 鏈上修改子樹查詢 \(\Leftrightarrow\) 單點修改子樹查詢

    彷佛是利用 \(dep\) 數組實現的,不太記得怎麼搞了,之後作了題再來解釋吧。

點、邊

一些與「鏈相交」的問題,咱們能夠在點上賦正權,邊上賦負權的方式簡化問題。

例題

題意

  • 插入一條鏈
  • 給定一條鏈,問有多少條鏈於這條鏈相交。

題解

咱們只須要在插入的時候,給鏈上的點 \(+1\) ,鏈上的邊 \(-1\) ,詢問的時候就等價於一個鏈上求和。

這爲何是正確的呢?對於兩條鏈,咱們把負的邊權和下面正的點權抵消掉,那麼就只剩下了最上面共有的交點有多的 \(1\) 的貢獻了。

提取關鍵點

咱們能夠在一棵樹中取不超過 \(\sqrt n\) 個關鍵點,保證每一個點到最近的祖先距離 \(\le \sqrt n\)

具體地,咱們自底向上標記關鍵點。若是當前點子樹內到它最遠的點距離 \(\ge \sqrt n\) 就把當前點標記成關鍵點。

其實相似於序列上的分塊處理。

HDU 6271 Master of Connected Component

題意

給定兩顆 \(n\) 個節點的樹,每一個節點有一對數 \((x, y)\) ,表示圖 \(G\) 中的一條邊。

對於每個 \(x\) ,求出兩棵樹 \(x\) 到根路徑上的全部邊在圖 \(G\) 中構成的子圖聯通塊個數。

多組數據,\(n \le 10000\)

題解

考慮對於第一顆樹提取關鍵點,而後對於每一個點的詢問掛在它最近的關鍵點祖先處。

到每一個關鍵點處理它所擁有的詢問,到第二顆樹上進行遍歷,遍歷到一個當前關鍵點所管轄的節點的時刻就處理它在第一棵樹的信息。

對於一個關鍵點 \(p\) 將它到根節點路徑上的節點所有放入並查集中,而後用支持撤回的並查集維護聯通塊個數。

具體來講,對於那個撤回並查集只須要按秩合併,也就是深度小的連到深度大的上,而後記一下上次操做的深度,以及連的邊。

不難發現每一個點在第一棵數上只會更新到被管轄關鍵點的距離,這個只有 \(\mathcal O(\sqrt n)\) 。而後第二棵樹同時也只會被遍歷 \(\mathcal O(\sqrt n)\) 次。

而後這個時間複雜度就是 \(O(n \sqrt n \log n)\) 的,其實跑的很快?

代碼

強烈建議認真閱讀代碼,提升碼力。

#include <bits/stdc++.h>

#define For(i, l, r) for(int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(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

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 ("6271.in", "r", stdin);
    freopen ("6271.out", "w", stdout);
#endif
}

const int N = 2e4 + 50, M = N * 2, blksize = 350;

typedef pair<int, int> PII;
#define fir first
#define sec second
#define mp make_pair

struct Data {
    
    int x, y, type; 

    Data() {}

    Data(int a, int b, int c) : x(a), y(b), type(c) {}

} opt[N];

namespace Union_Set {

    int fa[N]; int Find(int x) { return x == fa[x] ? x : Find(fa[x]); }

    int height[N], tot = 0;
    inline Data Merge(int x, int y){
        int rtx = Find(x), rty = Find(y);
        if (rtx == rty) return Data(0, 0, 0);
        if (height[rtx] < height[rty]) swap(rtx, rty);
        fa[rty] = rtx; -- tot;
        if (height[rtx] == height[rty]) { ++ height[rtx]; return Data(rtx, rty, 2); }
        else return Data(rtx, rty, 1);
    }

    inline void Retract(Data now) {
        int x = now.x, y = now.y, type = now.type;
        if (!type) return ; height[x] -= (type - 1); fa[y] = y; ++ tot;
    }

}

PII Info[N];
inline Data Insert(int pos) {
    int x = Info[pos].fir, y = Info[pos].sec;
    return Union_Set :: Merge(x, y);
}

inline void Delete(int pos) {
    Union_Set :: Retract(opt[pos]); 
}

int from[N], nowrt;
inline int Get_Ans(int u) {
    static int stk[N], top; top = 0;
    while (u ^ nowrt) {
        opt[u] = Insert(u), stk[++ top] = u, u = from[u];
    }
    int res = Union_Set :: tot;
    while (top) Delete(stk[top --]);
    return res;
}

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

int maxd[N], vis[N];

#define Travel(i, u, v) for(int i = Head[u], v = to[i]; i; i = Next[i], v = to[i])
void Dfs_Init(int u, int fa = 0) {
    from[u] = fa; maxd[u] = 1;
    Travel(i, u, v) if (v != fa) {
        Dfs_Init(v, u);
        chkmax(maxd[u], maxd[v] + 1);
    }
    if (maxd[u] == blksize || u == 1) maxd[u] = 0, vis[u] = true;
}

int n, m;

vector<int> child[N];
inline bool App(int u) {
    vector<int> :: const_iterator it = lower_bound(child[nowrt].begin(), child[nowrt].end(), u);
    if (it == child[nowrt].end()) return false; return (*it == u);
}

int ans[N];
void Dfs2(int u, int fa = 0) {
    opt[u] = Insert(u);
    if (App(u - n)) ans[u - n] = Get_Ans(u - n);
    Travel(i, u, v) if (v != fa) Dfs2(v, u);
    Delete(u);
}

void Dfs1(int u, int fa = 0) {
    opt[u] = Insert(u);
    if (vis[u]) nowrt = u, Dfs2(n + 1, 0);
    Travel(i, u, v) if (v != fa) Dfs1(v, u);
    Delete(u);
}

inline void Init() {
    e = 0; 
    For (i, 1, n * 2) 
        from[i] = 0, Head[i] = 0, child[i].clear(), vis[i] = false;
    For (i, 1, m)
        Union_Set :: fa[i] = i, Union_Set :: height[i] = 1;
    Union_Set :: tot = m;
}

int main () {
    File();

    for (int cases = read(); cases; -- cases) {
        n = read(); m = read(); Init();

        For (id, 0, 1) {
            For (i, 1, n)
                Info[i + id * n] = mp(read(), read());
            For (i, 1, n - 1) {
                int u = read() + id * n, v = read() + id * n;
                add_edge(u, v); add_edge(v, u);
            }
            Dfs_Init(1 + id * n);
        }

        For (i, 1, n) {
            int u = i;
            for (; !vis[u]; u = from[u]) ;
            child[u].push_back(i);
        }

        Dfs1(1); For (i, 1, n) printf ("%d\n", ans[i]); Init();
    }

    return 0;
}

啓發式合併

啓發式合併即合併兩個集合時按照必定順序(一般是將較小的集合的元素一個個插入較大的集合)合併的一種合併方式,常見的數據結構有並查集、平衡樹、堆、字典樹等。

具體地,若是單次合併的複雜度爲 \(O(B)\) ,總共有 \(M\) 個信息,那麼總複雜度爲 \(O(B M \log M)\)

樹的特殊結構,決定了經常可使用啓發式合併優化信息合併的速度。

LOJ #2107. 「JLOI2015」城池攻佔

此處例題有不少,就放一個還行的題目上來。

題意

請點下上面的連接,太長了不想寫了。

題解

不難發現兩個騎士通過同一個節點的時候,攻擊力的相對大小是不會改變的;

而後咱們每次找當前攻擊力最小的騎士出來,判斷是否會死亡。

這個能夠用一個可並小根堆實現(也能夠用 splay 或者 treap 各種平衡樹實現)。

咱們能夠用 lazy 標記來支持加法和乘法操做就好了。

用斜堆實現彷佛常數比左偏樹小?還少了一行qwq

而且斜堆中每一個元素的下標就是對應着騎士的編號,很好寫!

複雜度是 \(O(m \log m)\)lych 說是 \(O(m \log ^ 2 m)\) ? 我也不知道是否是qwq

代碼

#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;}

typedef long long ll;
inline ll read() {
    ll 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 ("2107.in", "r", stdin);
    freopen ("2107.out", "w", stdout);
#endif
}

const int N = 3e5 + 1e3;
const ll inf = 1e18;

int n, m; ll Def[N];
int opt[N]; ll val[N];

namespace Lifist_Tree {

    ll val[N], TagMult[N], TagAdd[N];

    int ls[N], rs[N];

    inline void Mult(int pos, ll uv) { if (pos) val[pos] *= uv, TagAdd[pos] *= uv, TagMult[pos] *= uv; }

    inline void Add(int pos, ll uv) { if (pos) val[pos] += uv, TagAdd[pos] += uv; }

    inline void Push_Down(int x) {
        if (TagMult[x] != 1)
            Mult(ls[x], TagMult[x]), Mult(rs[x], TagMult[x]), TagMult[x] = 1;

        if (TagAdd[x] != 0)
            Add(ls[x], TagAdd[x]), Add(rs[x], TagAdd[x]), TagAdd[x] = 0;
    }

    int Merge(int x, int y) {
        if (!x || !y) return x | y;
        if (val[x] > val[y]) swap(x, y);
        Push_Down(x); 
        rs[x] = Merge(rs[x], y);
        swap(ls[x], rs[x]);
        return x;
    }

    inline int Pop(int x) {
        Push_Down(x);
        int tmp = Merge(ls[x], rs[x]);
        ls[x] = rs[x] = 0;
        return tmp;
    }

}

vector<int> G[N];
int dep[N], die[N], ans[N], rt[N];
void Dfs(int u) {
    int cur = rt[u];
    for (int v : G[u])
        dep[v] = dep[u] + 1, Dfs(v), cur = Lifist_Tree :: Merge(cur, rt[v]);

    while (cur && Lifist_Tree :: val[cur] < Def[u])
        die[cur] = u, cur = Lifist_Tree :: Pop(cur), ++ ans[u];
    if (opt[u])
        Lifist_Tree :: Mult(cur, val[u]);
    else
        Lifist_Tree :: Add(cur, val[u]);

    rt[u] = cur;
}

int pos[N];
int main () {

    File();

    n = read(); m = read();

    Def[0] = inf; For (i, 1, n) Def[i] = read();

    For (i, 2, n) {
        int from = read();
        G[from].push_back(i);
        opt[i] = read(); val[i] = read();
    }
    G[0].push_back(1);

    For (i, 1, m) {
        Lifist_Tree :: val[i] = read(); Lifist_Tree :: TagMult[i] = 1; pos[i] = read();
        rt[pos[i]] = Lifist_Tree :: Merge(rt[pos[i]], i);
    }

    Dfs(0);
    For (i, 1, n) 
        printf ("%d\n", ans[i]);
    For (i, 1, m)
        printf ("%d\n", dep[pos[i]] - dep[die[i]]);

    return 0;
}

直徑的性質

\(F(S)\) 表示集合 \(S\) 中最遠的兩個點構成的集合,那麼對同一棵樹中的集合 \(S, T\)\(F(S \cup T) \subseteq F(S) \cup F(T)\)

這個證實。。。我不會qwq fakesky 說能夠反證法來證實?

51nod 1766 樹上最遠點對

題意

給定一棵樹,屢次詢問 \(a, b, c, d\) ,求 \(\displaystyle \max_{a \le i \le b, c \le j \le d} dist(i, j)\)

題解

用線段樹維護區間最遠點對,而後利用上面的性質。

每次合併的時候枚舉 \(\displaystyle\binom 4 2 = 6\) 種狀況,取最遠的一對做爲答案就好了。

用前面講的歐拉序列和 \(ST\) 表求 \(lca\) ,複雜度能夠優化成 \(O((n + q) \log n)\)

代碼

#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() {
    freopen ("1766.in", "r", stdin);
    freopen ("1766.out", "w", stdout);
}

const int N = 110000;

typedef pair<int, int> PII;
#define fir first
#define sec second

vector<PII> G[N];
int dep[N], dis[N], minpos[N * 2][21], tot = 0, Log2[N * 2], app[N];

inline bool cmp(int x, int y) { return dep[x] < dep[y]; }

inline int Get_Lca(int x, int y) {
    int len = Log2[y - x + 1], 
        p1 = minpos[x][len], 
        p2 = minpos[y - (1 << len) + 1][len];
    return cmp(p1, p2) ? p1 : p2;
}

inline int Get_Dis(int x, int y) {
    int tmpx = app[x], tmpy = app[y];
    if (tmpx > tmpy) swap(tmpx, tmpy);
    int Lca = Get_Lca(tmpx, tmpy);
    return dis[x] + dis[y] - dis[Lca] * 2;
}

void Dfs_Init(int u, int fa = 0) {
    minpos[app[u] = ++ tot][0] = u;
    dep[u] = dep[fa] + 1;
    For (i, 0, G[u].size() - 1) {
        PII cur = G[u][i];
        int v = cur.fir;
        if (v != fa) dis[v] = dis[u] + cur.sec, Dfs_Init(v, u);
    }
    if (fa) minpos[++ tot][0] = fa;
}

typedef pair<int, int> PII;
#define fir first
#define sec second
#define mp make_pair

inline void Update(PII &cur, PII a, PII b, bool flag) {
    int lx = a.fir, ly = a.sec, rx = b.fir, ry = b.sec, res = 0;

    if (flag && chkmax(res, Get_Dis(lx, ly))) cur = mp(lx, ly);
    if (chkmax(res, Get_Dis(lx, rx))) cur = mp(lx, rx);
    if (chkmax(res, Get_Dis(lx, ry))) cur = mp(lx, ry);

    if (chkmax(res, Get_Dis(ly, rx))) cur = mp(ly, rx);
    if (chkmax(res, Get_Dis(ly, ry))) cur = mp(ly, ry);
    if (flag && chkmax(res, Get_Dis(rx, ry))) cur = mp(rx, ry);
}

namespace Segment_Tree {

#define lson o << 1, l, mid
#define rson o << 1 | 1, mid + 1, r

    PII Adv[N << 2];

    void Build(int o, int l, int r) {
        if (l == r) { Adv[o] = mp(l, r); return ; }
        int mid = (l + r) >> 1;
        Build(lson); Build(rson);
        Update(Adv[o], Adv[o << 1], Adv[o << 1 | 1], true);
    }

    PII Query(int o, int l, int r, int ql, int qr) {
        if (ql <= l && r <= qr) return Adv[o];
        PII tmp; int mid = (l + r) >> 1;
        if (qr <= mid) tmp = Query(lson, ql, qr);
        else if (ql > mid) tmp = Query(rson, ql, qr);
        else Update(tmp, Query(lson, ql, qr), Query(rson, ql, qr), true);
        return tmp;
    }

#undef lson
#undef rson

}

int n, m;

int main () {

    n = read();
    For (i, 1, n - 1) {
        int u = read(), v = read(), w = read();
        G[u].push_back(mp(v, w));
        G[v].push_back(mp(u, w));
    }
    Dfs_Init(1);

    For (i, 2, tot) Log2[i] = Log2[i >> 1] + 1;

    For (j, 1, Log2[tot]) For (i, 1, tot - (1 << j) + 1) {
        register int p1 = minpos[i][j - 1], p2 = minpos[i + (1 << (j - 1))][j - 1];
        minpos[i][j] = cmp(p1, p2) ? p1 : p2;
    }


    Segment_Tree :: Build(1, 1, n);

    m = read();
    For (i, 1, m) {
        int a = read(), b = read(), c = read(), d = read();

        PII ans;

        Update(ans, 
                Segment_Tree :: Query(1, 1, n, a, b), 
                Segment_Tree :: Query(1, 1, n, c, d), false);

        printf ("%d\n", Get_Dis(ans.fir, ans.sec));
    }

    return 0;
}

雅禮NOIp 7-22 Practice

題意

給你一棵以 \(1\) 爲根的樹,一開始全部點全爲黑色。

須要支持兩個操做:

  • \(C ~ p\) ,將 \(p\) 節點反色
  • \(G ~ p\) ,求 \(p\) 子樹中最遠的兩個黑色節點的距離。

題解

[ZJOI2007] 捉迷藏 進行了增強,支持查詢子樹。

和上面那題是同樣的,由於每棵樹的子樹的 \(dfs\) 序是連續的。

咱們考慮用線段樹維護一段連續 \(dfs\) 序的點的最遠點對就好了。

長鏈剖分

把重鏈剖分中重兒子的定義變成子樹內葉子深度最大的兒子,就是長鏈剖分了。

但爲何咱們作鏈上操做的時候不用長鏈剖分呢?由於一個點到根的輕邊個數能夠是 \(O(\sqrt n)\) 的級別,如圖:

img

k-th ancestor

用這個能夠實現 \(O(n \log n)\) 預處理, \(O(1)\) 查詢一個點 \(x\)\(k\) 級祖先。

具體實現參考這個 Bill Yang 大佬的博客 講的挺好的qwq

O(n) 統計每一個點子樹中以深度爲下標的可合併信息

具體來講就是巧妙的繼承重兒子狀態,把本身的狀態 \(O(1)\) 添加在最後,而後暴力按其餘輕兒子重鏈長度繼承狀態,就好了。複雜度是 \(O(\sum\) 重鏈長 \() = O(n)\) 的。

BZOJ 3653: 談笑風生

點進去就行啦,網上惟一一篇長鏈剖分的題解。。(真不要臉)

定長最長鏈

給定一棵樹,求長度爲 \(L\) 的邊權和最大的鏈。

對點 \(x\) ,設重鏈長爲 \(l\) ,維護 \(f_{x, 1..l}\) 表示以 \(x\) 爲根長度爲 \(1 .. l\) 的鏈最大的邊權和。

每次直接繼承重兒子的 \(f\) ,而後和輕兒子依次合併,合併的時候順便計算就好了。

時間複雜度 \(O(n)\)

樹鏈剖分維護動態 dp

動態修改邊權,維護直徑

\(f_x, g_x\) 表示以 \(x\) 爲根的最長鏈和 \(x\) 子樹內直徑的長度,令 \(y\)\(x\) 的兒子,每次用 \((f_y + 1, \max\{f_x + f_y + 1, g_y\})\) 來更新 \((f_x, g_x)\)

每次考慮優先轉移輕兒子,最後再來轉移重兒子。令 \(f', g'\) 表示轉移完輕兒子的結果,那麼每次只會修改 \(O(\log )\)\(f', g'\) ,能夠暴力處理;重鏈上的轉移能夠維護相似 \(a_i = \max \{a_{i+1} + A, B\}\) 的標記 \(A, B\) 。使用線段樹合併,也可使用矩陣來作。總複雜度 \(O(m \log^2 n)\) 常數有點大。

動態修改點權,詢問最大權獨立集

這個直接點進去看我博客就行啦,繼續不要臉一波。。。

link-cut-tree

這個是個解決大量樹上(甚至圖上)問題的利器。能夠見我以前的博客講解

LOJ #2001. 「SDOI2017」樹點塗色

題意

直接點上面的連接,題意很清楚啦qwq

題解

首先解決鏈的答案,考慮差分,\(ans = ans_u + ans_v - 2 \times ans_{lca(u, v)} + 1\)

這個證實能夠分兩種狀況討論,一種 \(lca\) 和下面的點有同樣的顏色,另外一種沒有,其實都是同樣的狀況。

只是要注意每次染上的都是不一樣的顏色,因此知足。

每次把一個點到根染上一種新顏色,不難發現這很相似於 \(lct\) 中的 \(Access\) 操做。

具體來講,\(lct\) 每一個 \(splay\) 維護的是一個相同顏色的集合。

每次塗新顏色,不難發現就是 \(Access\) 斷開的右兒子 \(splay\) 的根所在的子樹答案會增長 \(1\) ,新接上去的兒子須要減 \(1\)

而後咱們須要子樹加,子樹查 \(\max\) ,這個直接用 樹剖 + 線段樹 就能夠維護了。

代碼

#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 ("2001.in", "r", stdin);
    freopen ("2001.out", "w", stdout);
#endif
}

const int Maxn = 1e5 + 1e3, N = 1e5 + 1e3;


int n, m;
vector<int> G[N]; 

int fa[N], sz[N], dep[N], son[N];
void Dfs_Init(int u, int from = 0) {
    dep[u] = dep[fa[u] = from] + 1; sz[u] = 1;
    for (int v : G[u]) if (v != from) {
        Dfs_Init(v, u); 
        sz[u] += sz[v];
        if (sz[son[u]] < sz[v]) son[u] = v;
    }
}
 
int dfn[N], num[N], top[N];
void Dfs_Part(int u) {
    static int clk = 0;
    num[dfn[u] = ++ clk] = u;
    top[u] = son[fa[u]] == u ? top[fa[u]] : u;
    if (son[u]) Dfs_Part(son[u]);
    for (int v : G[u]) if (v != fa[u] && v != son[u]) Dfs_Part(v);
}

namespace Segment_Tree {

#define lson o << 1, l, mid
#define rson o << 1 | 1, mid + 1, r

    int maxv[N << 2], Tag[N << 2];

    inline void Add(int o, int uv) {
        maxv[o] += uv; Tag[o] += uv; 
    }
    
    inline void Push_Down(int o) {
        if (!Tag[o]) return ;
        Add(o << 1, Tag[o]); Add(o << 1 | 1, Tag[o]); Tag[o] = 0;
    }

    inline void Push_Up(int o) {
        maxv[o] = max(maxv[o << 1], maxv[o << 1 | 1]);
    }

    void Build(int o, int l, int r) {
        if (l == r) { maxv[o] = dep[num[l]]; return ; }
        int mid = (l + r) >> 1; Build(lson); Build(rson); Push_Up(o);
    }

    void Update(int o, int l, int r, int ul, int ur, int uv) {
        if (ul <= l && r <= ur) { Add(o, uv); return ; }
        int mid = (l + r) >> 1; Push_Down(o);
        if (ul <= mid) Update(lson, ul, ur, uv);
        if (ur > mid) Update(rson, ul, ur, uv); Push_Up(o);
    }

    int Query(int o, int l, int r, int ql, int qr) {
        if (ql <= l && r <= qr) return maxv[o];
        int tmp = 0, mid = (l + r) >> 1; Push_Down(o);
        if (ql <= mid) chkmax(tmp, Query(lson, ql, qr));
        if (qr > mid) chkmax(tmp, Query(rson, ql, qr));
        Push_Up(o); return tmp;
    }

#undef lson
#undef rson

}

namespace Link_Cut_Tree {

#define ls(o) ch[o][0]
#define rs(o) ch[o][1]

    int fa[Maxn], ch[Maxn][2];

    inline bool is_root(int o) {
        return o != ls(fa[o]) && o != rs(fa[o]);
    }

    inline bool get(int o) { return o == rs(fa[o]); }

    inline void Rotate(int v) {
        int u = fa[v], t = fa[u], d = get(v);
        fa[ch[u][d] = ch[v][d ^ 1]] = u;
        fa[v] = t; if (!is_root(u)) ch[t][rs(t) == u] = v;
        fa[ch[v][d ^ 1] = u] = v;
    }

    inline void Splay(int o) {
        for (; !is_root(o); Rotate(o)) 
            if (!is_root(fa[o])) 
                Rotate(get(o) ^ get(fa[o]) ? o : fa[o]);
    }

    inline int Find_Root(int o) {
        while (ls(o)) o = ls(o); return o;
    }

    inline void Access(int o) {
        for (register int t = 0, rt; o; o = fa[t = o]) {
            Splay(o); 
            if (rs(o)) rt = Find_Root(rs(o)), Segment_Tree :: Update(1, 1, n, dfn[rt], dfn[rt] + sz[rt] - 1, 1);
            rs(o) = t;
            if (rs(o)) rt = Find_Root(rs(o)), Segment_Tree :: Update(1, 1, n, dfn[rt], dfn[rt] + sz[rt] - 1, - 1);
        }
    }

}

inline int Get_Lca(int x, int y) {
    for (; top[x] ^ top[y]; x = fa[top[x]])
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
    return dep[x] < dep[y] ? x : y;
}

int main () {

    File();

    n = read(); m = read();
    For (i, 1, n - 1) {
        int u = read(), v = read();
        G[u].push_back(v);
        G[v].push_back(u);
    }
    Dfs_Init(1); Dfs_Part(1);
    Segment_Tree :: Build(1, 1, n);

    For (i, 1, n)
        Link_Cut_Tree :: fa[i] = fa[i];

    For (i, 1, m) {
        int opt = read();
        if (opt == 1) {
            int pos = read();
            Link_Cut_Tree :: Access(pos);
        }
        if (opt == 2) {
            int x = read(), y = read(), Lca = Get_Lca(x, y);
            printf ("%d\n", 
                    Segment_Tree :: Query(1, 1, n, dfn[x], dfn[x]) + Segment_Tree :: Query(1, 1, n, dfn[y], dfn[y]) - 
                    2 * Segment_Tree :: Query(1, 1, n, dfn[Lca], dfn[Lca]) + 1);
        }
        if (opt == 3) {
            int pos = read();
            printf ("%d\n", Segment_Tree :: Query(1, 1, n, dfn[pos], dfn[pos] + sz[pos] - 1));
        }
    }

    return 0;
}

維護 MST

利用 \(lct\) 能夠維護只有插入的 \(MST\)

爲了方便,拆邊爲點,也就是說 \(x \to y\) 變成 \(x \to z \to y\) ,將邊權變成點權。

每次只要支持查找一條路徑上邊權最大的邊,以及刪邊和加邊就好了。

維護圖的連通性

若是容許離線,那麼能夠實現利用 \(lct\) 維護圖的連通性的有關信息。

根據貪心的思想,咱們但願保留儘可能晚被刪除的邊。因而能夠考慮維護以刪除時間爲權值的最大生成森林,和上面那個方法就是同樣的。

若是一個圖 \(G\) 存在補圖 \(G'\) ,能夠考慮同時維護圖 \(G\)\(G'\) 的兩個生成森林 \(T\)\(T'\) ,在 \(G\) 中刪邊至關於在 \(G'\) 中加邊,這樣能夠解決 \(lct\) 難以實現刪邊的問題。

點分治

樹的重心

通常狀況下,若是一個點知足它做爲根時最大子樹的大小最小,咱們就稱這個點爲樹的重心。

應用

點分治是將當前子樹的重心做爲分治中心的一種分治方法,這個相似與序列分治找中點分治,經常用來優化 \(dp\) 或 加快合併速度。

例題

LuoguP2634 [國家集訓隊]聰聰可可

題意

詢問樹上有多少條路徑,使得這條路徑長 \(\bmod \{k = 3\}\) 等於 \(0\)

題解

最裸的一道題。。

其實能夠直接 \(dp\) ,但若是把 \(k=3\) 改爲 \(k=10^6\) 之類的, \(dp\) 就不能作了,只能用點分治。

那樣能夠達到 \(O((n + k) \log n)\) 的優秀的複雜度。

咱們考慮每次點分治,而後對於分治重心的每個子樹,統計一下到子樹根節點有多少條路徑 \(\bmod k = b\)

而後每次合併的時候,直接枚舉其中一個,而後另外一個就是 \((k - b) \mod k\) 了。

代碼

其實很好寫的。

#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 ("P2634.in", "r", stdin);
    freopen ("P2634.out", "w", stdout);
#endif
}

const int N = 30100, M = N << 1, inf = 0x7f7f7f7f;

int Head[N], Next[M], to[M], val[M], e = 0;
void add_edge(int u, int v, int w) {
    to[++ e] = v; val[e] = w; Next[e] = Head[u]; Head[u] = e;
}
#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], nodesum, rt;
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;
}

int tot[3];
void Get_Info(int u, int fa, int dis) {
    ++ tot[dis];
    Travel(i, u, v) if (v != fa && !vis[v])
        Get_Info(v, u, (dis + val[i]) % 3);
}

typedef long long ll; ll ans = 0;
int sum[3];
inline void Init() { Set(sum, 0); sum[0] = 1; ++ ans; }
inline void Calc() {
    For (i, 0, 2)
        ans += 2ll * sum[i] * tot[(3 - i) % 3];
    For (i, 0, 2) sum[i] += tot[i];
}

void Solve(int u) {
    vis[u] = true; Init();
    Travel(i, u, v) if (!vis[v])
        Set(tot, 0), Get_Info(v, u, val[i]), Calc();
    Travel(i, u, v) if (!vis[v])
        nodesum = sz[v], rt = 0, Get_Root(v), Solve(rt);
}

int main () {

    File();
    int n = read();
    For (i, 1, n - 1) {
        int u = read(), v = read(), w = read() % 3;
        add_edge(u, v, w); add_edge(v, u, w);
    }
    maxsz[0] = inf; nodesum = n, Get_Root(1), Solve(rt);

    ll gcd = __gcd(ans, 1ll * n * n);
    printf ("%lld/%lld\n", ans / gcd, 1ll * n * n / gcd);

    return 0;
}

點分樹

能夠發如今點分治結構中,一個點與一個以它爲重心的子樹對應。若是將當前重心與全部子樹的重心相連,獲得的樹稱爲 點分樹 或者 重心樹 。點分樹的高度爲 \(O(\log n)\) ,修改一個點時,將會修改點分樹上它到根路徑上全部點對應的子樹信息。

「ZJOI2015」幻想鄉戰略遊戲

爲了充分理解這個數據結構,強烈建議點入我博客中上面對於這道題的題解。

這道題不只充分展示了點分樹的運用,而且個人博客中講解了帶權重心的一個性質,以及求帶權距離和的兩種方法,做爲此處對於點分樹的補充。

線段樹合併

對兩顆線段樹(通常爲動態開點線段樹)合併方法爲:

  • 令根節點爲 \(x, y\)
  • 若是其中一顆線段樹爲空 ( \(x = 0\)\(y = 0\) ),返回另一顆
  • 不然遞歸合併 \(x, y\) 的左、右子樹,最後合併信息。

假設總共有 \(n\) 個信息,那麼容易證實最後複雜度是 \(O(n \log n)\) 的。這是由於每次合併都會使得一個信息所在的集合大小翻倍。

例題

LOJ #2537. 「PKUWC 2018」Minimax

點進去看看qwq

這個介紹了對於一類狀態種數與子樹大小有關的 \(dp\) 能夠考慮用線段樹合併統計狀態。

LOJ #2359. 「NOIP2016」每天愛跑步

再點進去看看QwQ

這個介紹了一類狀態種數與深度大小有關的信息統計能夠考慮用線段樹合併來統計狀態。

以及 NOIP 題,能用高端數據結構來彌補思惟缺陷。

其實對於這類狀態數與深度有關的合併,有些能用上面介紹的長鏈剖分來處理,更加優秀。

Codeforces Round #463 F. Escape Through Leaf

仍是點進去看看。。。

咱們每次考慮一個點,須要對於它子樹全部狀態進行考慮的時候,可使用線段樹合併。

而後對於這個線段樹就能知足子樹合併的性質。

一些鬼畜的線段樹有可能也能夠寫出神奇的合併方法。

相關文章
相關標籤/搜索