1. 程式人生 > 其它 >樹鏈剖分&動態樹學習筆記

樹鏈剖分&動態樹學習筆記

如何將樹上的一段路徑轉化為區間問題?我們可能會想到樹上莫隊中利用尤拉序性質的做法,但其不具有普適性,對於很多區間問題,難以將出現兩次的元素減掉。而樹鏈剖分與動態樹都可以很好地解決這類問題。

樹鏈剖分

樹鏈剖分也稱為重鏈剖分,適用於形態結構不發生變化的樹(即靜態)。
將樹上所有邊分為重邊輕邊,每個節點與其子節點的所有連邊中,有且僅有一條重邊(葉子結點除外),其餘為輕邊。重邊的求法:對於每個節點,考察其所有子節點,子樹中包含節點最多的子節點稱為“最重”。重邊連線此節點與其“最重”的子節點。若有多個子節點同為“最重”,任選其一。
連在一起的重邊從上而下形成重鏈。所有節點都包含在重鏈中,若一個節點上下均無重邊,則其獨自構成一個重鏈。此時有性質:從根節點出發的任意一條路徑“穿過”的重鏈數不超過 \(\log{N}\)


證明:每離開一條重鏈,走進另一條重鏈,必然使得其子樹大小變為原來的 \(\frac{1}{k}\),其中 \(k\) 表示當前節點的子節點個數,且必然 \(≥2\)。於是得證。
對此樹求DFS序(注意不是尤拉序,每個節點僅出現一次)。DFS優先拓展重邊。此時有性質:一條重鏈上的所有節點在DFS序中連在一起。

於是,樹上的任意一條路徑,一定可以被拆分為至多 \(\log{N}\) 個連續區間。我們對每個區間分別求解,最後加在一起,即為答案。
對於DFS序中的元素,常用線段樹/樹狀陣列/平衡樹等 \(\log{N}\) 級別的資料結構維護。故樹鏈剖分的時間複雜度為 \(O(N\log^2{N})\)

。但由於路徑穿過的重鏈往往遠不及 \(\log{N}\) 個,且每個重鏈對應的區間長度也較小,故此演算法常數很小,實際效率很高。

模板題
Code

#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
const int N = 1e5 + 5;
int n, m, a[N];
int head[N], nxt[N << 1], ver[N << 1], tot;
int id[N], sz[N], idx, dfn[N], d[N], son[N], fa[N], top[N];
struct Tree {
    int l, r;
    ll sum, add;
} t[N << 2];
void Add(int x, int y) {
    nxt[++tot] = head[x]; head[x] = tot; ver[tot] = y;
}
void dfs1(int x) {
    sz[x] = 1;
    for (int i = head[x]; i; i = nxt[i]) {
        int y = ver[i];
        if (d[y]) continue;
        d[y] = d[x] + 1; fa[y] = x;
        dfs1(y);
        sz[x] += sz[y];
        if (sz[y] > sz[son[x]]) son[x] = y;
    }
}
void dfs2(int x) {
    dfn[++idx] = x; id[x] = idx;
    if (!son[x]) return ;
    top[son[x]] = top[x]; dfs2(son[x]);
    for (int i = head[x]; i; i = nxt[i]) {
        int y = ver[i];
        if (d[y] < d[x] || y == son[x]) continue;
        top[y] = y; dfs2(y);
    }
}
void pushup(int p) {
    t[p].sum = t[p << 1].sum + t[p << 1 | 1].sum;
}
void pushdown(int p) {
    if (!t[p].add) return ;
    Tree &u = t[p], &l = t[p << 1], &r = t[p << 1 | 1];
    l.add += u.add; l.sum += u.add * (l.r - l.l + 1);
    r.add += u.add; r.sum += u.add * (r.r - r.l + 1);
    u.add = 0;
}
void Build(int l, int r, int p) {
    t[p].l = l; t[p].r = r;
    if (l == r) {
        t[p].sum = a[dfn[l]]; return ;
    }
    int mid = l + r >> 1;
    Build(l, mid, p << 1); Build(mid + 1, r, p << 1 | 1);
    pushup(p);
}
void Insert(int l, int r, int v, int p) {
    if (l <= t[p].l && t[p].r <= r) {
        t[p].add += v; t[p].sum += (ll)v * (t[p].r - t[p].l + 1); return ;
    }
    int mid = t[p].l + t[p].r >> 1;
    pushdown(p);
    if (l <= mid) Insert(l, r, v, p << 1);
    if (r > mid) Insert(l, r, v, p << 1 | 1);
    pushup(p);
}
ll Query(int l, int r, int p) {
    if (l <= t[p].l && t[p].r <= r) return t[p].sum;
    int mid = t[p].l + t[p].r >> 1;
    ll res = 0;
    pushdown(p);
    if (l <= mid) res += Query(l, r, p << 1);
    if (r > mid) res += Query(l, r, p << 1 | 1);
    pushup(p);
    return res;
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i < n; i++) {
        int x, y;
        scanf("%d%d", &x, &y);
        Add(x, y); Add(y, x);
    }
    d[1] = 1; dfs1(1); top[1] = 1; dfs2(1);
    Build(1, n, 1);
    scanf("%d", &m);
    while (m--) {
        int opt, x, y, z;
        scanf("%d%d", &opt, &x);
        if (opt == 1) {
            scanf("%d%d", &y, &z);
            while (top[x] != top[y]) { //樹剖部分
                if (d[top[x]] < d[top[y]]) swap(x, y);
                Insert(id[top[x]], id[x], z, 1);
                x = fa[top[x]];
            }
            if (d[x] < d[y]) swap(x, y);
            Insert(id[y], id[x], z, 1); //剩下一段單獨處理
        }
        else if (opt == 2) {
            scanf("%d", &z);
            Insert(id[x], id[x] + sz[x] - 1, z, 1);
        }
        else if (opt == 3) {
            scanf("%d", &y);
            ll res = 0;
            while (top[x] != top[y]) {
                if (d[top[x]] < d[top[y]]) swap(x, y);
                res += Query(id[top[x]], id[x], 1);
                x = fa[top[x]];
            }
            if (d[x] < d[y]) swap(x, y);
            res += Query(id[y], id[x], 1);
            printf("%lld\n", res);
        }
        else printf("%lld\n", Query(id[x], id[x] + sz[x] - 1, 1));
    }
    return 0;
}

對於靜態的樹來說,樹剖還是很不錯的演算法。

動態樹

這就是大名鼎鼎的LCT(Link-Cut Tree)。支援動態地加邊、刪邊,併兼容樹鏈剖分的幾乎所有操作。時間複雜度僅為 \(O(N\log{N})\)
實際上LCT維護的是森林,以下以一棵樹進行討論。
與樹鏈剖分類似地,將所有邊分別實邊虛邊,每個節點可以向下連最多一條實邊,但也可以不連。實邊構成實邊鏈,每個節點包含在一條鏈中。
接下來就是LCT的關鍵。對每條實邊鏈,用一個Splay維護,Splay的中序遍歷對應鏈中從上往下的順序。實邊鏈中最上面的節點不一定是Splay的根節點。
然後,將這些Splay連線起來,構成一棵樹,以下稱為Splay的樹。對於每條實邊鏈,連線其最上面節點(設為 \(x\) )與其父節點(設為 \(y\) )的虛邊在Splay的樹中連線這條實邊鏈對應Splay的根節點與 \(y\)(連線方法之後討論)。而如何通過Splay的樹對應到原樹?實邊顯然可以通過每個Splay的中序遍歷得到,虛邊則通過每個Splay中最“左”的點與其根節點向上連到的點得到。
接著討論如何連線Splay的樹。在以下的操作中,要求每個Splay要隔離,但又要求向上追溯到原樹的根(為什麼?1.LCT維護的實際是森林;2.LCT是無根樹,之後的操作會進行“換根”)。我們的處理方法是:每個Splay的根節點向上的連邊僅有“向上連”而沒有“向下連”,即“認父不認子”。Splay和rotate中也要特殊判斷,防止轉錯。
以下介紹LCT的幾大操作。

access(int x)
作用是,在原樹中構造出從根到 \(x\) 的實邊路徑,且不再包含 \(x\) 的子節點。並將 \(x\) splay到其Splay的根。
\(x\) splay到根節點(僅為其splay根節點),找到其父節點 \(y\),將 \(y\) splay到根節點。則此時 \(y\) 一定沒有後繼。將 \(x\) 為根的子樹全部接到 \(y\) 的右兒子。不斷迴圈該操作即可。
makeroot(int x)
\(x\) 變成原樹中的根節點。
只需access(x),此時 \(x\) 到根的路徑構成一棵splay,將其翻轉即可。
findroot(int x)
找到 \(x\) 在原樹中的根節點。附屬作用,將根節點轉到splay中的根。
只需access(x),再一路向左即可。最後記得splay。
split(int x, int y)
\(x,y\) 在原樹中連通,則建立一條 \(x\)\(y\) 的實邊路徑。
makeroot(x),access(y)。
link(int x, int y)
\(x,y\) 不連通,加入邊 \((x,y)\)
先makeroot(x)。若findroot(y)!=x,則連線。因為不確定此時 \(x\) 是否有向下的實邊,故只能連虛邊。因為已經makeroot(x)了,故可以直接將 \(x\) 的父親設為 \(y\)
cut(int x, int y)
\(x,y\) 之間有邊,刪除此邊。
先makeroot(x)。若 \(x,y\) 有邊,則此時 findroot(y)==x。且因為前面的findroot(y),\(y\)\(x\) 一定在同一條實邊鏈上,則 \(y\)\(x\) 的後繼。且根據rotate的操作,\(y\) 一定為 \(x\) 的右子節點。於是:

void cut(int x, int y) {
    makeroot(x);
    if (findroot(y) == x && t[x].s[1] == y && !t[y].s[0]) {
        t[x].s[1] = t[y].p = 0; pushup(x);
    }
}

至此為LCT的所有操作。第一次寫程式碼時可能很難,但背模板還是可以的。LCT與網路流有些相似,不會在程式碼的實現上有很多變通,基本只有pushup和pushdown需要注意。
關於LCT的時間複雜度,為 \(O(N\log{N})\),常數較大。目前還不會證明。畢竟Splay就已經很玄學了,這麼多個Splay更玄學……

模板題
Code

#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e5 + 5;
int n, m, stack[N], top;
struct Node {
    int s[2], p, v, sum, rev;
} t[N];
bool isroot(int x) {
    return t[t[x].p].s[0] != x && t[t[x].p].s[1] != x;
}
void pushup(int x) {
    t[x].sum = t[t[x].s[0]].sum ^ t[t[x].s[1]].sum ^ t[x].v;
}
void pushrev(int x) {
    swap(t[x].s[0], t[x].s[1]); t[x].rev ^= 1;
}
void pushdown(int x) {
    if (!t[x].rev) return ;
    pushrev(t[x].s[0]); pushrev(t[x].s[1]); t[x].rev = 0;
}
void rotate(int x) {
    int y = t[x].p, z = t[y].p, k = t[y].s[1] == x;
    if (!isroot(y)) t[z].s[t[z].s[1] == y] = x; t[x].p = z;
    t[y].s[k] = t[x].s[k ^ 1]; t[t[x].s[k ^ 1]].p = y;
    t[x].s[k ^ 1] = y; t[y].p = x;
    pushup(y); pushup(x);
}
void splay(int x) {
    int p = x;
    stack[++top] = p;
    while (!isroot(p)) stack[++top] = p = t[p].p;
    while (top) pushdown(stack[top--]);
    while (!isroot(x)) {
        int y = t[x].p, z = t[y].p;
        if (!isroot(y))
            if (t[z].s[1] == y ^ t[y].s[1] == x) rotate(x);
            else rotate(y);
        rotate(x);
    }
}
void access(int x) {
    int z = x;
    for (int y = 0; x; y = x, x = t[x].p) {
        splay(x); t[x].s[1] = y; pushup(x);
    }
    splay(z);
}
void makeroot(int x) {
    access(x); pushrev(x);
}
int findroot(int x) {
    access(x);
    while (t[x].s[0]) {
        pushdown(x); x = t[x].s[0];
    }
    splay(x);
    return x;
}
void split(int x, int y) {
    makeroot(x); access(y);
}
void link(int x, int y) {
    makeroot(x);
    if (findroot(y) != x) t[x].p = y;
}
void cut(int x, int y) {
    makeroot(x);
    if (findroot(y) == x && t[x].s[1] == y && !t[y].s[0]) {
        t[x].s[1] = t[y].p = 0; pushup(x);
    }
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &t[i].v);
    while (m--) {
        int opt, x, y;
        scanf("%d%d%d", &opt, &x, &y);
        if (opt == 0) {
            split(x, y); printf("%d\n", t[y].sum);
        }
        else if (opt == 1) link(x, y);
        else if (opt == 2) cut(x, y);
        else {
            splay(x); t[x].v = y; pushup(x);
        }
    }
    return 0;
}

例:Acwing999 魔法森林
有一張 \(n\) 個節點 \(m\) 條邊的無向圖,每條邊有兩個權值 \(a_i,b_i\)。要求找到一條從 \(1\)\(n\) 的路徑,要求路徑上 \(a\) 的最大值與 \(b\) 的最大值的和最小。\(1≤n≤50000\)\(0≤m≤100000\)

首先,若 \(a\) 的最大值已經確定(設為 \(\bar{a}\)),則無向圖中可以走的邊是確定的。問題轉化為求解此圖中從 \(1\)\(n\) 的路徑使最大值最小,以 \(b\) 為權值。可以想到一種類似最小生成樹的做法。將邊按 \(b\) 從小到大排序,不斷加邊,直到連通,並查集維護。
可以想到二分。但是,此題中雖然隨著 \(\bar{a}\) 的增加圖中邊數增加,但同時最小生成樹的值會減小,最終答案不具有單調性。那麼對於所有 \(\bar{a}\),均需更新一次答案。邊數為 \(100000\),那麼就需要 \(log\) 級別的演算法。
那麼我們將所有邊按 \(a\) 排序,則 \(\bar{a}\) 不斷增大,不斷向圖中加邊。我們需要維護 \(1\)\(n\) 的最大值最小路徑。不難想到維護最小生成樹。加邊時,用類似求次小生成樹的方法,加入這條邊,若構成環,刪除環中權值最大的邊。
那麼,在最小生成樹中如何 \(\log{N}\) 維護 \(1\)\(n\) 最大值最小路徑呢?可以想到LCT。利用點邊轉化的技巧,將邊轉化為點,邊權轉化為點權,原來點的權值為0,則可以實現。
此題需要卡常,在判斷點連通時可以捨棄findroot,使用並查集。

Code

#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 5e4 + 5, M = 1e5 + 5;
int n, m, v[N + M], fa[N];
int stack[N + M], top;
struct Edge {
    int x, y, a, b;
    bool operator <(const Edge &o) const {
        return a < o.a;
    }
} e[M];
struct Node {
    int s[2], p, mx, rev;
} t[N + M];
int read() {
    int x = 0; char c = getchar();
    while (c < '0' || c > '9') c = getchar();
    while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
    return x;
}
int get(int x) {
    if (x == fa[x]) return x;
    return fa[x] = get(fa[x]);
}
bool isroot(int x) {
    return t[t[x].p].s[0] != x && t[t[x].p].s[1] != x;
}
void pushrev(int x) {
    swap(t[x].s[0], t[x].s[1]); t[x].rev ^= 1;
}
void pushup(int x) {
    t[x].mx = x;
    for (int i = 0; i < 2; i++)
        if (v[t[t[x].s[i]].mx] > v[t[x].mx])
            t[x].mx = t[t[x].s[i]].mx;
}
void pushdown(int x) {
    if (!t[x].rev) return ;
    pushrev(t[x].s[0]); pushrev(t[x].s[1]); t[x].rev = 0;
}
void rotate(int x) {
    int y = t[x].p, z = t[y].p, k = t[y].s[1] == x;
    if (!isroot(y)) t[z].s[t[z].s[1] == y] = x; t[x].p = z;
    t[y].s[k] = t[x].s[k ^ 1]; t[t[x].s[k ^ 1]].p = y;
    t[x].s[k ^ 1] = y; t[y].p = x;
    pushup(y); pushup(x);
}
void splay(int x) {
    int p = x;
    stack[++top] = p;
    while (!isroot(p)) stack[++top] = p = t[p].p;
    while (top) pushdown(stack[top--]);
    while (!isroot(x)) {
        int y = t[x].p, z = t[y].p;
        if (!isroot(y))
            if (t[z].s[1] == y ^ t[y].s[1] == x) rotate(x);
            else rotate(y);
        rotate(x);
    }
}
void access(int x) {
    int z = x;
    for (int y = 0; x; y = x, x = t[x].p) {
        splay(x); t[x].s[1] = y; pushup(x);
    }
    splay(z);
}
void makeroot(int x) {
    access(x); pushrev(x);
}
int findroot(int x) {
    access(x);
    while (t[x].s[0]) {
        pushdown(x); x = t[x].s[0];
    }
    splay(x);
    return x;
}
void split(int x, int y) {
    makeroot(x); access(y);
}
void link(int x, int y) {
    makeroot(x);
    if (findroot(y) != x) t[x].p = y;
}
void cut(int x, int y) {
    makeroot(x);
    if (findroot(y) == x && t[x].s[1] == y && !t[y].s[0]) {
        t[x].s[1] = t[y].p = 0; pushup(x);
    }
}
int main() {
    int ans = 0x7fffffff;
    n = read(); m = read();
    for (int i = 1; i <= m; i++) e[i] = (Edge){read(), read(), read(), read()};
    sort(e + 1, e + m + 1);
    for (int i = 1; i <= n + m; i++) {
        t[i].mx = i;
        if (i <= n) fa[i] = i;
        else v[i] = e[i - n].b;
    }
    for (int i = 1; i <= m; i++) {
        int x = e[i].x, y = e[i].y;
        if (get(x) == get(y)) {
            split(x, y);
            if (v[t[y].mx] > e[i].b) {
                int p = t[y].mx;
                cut(e[p - n].x, p); cut(e[p - n].y, p);
                link(x, i + n); link(y, i + n);
            }
        }
        else {
            link(x, i + n); link(y, i + n); fa[get(x)] = get(y);
        }
        if (get(1) == get(n)) {
            split(1, n); ans = min(ans, e[i].a + v[t[n].mx]);
        }
    }
    printf("%d\n", ans == 0x7fffffff ? -1 : ans);
    return 0;
}