樹鏈剖分&動態樹學習筆記
如何將樹上的一段路徑轉化為區間問題?我們可能會想到樹上莫隊中利用尤拉序性質的做法,但其不具有普適性,對於很多區間問題,難以將出現兩次的元素減掉。而樹鏈剖分與動態樹都可以很好地解決這類問題。
樹鏈剖分
樹鏈剖分也稱為重鏈剖分,適用於形態結構不發生變化的樹(即靜態)。
將樹上所有邊分為重邊和輕邊,每個節點與其子節點的所有連邊中,有且僅有一條重邊(葉子結點除外),其餘為輕邊。重邊的求法:對於每個節點,考察其所有子節點,子樹中包含節點最多的子節點稱為“最重”。重邊連線此節點與其“最重”的子節點。若有多個子節點同為“最重”,任選其一。
連在一起的重邊從上而下形成重鏈。所有節點都包含在重鏈中,若一個節點上下均無重邊,則其獨自構成一個重鏈。此時有性質:從根節點出發的任意一條路徑“穿過”的重鏈數不超過 \(\log{N}\)
證明:每離開一條重鏈,走進另一條重鏈,必然使得其子樹大小變為原來的 \(\frac{1}{k}\),其中 \(k\) 表示當前節點的子節點個數,且必然 \(≥2\)。於是得證。
對此樹求DFS序(注意不是尤拉序,每個節點僅出現一次)。DFS優先拓展重邊。此時有性質:一條重鏈上的所有節點在DFS序中連在一起。
於是,樹上的任意一條路徑,一定可以被拆分為至多 \(\log{N}\) 個連續區間。我們對每個區間分別求解,最後加在一起,即為答案。
對於DFS序中的元素,常用線段樹/樹狀陣列/平衡樹等 \(\log{N}\) 級別的資料結構維護。故樹鏈剖分的時間複雜度為 \(O(N\log^2{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;
}