树链剖分

树链剖分

概念

树链剖分(重链剖分)是一个用来解决树上多点修改与查询(树上区间修改与查询)在线算法。

提到区间修改,线段树无疑是一个很优秀的方案,但线段树只支持维护一串连续的序列。而树剖就是一个将树拍平成一个序列进行处理的算法。

算法思想

先来用一个很简单的拍平方法:用 DFS序 对书中每一个节点赋予一个编号 \(dfn_u\),如下图:

graph TB 1((1)) 2((2)) 3((3)) 4((4)) 5((5)) 6((6)) 1---2 2---3 1---4 4---5 4---6

此时,很容易处理子树的修改与查询操作:

DFS 的性质可以得出:在一棵子树内,所有节点编号连续且根的编号最小。所以这颗子树在序列中对应的区间是:

\[[dfn_u,dfn_u+size_u-1]\quad (size_u\text{为以u为根的子树的大小}) \]

链的修改与查询操作就不行了。

这时,就需要用到树链剖分的一个核心概念:重儿子与轻儿子。重儿子是指在一个节点的所有儿子中,子树大小最大的那一个

DFS序 遍历时,优先枚举重儿子,再枚举轻儿子。这样,所有重儿子都会相连组成多条重链,这些链中的节点的编号一定是连续的。而整棵树则变成了一条条重链上挂着轻儿子,而下图中:

graph TB 1[1] 2[2] 3[3] 4((4)) 5[5] 6[6] 7[7] 1---2 2---4 2---5 5---6 1---3 3---7

\(\{1,2,5,6\}\)\(\{3,7\}\) 构成两条重链。

在操作路径 \(u\longrightarrow v\) 时,分为两种情况:

  • \(u\)\(v\) 在同一重链上,这时可以直接算区间(重链中编号连续)

  • \(u\)\(v\) 不在同一重链上,我们需要让它们跳到同一条重链上,此时类比求LCA

    \(u\)\(v\) 中所属重链顶端较低的一个节点开始(防止跳过)。先直接跳到该点所属重链的顶端并统计当前答案,再向上跳一。重复这个过程直到\(u\)\(v\) 在同一重链上。

时间复杂度

显然,如果节点 \(v\) 是重儿子,则它与其父亲 \(u\) 的不等关系可以表示为:

\[size_u\geq 2\cdot size_v \]

在极限情况下取等时,共有约 \(log_2n\) 条重链,轻链在极限情况下与重链数目相同,再算上线段树处理区间的 \(log_2n\) 后,时间复杂度是:\(O(2log_2^2n)\)

代码

参考例题:洛谷 P3384 【模板】重链剖分/树链剖分

#include <bits/stdc++.h>
#define ll long long
#define lson(x) (x * 2)
#define rson(x) (x * 2 + 1)
using namespace std;
const int maxn = 1e5;
// 基本
int n,q,root; ll mod;
vector<int> G[maxn + 5];
// 线段树部分
ll a0[maxn + 5],a[maxn + 5];
ll tree[4 * maxn + 5],tag[4 * maxn + 5];
void push_up(int u) { tree[u] = (tree[lson(u)] % mod + tree[rson(u)] % mod) % mod; }
void push_down(int u,int l,int r) {
	if (tag[u]) {
		int mid = (l + r) / 2;
		tree[lson(u)] += tag[u] * (ll)(mid - l + 1); tree[lson(u)] %= mod; 
		tree[rson(u)] += tag[u] * (ll)(r - mid); tree[rson(u)] %= mod; 
		tag[lson(u)] += tag[u]; tag[lson(u)] %= mod; 
		tag[rson(u)] += tag[u]; tag[rson(u)] %= mod; 
		tag[u] = 0ll;
	}
}
void build(int u,int l,int r) {
	if (l == r) {
		tree[u] = a[l] % mod;
		return ;
	}
	int mid = (l + r) / 2;
	build(lson(u),l,mid);
	build(rson(u),mid + 1,r);
	push_up(u);
}
void update(int u,int l,int r,int ql,int qr,ll val) {
	if (l > qr || r < ql) return ;
	if (ql <= l && r <= qr) {
		tree[u] += val * (r - l + 1); tree[u] %= mod;
		tag[u] += val; tag[u] %= mod;	
		return ;
	}
	push_down(u,l,r);
	int mid = (l + r) / 2;
	update(lson(u),l,mid,ql,qr,val);
	update(rson(u),mid + 1,r,ql,qr,val);
	push_up(u);
}
ll query(int u,int l,int r,int ql,int qr) {
	if (l > qr || r < ql) return 0ll;
	if (ql <= l && r <= qr) return tree[u];
	push_down(u,l,r);
	int mid = (l + r) / 2;
	return (query(lson(u),l,mid,ql,qr) % mod + query(rson(u),mid + 1,r,ql,qr) % mod) % mod;
}
// 预处理
// 父亲 子树大小 重儿子 深度
int fa[maxn + 5],size[maxn + 5],hson[maxn + 5],depth[maxn + 5]; 
void dfs1(int u,int f) {
	size[u] ++;
	fa[u] = f;
	for (auto v : G[u]) {
		if (v == f) continue;
		depth[v] = depth[u] + 1;
		dfs1(v,u);
		size[u] += size[v];
		if (size[v] > size[hson[u]]) hson[u] = v;
	}
} 
int dfn[maxn + 5],dfncnt = 0; // dfs序号 
int htop[maxn + 5]; // 重链顶端节点 
void dfs2(int u,int f,int top) {
	dfn[u] = ++ dfncnt;
	htop[u] = top;
	if (hson[u]) dfs2(hson[u],u,top);
	for (auto v : G[u]) {
		if (v == f || v == hson[u]) continue;
		dfs2(v,u,v);
	}
} 
// 路径更新
void update_path(int u,int v,ll val) {
	while (htop[u] != htop[v]) { // 不在同一重链
		int tu = htop[u],tv = htop[v];
		if (depth[tu] < depth[tv]) swap(u,v),swap(tu,tv); // 取得较深的节点
		update(1,1,n,dfn[tu],dfn[u],val); // 更新该节点走过的答案
		u = fa[tu];
	}
	if (depth[u] < depth[v]) swap(u,v); 
	update(1,1,n,dfn[v],dfn[u],val); // 在同一重链的最终答案
}
// 路径查询(与路径更新相似)
ll query_path(int u,int v) {
	ll ans = 0ll;
	while (htop[u] != htop[v]) {
		int tu = htop[u],tv = htop[v];
		if (depth[tu] < depth[tv]) swap(u,v),swap(tu,tv);
		ans += query(1,1,n,dfn[tu],dfn[u]); ans %= mod;
		u = fa[tu];
	}
	if (depth[u] < depth[v]) swap(u,v); 
	ans += query(1,1,n,dfn[v],dfn[u]); ans %= mod;
	return ans;
}
int main() {
	scanf("%d %d %d %lld",&n,&q,&root,&mod);
	for (int i = 1;i <= n;i ++) scanf("%lld",&a0[i]);
	for (int i = 1;i <= n - 1;i ++) {
		int u,v; scanf("%d %d",&u,&v);
		G[u].push_back(v); G[v].push_back(u);
	}
	dfs1(root,0);
	dfs2(root,0,root);
	for (int i = 1;i <= n;i ++) a[dfn[i]] = a0[i];
	build(1,1,n);
	for (int i = 1;i <= q;i ++) {
		int opt; scanf("%d",&opt);
		if (opt == 1) {
			int u,v; ll val; scanf("%d %d %lld",&u,&v,&val);
			update_path(u,v,val);
		} else if (opt == 2) {
			int u,v; scanf("%d %d",&u,&v);
			printf("%lld\n",query_path(u,v));
		} else if (opt == 3) {
			int u; ll val; scanf("%d %lld",&u,&val);
			update(1,1,n,dfn[u],dfn[u] + size[u] - 1,val); // 子树更新
		} else if (opt == 4) {
			int u; scanf("%d",&u);
			printf("%lld\n",query(1,1,n,dfn[u],dfn[u] + size[u] - 1)); // 子树查询
		}
	}
	return 0;
}
posted @ 2026-08-27 07:21  Denia-kawaii  阅读(4)  评论(0)    收藏  举报