树链剖分详解
前言
当遇到有关树上的操作时,总有一种想法去把树形结构破坏成线性结构。DFS序是一个不错的选择,但是当求树上的某些路径时,DFS序又无能为力了。本文将介绍一个将树形结构破坏成线性结构的算法。
概述
树链剖分将树上的路径划分为了 重链 和 轻链 。当然,划分又有一定的规则:重链上相连的各个节点在形成的线性结构中相邻,同一颗子树的所有节点在线性结构中都排列在这棵子树的根节点之后。这样使得查询和修改操作都可以在线段树或树状数组等数据结构中进行。
下面对一些名词进行解释
重儿子:如果说 u 的所有子节点中 以 v 为根的子树拥有最多的节点,那么 v 就是 u 的重儿子
轻儿子:u 中除 v 以外的所有子节点
重边:点 u 与其重儿子的连边。
轻边:点 u 与其轻儿子的连边。
重链:由重边连成的路径。
轻链:轻边。
实现树链剖分需要预处理出一下各个数组:父节点(fa[]), 深度(deep[]), 以其为根的子树中节点个数(size[]), 重儿子的编号(son[]), 在线性结构中的编号(id[]), 线性结构中的编号所对应的实际编号(id_map[]) 所在链的头结点编号(top[]),
我们可以通过两遍深搜来初始化这些数组,第一遍深搜可以初始化fa[], deep[], size[], son[]这几个数组。而第二遍深搜,我们就可以将所有的重链与轻链求出来。
void dfs1(int x) { //第一遍深搜代码
size[x] = 1;//初始化当前子树所拥有的节点个数
son[x] = 0;//初始化为无重儿子
int to;
for (int i = head[x]; i; i = edges[i].next) {
to = edges[i].to;//枚举所有能够到达的节点
if (to != fa[x]) {//排除父节点
fa[to] = x;//to 节点的父节点为当前节点
deep[to] = deep[x] + 1;//to 节点的深度为当前节点深度 +1
dfs1(to);//深搜 to 节点
size[x] += size[to]; //累加当前子树的节点个数
if (son[x] == 0 || size[son[x]] < size[to])//判断to 节点是否可以作为当前节点的重儿子
son[x] = to;
}
}
}
void dfs2(int x, int tp) { // 第二遍深搜代码
top[x] = tp;//设置当前节点所在链的头结点编号为 tp
id[x] = ++id_num;//当前节点在线性结构中的编号
id_map[id_num] = x;//对应线性结构编号与实际编号
int to;
//由于需要将重链上相邻的节点放在一起,所以应该优先深搜重儿子
if (son[x]) // 如果当前节点含有重儿子
dfs2(son[x], tp);//优先深搜重儿子
for (int i = head[x]; i; i = edges[i].next) {
to = edges[i].to; //遍历与当前节点相邻的所有节点
if (to != fa[x] && to != son[x])//排除父节点与重儿子
dfs2(to, to);//深搜轻儿子
}
}
求出线性排列之后,便可以通过线段树等数据结构对两点之间的路径进行查询或修改操作了。
当需要对 a b 两点之间的路径上的所有点进行操作时,算法如下:
设 f1 为 top[a], f2 为 top[b]
若 f1 != f2 时:不妨设 f2 是 f1 与 f2 两点中深度最大的一个。那么在线性结构中 id[f2] 到 id[b] 之间的所有节点一定在这个路径之上。并使 b = fa[f2];
重复上述过程直到 f1 == f2
此时 f1 与 f2 相等,这说明 a 与 b 位于同一条重链上。假设 b 的深度较大。则在线性结构中 id[a] 到 id[b] 之间的所有节点又一定位于这条路径上。
至此,所有位于路径上的点都已经被找到,可以在循环中对这些节点进行更改或查询操作。
PS:当需要对树上的边进行操作时,我们也可以存点的编号,只是他们代表了由这个点出发到其父节点的边
特别提醒:若对边进行操作,则查询代码与点权查询有一处不同!!!!已在下方代码中标注!!!!!
附: 查询 a b 两点之间路径上所有点的点权和:
long long tree_query(int a, int b) {
long long sum = 0;
while (top[a] != top[b]) {//f1 与 f2 不相等时进行循环
if (deep[top[a]] > deep[top[b]]) // 始终满足 f2 为深度较大的点
swap(a, b);
sum += query(id[top[b]], id[b], root); // id[f2] 到 id[b] 之间的所有点一定在路径上
b = fa[top[b]]; // 使 b = fa[f2]
}
// 此时 a 与 b 一定位于同一条重链上
if (id[a] > id[b]) // 使 b 为 a, b 中深度较大的节点
swap(a, b);
sum += query(id[a], id[b], root); // 此时 id[a] 与 id[b] 之间的节点一定位于路径上
//若查询边权,此处应写成
//sum += query(id[a] + 1, id[b], root);
//因为 id[a] 代表的是 a 节点连向其父节点的边,由于 a 的深度较低,所以这条边不在路径上
return sum;
}
例题讲解
来源
解题报告
本题是树链剖分的模板
首先我们需要构造出一个支持区间修改、区间求和的线段树。
通过两遍 dfs 将树上的点映射到线段树中。
操作1:将节点 a 的权值增加 b。需要修改线段树中编号为 id[a] 的节点的权值即可
操作2:将以节点 a 为根的子树上所有节点的权值都增加 b。修改线段树中编号为 id[a] 到 id[a] + size[a] - 1 之间的所有节点的权值。这是因为任何一棵子树中所有的节点在线性结构中都排列在这棵子树的根节点之后。
操作3:查询节点 a 到根节点 1 的路径上所有的点的点权和,我们只需要进行树链剖分的查询操作即可
源代码
#include <iostream>
#include <cstdio>
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
#define root 1, n, 1
using namespace std;
int n, m;
int value[1000005];
int size[1000005], deep[1000005], son[1000005], id_map[1000005];
int top[1000005], fa[1000005], id[1000005], id_num;
long long sum[1000005 << 2], col[1000005 << 2];
int head[1000005], edge_len;
struct Edge {
int to;
int next;
} edges[2000005];
void add(int from, int to) {//加边函数(使用前向星方式存图)
edges[++edge_len].to = to;
edges[edge_len].next = head[from];
head[from] = edge_len;
}
void dfs1(int x) {//第一遍dfs,参见概述
size[x] = 1;
son[x] = 0;
int to;
for (int i = head[x]; i; i = edges[i].next) {
to = edges[i].to;
if (to != fa[x]) {
fa[to] = x;
deep[to] = deep[x] + 1;
dfs1(to);
size[x] += size[to];
if (son[x] == 0 || size[son[x]] < size[to])
son[x] = to;
}
}
}
void dfs2(int x, int tp) {//第二遍dfs,参见概述
top[x] = tp;
id[x] = ++id_num;
id_map[id_num] = x;
int to;
if (son[x])
dfs2(son[x], tp);
for (int i = head[x]; i; i = edges[i].next) {
to = edges[i].to;
if (to != fa[x] && to != son[x])
dfs2(to, to);
}
}
void PushUP(int x) {
sum[x] = sum[x << 1] + sum[x << 1 | 1];
}
void PushDown(int x, int l) {//下放标记
if (col[x]) {
col[x << 1] += col[x];
col[x << 1 | 1] += col[x];
sum[x << 1] += (l - (l >> 1)) * col[x];
sum[x << 1 | 1] += (l >> 1) * col[x];
col[x] = 0;
}
}
void build(int l, int r, int rt) {//构造线段树
if (l == r) {
sum[rt] = value[id_map[l]];
return;
}
int m = (r + l) >> 1;
build(lson);
build(rson);
PushUP(rt);
}
void update(int L, int R, long long num, int l, int r, int rt) {//线段树更新操作
if (L <= l && r <= R) {
sum[rt] += (r - l + 1) * num;
col[rt] += num;
return;
}
PushDown(rt, r - l + 1);
int m = (l + r) >> 1;
if (L <= m)
update(L, R, num, lson);
if (m < R)
update(L, R, num, rson);
PushUP(rt);
}
long long query(int L, int R, int l, int r, int rt) {//线段树查询操作
if (L <= l && r <= R)
return sum[rt];
PushDown(rt, r - l + 1);
int m = (l + r) >> 1;
long long ret = 0;
if (L <= m)
ret = query(L, R, lson);
if (m < R)
ret += query(L, R, rson);
return ret;
}
long long tree_query(int a, int b) {//树链剖分查询操作,参见概述
long long sum = 0;
while (top[a] != top[b]) {
if (deep[top[a]] > deep[top[b]])
swap(a, b);
sum += query(id[top[b]], id[b], root);
b = fa[top[b]];
}
if (id[a] > id[b])
swap(a, b);
sum += query(id[a], id[b], root);
return sum;
}
int main() {
freopen("in.txt", "r", stdin);
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &value[i]);
int from, to;
for (int i = 1; i < n; i++) {
scanf("%d%d", &from, &to);
add(from, to);
add(to, from);
}
deep[1] = 1;//设置根节点的深度为1(可以省略)
dfs1(1);
dfs2(1, 1);
build(root);//构建线段树
int k, a, b;
for (int i = 1; i <= m; i++) {
scanf("%d", &k);
if (k == 1) {
scanf("%d%d", &a, &b);
update(id[a], id[a], b, root);//单点修改
} else if (k == 2) {
scanf("%d%d", &a, &b);
update(id[a], id[a] + size[a] - 1, b, root);//区间修改
} else {
scanf("%d", &a);
printf("%lld\n", tree_query(1, a));//树链剖分查询
}
}
return 0;
}