樹鏈剖分解析

Miusybaby發表於2021-03-06

前置知識

線段樹 \(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]]\)):

  1. \(tp[x]!=tp[y]\) ,即不在一條重鏈上。需要 \(x\) 向上爬出這條重鏈,向上繼續尋找能與 \(y\) 匯合的重鏈點。\(x=fa[tp[x]]\)
  2. \(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;
}

相關文章