本文參考自 梁晏成《樹上數據結構》 ,感謝他在雅禮集訓的講解。
轉化成序列問題
dfs序
按照 \(dfs\) 的入棧順序形成一個序列。
例如對於這棵樹
它的 \(dfs\) 序就是 \(1~2~3~4~5~6~7~8\) 。(假設我遍歷兒子是從左到右的)
樹鏈剖分的運用
對於這個我們常常配合 樹鏈剖分 來使用。
這樣對於一個點,它的子樹編號是連續的一段區間,便於做子樹修改以及查詢問題。
重鏈上所有節點的標號也是連續的一段區間。
所以我們可以解決大部分鏈或子樹修改以及查詢的問題,十分的優秀。
也就是常常把樹上問題轉化成序列問題的通用解法。
括號序列
\(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\) 子樹修改單點查詢
這個如何理解呢,例如對於這顆樹。
我們考慮對於修改 \(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)\) 。
這個證明。。。我不會qwqfakesky 說可以反證法來證明?
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)\) 的級別,如圖:
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
還是點進去看看。。。
我們每次考慮一個點,需要對於它子樹所有狀態進行考慮的時候,可以使用線段樹合並。
然后對於這個線段樹就能滿足子樹合並的性質。
一些鬼畜的線段樹有可能也可以寫出神奇的合並方法。