树链剖分(重链剖分)总结
树链剖分(重链剖分)总结
基本内容
基本思想
\qquad 树链剖分,顾名思义,是应用在树上的一种数据结构。一般用于处理动态维护路径信息、子树信息的问题,例如路径权值修改,路径查询权值和(最值),子树查询权值和(最值)等。树链剖分是将树剖析成一条条链,再利用 dfn \text{dfn} dfn 序上套线段树实现动态维护区间信息。
实现过程
step1: 重儿子、重链
\qquad 回顾标题:树链剖分(重链剖分),为什么要在小括号里加个重链剖分 呢?
树链剖分(树剖/链剖)有多种形式,如重链剖分,长链剖分和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」),大多数情况下(没有特别说明时),「树链剖分」都指「重链剖分」。—— from oi-wiki
\qquad 话说至此,什么又是重链剖分呢?在了解这之前,我们先来了解下什么是重儿子。
\qquad
“重”儿子,为什么说它“重”呢?在树上,什么信息可以恰当,形象的说“轻重”呢?当然是
size
\text{size}
size (子树大小)啦!我们形式化的定义一下重儿子:假设
son
x
\text{son}_x
sonx 表示点
x
\text{x}
x 的重儿子,
to
x
\text{to}_x
tox 表示
x
\text{x}
x 的儿子们,
sze
x
\text{sze}_x
szex 表示以
x
\text{x}
x 为根的子树的大小,那么
sze
son
x
=
max
u
∈
to
x
sze
u
\large \text{sze}_{\text{son}_x}=\max_{u\in \text{to}_x}\text{sze}_u
szesonx=maxu∈toxszeu。用语言描述就是:重儿子的子树大小是所有子节点中最大的。我们把剩余的子节点定义为轻儿子,点
x
\text{x}
x 向
son
x
\text{son}_x
sonx 连的边定义为重边,剩余的边定义为轻边;同时,我们把由若干条(可以是
0
0
0)重边首尾拼接而成的链叫做重链,那么整棵树就可以被剖分成若干条重链,而且每一个点一定存在于一条重链中。这意味着我们可以用重链将树完全剖分。
step2:dfn 序
\qquad 在介绍树剖时,曾提到过树剖是利用 dfn \text{dfn} dfn 序加线段树来完成子树修改、查询和路径修改、查询。对于子树的操作,我们都知道子树内的点 dfn \text{dfn} dfn 序永远是连续的,也就是说对于子树的操作是可以轻松完成的。那么对于路径操作,怎么搞呢?
\qquad
既然上面提到重链可以将树完全剖分,那么我们不妨在
dfs
\text{dfs}
dfs 的时候优先递归重儿子,然后再递归轻儿子,这样我们不仅可以保证子树内
dfn
\text{dfn}
dfn 序连续,还可以保证一条重链上的点的
dfn
\text{dfn}
dfn 序连续。有了这个重要性质,我们便可以轻松做到对重链的修改。想到了这一点,那么路径修改也就很简单了:假设一条路径是从
x
\text{x}
x 到
y
\text{y}
y,我们将路径拆成从
lca
\text{lca}
lca 到
x
\text{x}
x,从
lca
\text{lca}
lca 到
y
\text{y}
y 两条路径,然后我们分别从
x
\text{x}
x,
y
\text{y}
y 开始往
lca
\text{lca}
lca 跳重链,对于一条重链,我们先跳到这条链的一个端点,在
Θ
(
log
n
)
\Theta(\log n)
Θ(logn) 的时间内修改,然后直接跳到这条链的另一个端点,接着往上跳别的重链,重复上述操作即可。这就是重链剖分操作的整体过程。
step3:时间复杂度分析
\qquad 上述操作看起来非常暴力,我们接下来来分析一下这一过程的时间复杂度。首先,这一过程的时间复杂度跟我们跳过的重链的条数有着密切关系,跟长度并无太大关系。我们不难发现,两条重链是会被一条轻边断开的。因此,我们只需分析出这一过程中会跳过多少轻边即可。这就要回到我们为什么要定义重儿子了。
\qquad 重儿子的子树大小是所有儿子中最大的,但是我们并不能得知重儿子子树大小的范围。对于一条链,重儿子子树大小可能达到 Θ ( n ) \Theta(n) Θ(n) 级别,但是对于菊花图,这一大小可能只有 1 1 1。但是,我们是可以得到轻儿子的子树大小范围的。轻儿子的子树大小一定不超过 sze x 2 \Large \frac{\text{sze}_x}{2} 2szex。这一点我们通过反证法很好证明。接着分析下去,我们发现每经过一条轻边,子树大小就会至少除以 2 2 2,那是不是就意味着我们最多只会经过 Θ ( log n ) \Theta(\log n) Θ(logn) 条轻边呢?那么我们跳链的次数也是 Θ ( log n ) \Theta(\log n) Θ(logn) 级别的,配上线段树,总体时间复杂度 Θ ( n log 2 n ) \Theta(n\log^2 n) Θ(nlog2n),就能轻松解决掉子树操作、路径操作的问题了。
代码实现
求重儿子
void dfs_pre(int x, int fa) {
sze[x] = 1, dep[x] = dep[fa] + 1, fat[x] = fa;//需要记录每个点的父亲,跳链时会用
for(int i = head[x]; i; i = edge[i].lst) {
int To = edge[i].to;
if(To == fa) continue;
dfs_pre(To, x);
sze[x] += sze[To];
if(sze[To] > sze[son[x]]) son[x] = To;//更新重儿子
}
}
重链剖分
\qquad 这一段应该是树剖中最重要的一个 dfs \text{dfs} dfs 了。
void dfs(int x, int fa, int chain) {
bel[x] = chain;//记录当前点所在的链的链头
dfn[x] = ++ num;//记录dfn序
v[num] = x;//记录dfn序为num的点是哪一个
if(son[x]) dfs(son[x], x, chain);//重儿子跟自己在同一条链中
for(int i = head[x]; i; i = edge[i].lst) {
int To = edge[i].to;
if(To == fa || To == son[x]) continue;
dfs(To, x, To);//轻儿子跟自己不在一条链中,每个轻儿子都新开一条链
}
}
各种操作
求 lca:
int lca(int x, int y) {
while(bel[x] != bel[y]) {//只要链头不同,就让 链头 深度更大的点往上跳
if(dep[bel[x]] > dep[bel[y]]) x = fat[bel[x]];
else y = fat[bel[y]];
}
return dep[x] > dep[y] ? y : x;
}
路径修改:
void C(int x, int y, int z) {//C:change
for(; bel[x] != bel[y]; x = fat[bel[x]]) change(1, dfn[bel[x]], dfn[x], z);
change(1, dfn[y] + 1, dfn[x], z);
}
//main 中
int Lca = lca(u, v);
C(u, Lca, w), C(v, Lca, w);//将路径拆成两条,分别修改
路径查询:
int Q(int x, int y) {
int maxx = 0;
for(; bel[x] != bel[y]; x = fat[bel[x]]) maxx = max(maxx, query(1, dfn[bel[x]], dfn[x]));
maxx = max(maxx, query(1, dfn[y] + 1, dfn[x]));
return maxx;
}
// main 中
int Lca = lca(u, v);
printf("%d\n", max(Q(u, Lca), Q(v, Lca)));
例题推荐
\qquad [ZJOI2008] 树的统计 板子题
\qquad [HAOI2015] 树上操作 板子题
\qquad [NOI2015] 软件包管理器 板子题
\qquad 月下“毛景树” 板子题,但是边权转点权,有点小细节
\qquad QTREE - Query on a tree 也是边权转点权
\qquad [国家集训队] 旅游 树剖板子,主要考查线段树操作
\qquad [SDOI2011] 染色 处理颜色段时有一点点小细节
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)