树链剖分总结

树链剖分

一、树链剖分

1. 定义

树链剖分即为将一棵树分割为若干条树链;

树链剖分是一种常见的数据结构,对于路上路径的修改及路径信息的查询等问题有着较优的时间复杂度;

树链剖分分为两种,重链剖分与长链剖分,通常,树链剖分指的是重链剖分;

2. 相关定义

重儿子

每个节点的子树中,子树大小最大的子节点;

如果多个子树节点数同样多,则任意一个均可作为重儿子,一个点也可看作一条重链;

轻儿子

除重儿子外的其他子节点;

重边

节点与其重儿子间的边;

轻边

节点与其轻儿子间的边;

重链

重边连成的链;

轻链

轻边连成的链;

二、重链剖分

1. 定义

重链剖分即为将树按照重链进行剖分,最终剖分成许多重链;

2. 实现

进行重链剖分,先预处理出每个节点的重儿子;

然后对树进行一次 DFS 遍历,优先遍历其重儿子,再遍历其他节点;

预处理重儿子

即在 DFS 遍历时统计节点的子树大小,深度,从而得到重儿子;

void dfs1(int i) {
	size[i] = 1; // 初始子树大小
	flag[i] = true;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
			fa[v] = i;
			dep[v] = dep[i] + 1; // 节点深度
			dfs1(v);
			size[i] += size[v];
			if (size[v] > size[son[i]]) { // 更新节点 i 的重儿子
				son[i] = v;
			}
		}
	}
	return;
}
处理重链

若顶点有重儿子,则先 DFS 遍历其重儿子,得到重链,再遍历其的子节点;

void dfs2(int i, int tp) { // 节点 i 所在的重链为 tp
	top[i] = tp; // 存储重链顶端
	if (son[i]) dfs2(son[i], tp); // 有重儿子,遍历重儿子,顶点不变
	// 遍历轻儿子,让轻儿子作为链头继续遍历
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (v != fa[i] && v != son[i]) {
			dfs2(v, v);
		}
	}
	return;
}

3. 性质

  1. 所有的重链互不相交,每个点只属于一个重链;

  2. 所有重链上的节点数和为树上的总结点数;

  3. 一个节点到根路径经过的轻边最多为 l o g n logn logn 条;

    假设 ( u , v ) (u, v) (u,v) 为一条轻边,则有 s i z e [ v ] ∗ 2 ≤ s i z e [ u ] size[v] * 2 \leq size[u] size[v]2size[u] ,所以一个节点到根路径经过的轻边最多为 l o g n logn logn 条;

4. 应用

LCA
1. 思路

由于可将 LCA 看做两节点所在的链的交点,则可使用树链剖分;

通过将两点向上跳到其所在重链的链头,直到两点跳到同一条重链上时,取深度小的节点即为两点 LCA ;

则树链剖分的作用就是在向上跳跃的过程中,若遇到了重链,可以直接从重链的低端跳跃到重链的顶端,加速向上跳跃的过程,与倍增求 LCA 加速向上跳跃过程不同;

由于重链跳到链头,则跳的时间复杂度为 O ( 1 ) O(1) O(1) ,而跳轻边最多为 l o g n logn logn 跳,则时间复杂度为 O ( l o g n ) O(logn) O(logn)

2. 过程

先预处理出节点的父节点,深度,重儿子;

在查询中,步骤1,查看节点 x x x 与节点 y y y 是否在同一重链上;

  1. 若在,则 x x x y y y 的 LCA 即为此时深度较低的节点;
  2. 若不在,则将深度较大的节点调整到其所在重链顶端节点的父节点上,重复步骤 1 即可;
3.代码
int n, root, q, fa[MAXN], size[MAXN], son[MAXN], top[MAXN], dep[MAXN];
vector <int> g[MAXN];
bool flag[MAXN];
void dfs1(int i) {
	flag[i] = true;
	size[i] = 1;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
			fa[v] = i; // 预处理父节点
			dep[v] = dep[i] + 1; // 预处理深度
			dfs1(v);
			size[i] += size[v]; // 预处理子树大小
			if (size[i] > size[son[i]]) {
				son[i] = v; // 判断重儿子
			}
		}
	}
	return;
}
void dfs2(int i, int tp) {
	top[i] = tp; // 树链顶端
	if (son[i]) dfs2(son[i], tp); // 节点 x 有重儿子,则先从重儿子向下搜索
	for (int t = 0; t < g[i].size(); t++) { // 遍历 x 的轻儿子,让轻儿子作为某条链的链头继续搜索
		int v = g[i][t];
		if (v != fa[i] && v != son[i]) { // 节点不为 x 的重儿子或父节点
			dfs2(v, v);
		}
	}
	return;
}
int LCA(int u, int v) {
	while (top[u] != top[v]) { // 判断是否在同一条重链上
		if (dep[top[u]] < dep[top[v]]) {
			swap(u, v); // 优先跳深度较低的点
		}
		u = fa[top[u]];
	}
	return dep[u] < dep[v] ? u : v; // 在同一条重链上时,深度较低的节点为 LCA
}
int main() {
	scanf("%d %d %d", &n, &q, &root);
	for (int i = 1; i < n; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	dfs1(root);
	dfs2(root, root);
	for (int i = 1; i <= q; i++) {
		int u, v;
		scanf("%d %d", &u, &v);
		printf("%d\n", LCA(u, v));
	}
	return 0;
} 

维护树上路径信息
1. 思路

可以将两点间的路径看作两点向上走到所在树链交点得到的路径;

由于维护树上信息,想到 DFS 序,其可将树上的统计转化为线性结构中的区间统计;

则只需找到一种办法,使得可以通过区间修改的方式修改树链上的值即可解决;

由于在计算重链顶端编号的 DFS 中,优先遍历重儿子,所以在这样搜索的 DFS 序中,每条重链在 DFS 序中都是连续的一段 ,重链顶端在左,重链底端在右;

这样得到的 DFS 序称为树链剖分 DFS 序;

其也符合普通 DFS 序的特征,同一棵子树在 DFS 序中是连续的一段 ;

则可以对树剖序建线段树,通过树链剖分求 LCA 过程中进行路径信息修改或询问;

由于单次修改或查询线段树时间复杂度是 O ( l o g n ) O(logn) O(logn) ,则单次对路径修改或查询时间复杂度就是 O ( l o g 2 n ) O(log^2n) O(log2n)

2. 过程

先预处理出节点的父节点,深度,重儿子,以及树的树链剖分 DFS 序;

在修改或查询中,通过类似于求 LCA 的方法,每次跳时,查询或修改跳过的树链上的值,直到两点重合为止;

3. 代码

以单点修改,维护区间最大值和和为例;

#include <cstdio>
#include <vector>
#include <algorithm>
#define MAXN 100005
#define INF 2147483647
using namespace std;
int n, q, w[MAXN], dep[MAXN], fa[MAXN], size[MAXN], top[MAXN], son[MAXN], s[MAXN], id[MAXN], len;
vector <int> g[MAXN];
bool flag[MAXN];
void dfs1(int i) {
	size[i] = 1; // 初始子树大小
	flag[i] = true;
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!flag[v]) {
			fa[v] = i;
			dep[v] = dep[i] + 1; // 节点深度
			dfs1(v);
			size[i] += size[v];
			if (size[v] > size[son[i]]) { // 更新节点 i 的重儿子
				son[i] = v;
			}
		}
	}
	return;
}
void dfs2(int i, int tp) { // 节点 i 所在的重链为 tp
	top[i] = tp; // 存储重链顶端
	if (son[i]) dfs2(son[i], tp); // 有重儿子,遍历重儿子,顶点不变
	// 遍历轻儿子,让轻儿子作为链头继续遍历
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (v != fa[i] && v != son[i]) {
			dfs2(v, v);
		}
	}
	return;
}
int tot[2 * MAXN], maxn[2 * MAXN];
void build(int p, int l, int r) {
	if (l == r) {
		maxn[p] = id[l];
		tot[p] = id[l];
		return;
	}
	int mid = (l + r) / 2, lc = p * 2, rc = p * 2 + 1;
	build(lc, l, mid);
	build(rc, mid + 1, r);
	tot[p] = tot[lc] + tot[rc];
	maxn[p] = max(maxn[lc], maxn[rc]);
	return;
}
void change(int p, int l, int r, int i, int a) { // 线段树单点修改
	if (l == r) {
        tot[p] = a;
		maxn[p] = a;
        return;
    }
    int mid = (l + r) / 2, lc = p * 2, rc = p * 2 + 1;
    if (i <= mid) change(lc, l, mid, i, a);
    else change(rc, mid + 1, r, i, a);
    tot[p] = tot[lc] + tot[rc];
	maxn[p] = max(maxn[lc], maxn[rc]);
    return;
}
int query_max(int p, int sl, int sr, int l, int r) { // 线段树查询区间最值
    if (l <= sl && sr <= r) {
        return maxn[p];
    }
    int mid = (sl + sr) / 2, lc = p * 2, rc = p * 2 + 1, ans = -INF;
    if (l <= mid) ans = max(ans, query_max(lc, sl, mid, l, r));
    if (r > mid) ans = max(ans, query_max(rc, mid + 1, sr, l, r));
    return ans;
}
int query_sum(int p, int sl, int sr, int l, int r) { // 线段树查询区间和
    if (l <= sl && sr <= r) {
        return tot[p];
    }
    int mid = (sl + sr) / 2, lc = p * 2, rc = p * 2 + 1, sum = 0;
    if (l <= mid) sum += query_sum(lc, sl, mid, l, r);
    if (r > mid) sum += query_sum(rc, mid + 1, sr, l, r);
    return sum;
}
int ask_max(int u, int v) {
	int ans = -INF;
	while (top[u] != top[v]) {  // 判断是否在同一条重链上
		if (dep[top[u]] < dep[top[v]]) {
			swap(u, v);  // 优先跳深度较低的点
		}
		ans = max(ans, query_max(1, 1, n, s[top[u]], s[u])); // 修改跳过的链上的值
		u = fa[top[u]];
	}
	if (dep[u] > dep[v]) swap(u, v);
	ans = max(ans, query_max(1, 1, n, s[u], s[v])); // 在同一条重链上时,修改两点之间的节点
	return ans;
}
int ask_sum(int u, int v) {
	int sum = 0;
	while (top[u] != top[v]) {  // 判断是否在同一条重链上
		if (dep[top[u]] < dep[top[v]]) {
			swap(u, v);  // 优先跳深度较低的点
		}
		sum += query_sum(1, 1, n, s[top[u]], s[u]); // 查询跳过的链上的值
		u = fa[top[u]];
	}
	if (dep[u] > dep[v]) swap(u, v);
	sum += query_sum(1, 1, n, s[u], s[v]); // 在同一条重链上时,修改两点之间的节点
	return sum;
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i < n; i++) {
		int x, y;
		scanf("%d %d", &x, &y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	for (int i = 1; i <= n; i++) {
		scanf("%d", &w[i]);
	}
	dfs1(1);
	dfs2(1, 1); // 预处理
	build(1, 1, n); // 根据树剖 DFS 序建树
	scanf("%d", &q);
	for (int i = 1; i <= q; i++) {
		char c[10];
		int a, b;
		scanf("\n%s %d %d", c, &a, &b);
		if (c[0] == 'C') { // 单点修改
			change(1, 1, n, s[a], b);
		} else if (c[1] == 'M') { // 询问最大值
			printf("%d\n", ask_max(a, b));
		} else if (c[1] == 'S') { // 询问和
			printf("%d\n", ask_sum(a, b));
		}
	}
	return 0;
}
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值