树链剖分
重链剖分
【问题引入】
问题描述
给定一颗有 $n$ 个节点、带边权的树,现在有对树进行 $m$ 个操作,操作有 $2$ 类:
- 将节点 $a$ 到节点 $b$ 路径上所有边权的值都改为 $c$;
- 询问节点 $a$ 到节点 $b$ 路径上的最大边权值。
请你写一个程序依次完成这 $m$ 个操作。
有三个操作
- 修改 $2$ 号到 $9$ 号节点路径上的边值为 $7$;
- 修改 $2$ 号到 $7$ 号节点路径上的边值为 $4$ ;
- 查询 $9$ 号到 $8$ 号节点路径上的最大边权值。
每次修改和查询复杂度为 $O(n)$。
$\color{red}思考:既然每个操作都与树上的路径有关,能否把这些路径分段存储,方便修改和查询?$
【树链剖分的概念】
树链剖分是指一种对树进行划分的算法,它先通过轻重边剖分 $(Heavy-Light\ Decomposition)$ 将树分为多条链,保证每个点属于且只属于其中一条链,然后再通过数据结构(树状数组、SBT
、SPLAY
、线段树等)来维护每一条链。
使用这种方法后,一般可以将修改和查询的复杂度降为 $O(log_2\ n)$。
【树链剖分的方法】
定义
将树中的边分为:重边和轻边。
定义 $size(X)$ 为以 $X$ 为根的子树的节点个数。
令 $V$ 为 $U$ 的儿子节点中 $size$ 值最大的节点,那么 $V$ 称为 $U$ 的$\color{red}重儿子$,边 $(U,V)$ 被称为$\color{red}重边$,树中重边之外的边被称为$\color{red}轻边$,全部由重边构成的路径称为$\color{red}重链$。
性质1
对于轻边 $(U,V)$,$size(V)\le size(U)\div 2$。
从根到某一点的路径上,不超过 $log_2 N$ 条轻边,不超过 $log_2 N$ 条重路径。
特例
图中有 $5$ 条重链:
- $1\to 3\to 6\to 10$
- $2\to 5\to 9$
- $4$
- $7$
- $8$
性质2
每一条链首深度大于 $1$ 的重链都可以通过其$\color{red}链首的父亲节点$连到$\color{red}另一条重链$上。
核心定义
$size[i]$:以节点 $i$ 为根的子树中节点的个数;
$son[i]$:节点 $i$ 的重儿子;
$dep[i]$:节点 $i$ 的深度,根的深度为 $1$;
$top[i]$:节点 $i$ 所在重链的链首节点;
$fa[i]$:节点 $i$ 的父节点;
$tid[i]$:在 DFS
找重链的过程中为节点 $i$ 重新编的号码,每条重链上的编号节点是连续的。
两次DFS
- 第一次
DFS
:找重边,顺便求出所有的 $size[i],dep[i],fa[i],son[i]$; - 第二次
DFS
:将重边连成重链,顺便求出所有的 $top[i],tid[i]$。
第一次DFS
找重边,顺便求出所有的 $size[i],dep[i],fa[i],son[i]$;
第二次DFS
将重边连成重链,顺便求出所有的 $top[i],tid[i]$。
从根节点开始 ,沿重边向下扩展,连成重链;
不能加入当前重链上的节点,以该节点为链首向下拉一条新的重链(如果该节点是叶子节点,则自己构成一条重链);
DFS
过程中,对节点重新编号,因为是沿重边向下扩展,故一条重链上的节点新编号会是连续的。
参考代码
void DFS_2(int u, int sp) {//第二遍dfs求出top[],p[],fp[]的值
top[u] = sp;
p[u] = pos++;
fp[p[u]] = u;
if (son[u] != -1) DFS_2(son[u], sp);//先处理重边
for (int i = head[u]; i != -1; i = edge[i].next) {//再处理轻边情况
int v = edge[i].to;
if (v != son[u] && v != prt[u])
DFS_2(v, v);//轻链的顶点就是自己
}
}
【树链剖分的过程】
重链剖分后
剖分完成后,每条重链相当于一段区间,将所有的重链收尾相接,用适合的数据结构来维护这个整体。
- $tid[i]$:$\color{red}节点\ i$ 所对应的新编号
- $rank[i]$:$\color{red}编号\ i$ 所在原树中对应的节点编号
我们可以采用 $\Large\color{red}线段树$等数据结构维护每条重链。
以线段树维护为例
维护节点 $u,v$ 路径上的最大值
【树链剖分的修改操作】
即整体修改点 $U$ 和点 $V$ 的路径上每条边的权值。
点 $U$ 和点 $V$ 的关系分为两种情况:
- 情况 $1$ :$U$ 和 $V$ 在同一条重链上;
- 情况 $2$ :$U$ 和 $V$ 不在同一条重链上。
情况 $1$
以修改边为例:将原树中的边($6\to 10$)权值修改为 $6$($\color{red}边的权值存在边所到达顶点中$)。
$10$ 号节点的新编号为 $4$,故只需修改线段树中的 $[4,4]$ 的值即可。
$2$ 号和 $9$ 号节点的新编号分别为 $7$ 和 $9$,需要修改的区间为 $tid[son[2]]\sim tid[9]$,即 $[8,9]$。
情况 $2$
将原树中路径 ($8\to 9$)上所有边的权值都修改为 $8,\ top[9]=2,\ top[8]=8$。
它们不在同一条重链上,需要分段修改,边修改边$\color{red}往一条重链上靠$。
优先将$\color{red}链首$深度大的点往上爬,向另一条重链靠,直到两者爬到同一条重链,转换为情况 $1$ 解决。
后面的图不画了。
【树链剖分的查询操作】
和修改操作类似。
设查询 $u\to v\ \max$。
情况 $1$
$top[u] = top[v]$
在线段树上查询 $u\sim v$ 的区间即可。
情况 $2$
$top[u]\ne top[v]$
向上爬树,深度大的优先,深度一样随便爬一个。
重复这个操作,直到 $top[u] = top[v]$,转换成情况 $1$ 处理。
参考代码
int Change(int u, int v) {//查询 u -> v 边的最大值
int f1 = top[u], f2 = top[v];
int tmp = 0;
while (f1 != f2) {
//u 和 v 不在同一条重路径,深度深的点向上爬,直到在同一条重(轻)路径上为止
if (dep[f1] < dep[f2]) {
swap(f1, f2);
swap(u, v);
}
tmp = max(tmp, Query(1, p[f1], p[f2]));
//在重链中求 u 和链首端点 f1 路径上的最大值
u = prt[f1];
f1 = top[u];
}
if (u == v) return tmp;//为同一点,则退出
if (dep[u] > dep[v])
swap(u, v);
return max(tmp, Query(1, p[son[u]], p[v]));
}
//Query(v, l, r) 查询线段树中 [l,r] 的最大值
实链剖分
没有
长链剖分
没有