前置知識
線段樹 \(and\) 樹上基本操作
定義
幾個在樹鏈剖分很重要的概念。
重兒子
對於一個父節點,含有節點數最多的兒子稱為重兒子。但重兒子只有一個,若滿足條件的兒子有多個,則指定其中任意一個兒子為重兒子。
輕兒子
對於一個父節點,除了重兒子以為,其餘的都稱為輕兒子。
重邊
由父節點與重兒子構成的邊。
輕邊
由父節點與輕兒子構成的邊。
重鏈
由重邊構成的鏈。
輕鏈
由輕邊構成的鏈。
鏈頂
重鏈中深度最小的邊為該重鏈的鏈頂。
上述幾個概念具體如下圖:
其中,黃色點為重兒子,藍色點為輕兒子( \(1\) 除外)。黃色邊為重邊,藍色邊為輕邊。通常,一個單獨的點也看為一條重鏈,那麼重鏈有 \(5\) 條:
- \(\{ 1,2,6,7\}\) ,( \(1\) 為鏈頂)
- \(\{ 5\}\) ,( \(5\) 為鏈頂)
- \(\{ 3\}\) ,( \(3\) 為鏈頂)
- \(\{ 4,8\}\) ,( \(4\) 為鏈頂)
- \(\{ 9\}\) ,( \(9\) 為鏈頂)
對於任意一棵樹有如下性質:從任意一點到根節點的簡單路徑上,共有不超過 \(log2(n)\) 條輕鏈,有不超過 \(log2(n)\) 條輕鏈。
預處理
預處理需要使用到兩個 \(dfs\) 。
\(dfs1\) 需要處理:
- \(fa[i]\) : \(i\) 節點的父親。
- \(son[i]\) : \(i\) 節點的重兒子。
- \(sz[i]\) :以 \(i\) 為根節點的子樹的大小,可以進一步處理 \(son[i]\) 。
- \(dep[i]\) : \(i\) 節點的深度。
\(dfs2\) 需要處理:
- \(dfn[i]\) : \(i\) 節點的時間戳,但優先遍歷重兒子。顯然,在一條重鏈中,時間戳為一段連續的數字。
- \(tp[i]\) : \(i\) 節點的鏈頂。
C++程式碼
void dfs1(int now, int father) {
fa[now] = father;//初始化父節點
sz[now] = 1;//子樹大小包括自己
dep[now] = dep[father] + 1;//初始化深度
int SIZ = v[now].size();
int maxn = 0;//記錄最大的子樹大小
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(next == father)
continue;
dfs1(next, now);//遍歷這棵樹
sz[now] += sz[next];
if(maxn < sz[next]) {//更新子節點
maxn = sz[next];
son[now] = next;
}
}
}
void dfs2(int now, int Top) {
tp[now] = Top;//初始化鏈頂
if(son[now])
dfs2(son[now], Top);//優先遍歷重兒子
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(next == fa[now] || next == son[now])
continue;
dfs2(next, next);//繼續遍歷這棵樹
}
}
樹鏈剖分求LCA
線上查詢一棵樹上任意兩點的 \(LCA\) 。
分兩種情況向上爬即可(需要保證 \(dep[tp[x]] <= dep[tp[y]]\)):
- \(tp[x]!=tp[y]\) ,即不在一條重鏈上。需要 \(x\) 向上爬出這條重鏈,向上繼續尋找能與 \(y\) 匯合的重鏈點。\(x=fa[tp[x]]\)。
- \(tp[x]!=tp[y]\) ,即在一條重鏈上,那麼深度小的就位最近公共祖先。
正確性顯然,以為兩條重鏈不會交於同一個點。
C++程式碼
#include <cstdio>
#include <vector>
using namespace std;
const int MAXN = 5e5 + 5;
vector<int> v[MAXN];//vector存圖
int fa[MAXN], son[MAXN], tp[MAXN], sz[MAXN], dep[MAXN];
int n, m, s;
void dfs1(int now, int father) {//初始化如上
fa[now] = father;
sz[now] = 1;
dep[now] = dep[father] + 1;
int SIZ = v[now].size();
int maxn = 0;
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(next == father)
continue;
dfs1(next, now);
sz[now] += sz[next];
if(maxn < sz[next]) {
maxn = sz[next];
son[now] = next;
}
}
}
void dfs2(int now, int Top) {//初始化如上
tp[now] = Top;
if(son[now])
dfs2(son[now], Top);
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(next == fa[now] || next == son[now])
continue;
dfs2(next, next);
}
}
int Get_LCA(int x, int y) {
while(tp[x] != tp[y]) {
if(dep[tp[x]] < dep[tp[y]])
swap(x, y);
x = fa[tp[x]];
}
if(x == y)
return x;
if(dep[x] < dep[y])
return x;
return y;
}
int main() {
int A, B;
scanf("%d %d %d", &n, &m, &s);
for(int i = 1; i < n; i++) {
scanf("%d %d", &A, &B);
v[A].push_back(B);
v[B].push_back(A);
}
dfs1(s, 0);
dfs2(s, s);
for(int i = 1; i <= m; i++) {
scanf("%d %d", &A, &B);
printf("%d\n", Get_LCA(A, B));
}
return 0;
}
例題
樹鏈剖分通常結合著一些資料結構來進行操作,以為重鏈的 \(dfn\) 為連續的序列。
題目大意
對於一棵樹,有 \(5\) 中操作,根據要求完成操作。
- C i w 將輸入的第 \(i\) 條邊權值改為 \(w\) 。
- N u v 將 \(u,v\) 節點之間的邊權都變為相反數。
- SUM u v 詢問 \(u,v\) 節點之間邊權和。
- MAX u v 詢問 \(u,v\) 節點之間邊權最大值。
- MIN u v 詢問 \(u,v\) 節點之間邊權最小值。
思路
先考慮第二個操作。
設 \(lca\) 為 \(u,v\) 的最近公共祖先,那麼可以將操作二分解為從 \(u\) 到 \(lca\) 的路徑取反,和將從 \(v\) 到 \(lca\) 的路徑取反。
那麼按照上述 \(LCA\) 往上爬的過程剛好就可以遍歷完這條路徑一次。
按照點的 \(dfn\) 建造一顆線段樹,來維護點的資訊。
這裡點的資訊是指:這個點與它的父節點的連邊的資訊。
線段樹需要維護的資訊有:最大值,最小值,區間和。
具體的操作二程式碼如下:
void Negate(int pos, int l, int r) {
if(l <= L(pos) && R(pos) <= r) {
A(pos) = -A(pos);
I(pos) = -I(pos);
swap(A(pos), I(pos));//最大值取反,最小值取反,最大值邊最小值
S(pos) = -S(pos);//區間和取反
M(pos) ^= 1;//取反兩次後就相當於不取反。
return;
}
Push_Down(pos);//傳遞懶標記
if(l <= R(LC(pos)))
Negate(LC(pos), l, r);
if(r >= L(RC(pos)))
Negate(RC(pos), l, r);
Push_Up(pos);//修改後更新點的資訊
}
void Negatepast(int x, int y) {
while(tp[x] != tp[y]) {//不同重鏈向上爬
if(dep[tp[x]] < dep[tp[y]])
swap(x, y);
Negate(1, dfn[tp[x]], dfn[x]);//同一條重鏈dfn是連續的,線段樹維護
x = fa[tp[x]];//向上爬
}
if(x == y)
return;
if(dep[x] < dep[y])
swap(x, y);
Negate(1, dfn[son[y]], dfn[x]);//注意是son[y],y與其父節點的連邊並不需要修改
}
查詢操作與其類似,就不一一列舉了。
對於修改權值,使用深度較大的子節點,直接線上段樹上該就好了。
C++程式碼
#include <cstdio>
#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define INF 0x3f3f3f3f
const int MAXN = 2e5 + 5;
struct Segment_Tree {//線段樹
int Left_Section, Right_Section;
int Max_Data, Min_Data, Sum_Data, Lazy_Mark;
#define LC(x) (x << 1)
#define RC(x) (x << 1 | 1)
#define L(x) Tree[x].Left_Section//左區間
#define R(x) Tree[x].Right_Section//右區間
#define I(x) Tree[x].Min_Data//區間最小值
#define A(x) Tree[x].Max_Data//區間最大值
#define S(x) Tree[x].Sum_Data//區間和
#define M(x) Tree[x].Lazy_Mark//懶惰標記(延遲標記)
};
Segment_Tree Tree[MAXN << 2];
vector<int> v[MAXN];//vector存圖
int fa[MAXN], son[MAXN], dep[MAXN], siz[MAXN];
int tp[MAXN], dfn[MAXN];
int s[MAXN], t[MAXN], w[MAXN];
int n, q;
int tim;
void Push_Up(int pos) {//更新節點資訊
A(pos) = max(A(LC(pos)), A(RC(pos)));
I(pos) = min(I(LC(pos)), I(RC(pos)));
S(pos) = S(LC(pos)) + S(RC(pos));
}
void Push_Down(int pos) {//傳遞懶標記
if(M(pos)) {
I(LC(pos)) = -I(LC(pos));
A(LC(pos)) = -A(LC(pos));
swap(I(LC(pos)), A(LC(pos)));
S(LC(pos)) = -S(LC(pos));
M(LC(pos)) ^= 1;
I(RC(pos)) = -I(RC(pos));
A(RC(pos)) = -A(RC(pos));
swap(I(RC(pos)), A(RC(pos)));
S(RC(pos)) = -S(RC(pos));
M(RC(pos)) ^= 1;
M(pos) = 0;
}
}
void Build(int pos, int l, int r) {//初始化建樹
L(pos) = l;
R(pos) = r;
if(l == r)
return;
int mid = (l + r) >> 1;
Build(LC(pos), l, mid);
Build(RC(pos), mid + 1, r);
}
void Negate(int pos, int l, int r) {//取反操作
if(l <= L(pos) && R(pos) <= r) {
I(pos) = -I(pos);
A(pos) = -A(pos);
swap(I(pos), A(pos));
S(pos) = -S(pos);
M(pos) ^= 1;
return;
}
Push_Down(pos);
if(l <= R(LC(pos)))
Negate(LC(pos), l, r);
if(r >= L(RC(pos)))
Negate(RC(pos), l, r);
Push_Up(pos);
}
void Negatepast(int x, int y) {
while(tp[x] != tp[y]) {
if(dep[tp[x]] < dep[tp[y]])
swap(x, y);
Negate(1, dfn[tp[x]], dfn[x]);
x = fa[tp[x]];
}
if(x == y)
return;
if(dep[x] < dep[y])
swap(x, y);
Negate(1, dfn[son[y]], dfn[x]);
}
void Change(int pos, int x, int c) {//單點修改
if(L(pos) == R(pos)) {
I(pos) = c;
A(pos) = c;
S(pos) = c;
return;
}
Push_Down(pos);
if(x <= R(LC(pos)))
Change(LC(pos), x, c);
else
Change(RC(pos), x, c);
Push_Up(pos);
}
int Query_Sum(int pos, int l, int r) {//查詢最大值操作,與修改類似,都已同樣方向爬
if(l <= L(pos) && R(pos) <= r)
return S(pos);
Push_Down(pos);
int res = 0;
if(l <= R(LC(pos)))
res += Query_Sum(LC(pos), l, r);
if(r >= L(RC(pos)))
res += Query_Sum(RC(pos), l, r);
return res;
}
int Sumpast(int x, int y) {
int res = 0;
while(tp[x] != tp[y]) {
if(dep[tp[x]] < dep[tp[y]])
swap(x, y);
res += Query_Sum(1, dfn[tp[x]], dfn[x]);
x = fa[tp[x]];
}
if(x == y)
return res;
if(dep[x] < dep[y])
swap(x, y);
res += Query_Sum(1, dfn[son[y]], dfn[x]);
return res;
}
int Query_Min(int pos, int l, int r) {//查詢最小值
if(l <= L(pos) && R(pos) <= r)
return I(pos);
Push_Down(pos);
int res = INF;
if(l <= R(LC(pos)))
res = min(res, Query_Min(LC(pos), l, r));
if(r >= L(RC(pos)))
res = min(res, Query_Min(RC(pos), l, r));
return res;
}
int Minpast(int x, int y) {
int res = INF;
while(tp[x] != tp[y]) {
if(dep[tp[x]] < dep[tp[y]])
swap(x, y);
res = min(res, Query_Min(1, dfn[tp[x]], dfn[x]));
x = fa[tp[x]];
}
if(x == y)
return res;
if(dep[x] < dep[y])
swap(x, y);
res = min(res, Query_Min(1, dfn[son[y]], dfn[x]));
return res;
}
int Query_Max(int pos, int l, int r) {//查詢最大值
if(l <= L(pos) && R(pos) <= r)
return A(pos);
Push_Down(pos);
int res = -INF;
if(l <= R(LC(pos)))
res = max(res, Query_Max(LC(pos), l, r));
if(r >= L(RC(pos)))
res = max(res, Query_Max(RC(pos), l, r));
return res;
}
int Maxpast(int x, int y) {
int res = -INF;
while(tp[x] != tp[y]) {
if(dep[tp[x]] < dep[tp[y]])
swap(x, y);
res = max(res, Query_Max(1, dfn[tp[x]], dfn[x]));
x = fa[tp[x]];
}
if(x == y)
return res;
if(dep[x] < dep[y])
swap(x, y);
res = max(res, Query_Max(1, dfn[son[y]], dfn[x]));
return res;
}
void dfs1(int now, int father) {//初始化
dep[now] = dep[father] + 1;
siz[now] = 1;
fa[now] = father;
int SIZ = v[now].size();
int maxn = 0;
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(next == fa[now])
continue;
dfs1(next, now);
siz[now] += siz[next];
if(maxn < siz[next]) {
maxn = siz[next];
son[now] = next;
}
}
}
void dfs2(int now, int Top) {//初始化
tp[now] = Top;
dfn[now] = ++tim;
if(son[now])
dfs2(son[now], Top);
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(son[now] == next || fa[now] == next)
continue;
dfs2(next, next);
}
}
int main() {
scanf("%d", &n);
for(int i = 1; i < n; i++) {
scanf("%d %d %d", &s[i], &t[i], &w[i]);
s[i]++;
t[i]++;
v[s[i]].push_back(t[i]);
v[t[i]].push_back(s[i]);
}
dfs1(1, 0);
dfs2(1, 1);
Build(1, 1, n);
for(int i = 1; i < n; i++) {
if(dep[s[i]] < dep[t[i]])//深度小的一定就是子節點
swap(s[i], t[i]);
Change(1, dfn[s[i]], w[i]);//初始化線段樹
}
scanf("%d", &q);
string opt;
int a, b;
while(q--) {
cin >> opt;
scanf("%d %d", &a, &b);
a++;
b++;
if(opt[0] == 'N')
Negatepast(a, b);
else if(opt[0] == 'C')
Change(1, dfn[s[a - 1]], b - 1);
else if(opt[0] == 'S')
printf("%d\n", Sumpast(a, b));
else if(opt[1] == 'A')
printf("%d\n", Maxpast(a, b));
else
printf("%d\n", Minpast(a, b));
}
return 0;
}