【学习笔记】重链剖分

本文详细介绍了树链剖分的概念、作用和定义,并通过三个例题展示了如何使用树链剖分解决实际问题,包括求最近公共祖先、树上操作、路径权值修改和查询等。树链剖分的主要应用包括维护树的结构、优化查询和修改操作,适用于树的多种动态维护问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【学习笔记】重链剖分


关于树链剖分

树链剖分,指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构来维护每一条链。

树链剖分其实分三种:重链剖分,长链剖分,实虚链剖分 ( ( L C T \rm LCT LCT ) )
所以,为了使整体显得更有条理(其实是我还没学完 ),我就分开写了。


重链剖分能做什么

1. 求最近公共祖先 L C A \rm LCA LCA
2. 修改节点 x x x 到节点 y y y 的最短路径上的所有节点 / / /边的权值。
3. 修改以节点 x x x 为根的子树中的所有节点 / / /边的权值。
4. 查询节点 x x x 到节点 y y y 的最短路径上的所有节点 / / /边的权值和或极值。
5. 查询以节点 x x x 为根的子树中的所有节点 / / /边的权值和或极值。


重链剖分的一些定义

定义 s i z e [ x ] \rm size[x] size[x] 为以 x x x 为根的子树的节点个数。

那么,对于每一个非叶节点,其所有儿子中 s i z e \rm size size 最大的儿子便为重儿子(如果有两个或以上都最大的话,则是其中随意一个),而 y y y 其它的儿子,都是轻儿子 。

一个节点连向其重儿子的边被称为重边,树中重边之外的边被称为轻边。多条重边相连为一条重链

放张图理解一下

在这里插入图片描述

图中灰心点为重儿子,白心点为轻儿子。加粗边为重边。
图中重链有 5 5 5 条: 1 — 3 — 7 — 13 — 16 — 18 , 12 — 15 , 10 — 14 , 2 — 5 , 4 — 8 1—3—7—13—16—18,12—15,10—14,2—5,4—8 137131618121510142548

性质
1. 若 x x x 为轻儿子,则有 s i z e [ x ] \rm size[x] size[x] < = <= <= s i z e [ f a t h e r [ x ] ] 2 \rm \dfrac{size[father[x]]}{2} 2size[father[x]]
2. 从根到某一点的路径上不超过 log ⁡ 2 n \rm \log_2 n log2n 条重链,不超过 log ⁡ 2 n \rm \log_2 n log2n 条轻链。(最重要的性质)


重链剖分的实现

预处理

定义 h s o n i \rm hson_i hsoni 表示节点 i i i 的重儿子编号, s i z i \rm siz_i sizi 表示以节点 i i i 为根的子树的节点个数, d e p i \rm dep_i depi 表示节点 i i i 的深度, f a i \rm fa_i fai 表示节点 i i i 的父亲节点, t o p x \rm top_x topx 表示节点 i i i 位于的重链上的顶端节点标号, d f n i \rm dfn_i dfni 表示 d f s \rm dfs dfs 进入节点 i i i 的时间戳, t t r i \rm ttr_i ttri i i i d f s \rm dfs dfs 序里在树上对应的节点编号。

第一个 d f s \rm dfs dfs 先求出每个节点的 f a i \rm fa_i fai h s o n i \rm hson_i hsoni s i z i \rm siz_i sizi 、和 d e p i \rm dep_i depi
第二个 d f s \rm dfs dfs优先走重边为原则,遍历出 d f s \rm dfs dfs,同时求出每个节点的 t o p x \rm top_x topx d f n i \rm dfn_i dfni t t r i \rm ttr_i ttri

比较重要的两点:
1. 优先走重边——每一条重链的新编号是连续的。
2. d f s \rm dfs dfs 序 ——同一棵子树所对应的一定是dfs序中连续的一段。

还是那张图
在这里插入图片描述
P S : \rm PS: PS: 红字表示点的 d f s \rm dfs dfs 序。 d f s \rm dfs dfs 序为: 1 , 3 , 7 , 13 , 16 , 18 , 17 , 12 , 15 , 6 , 10 , 14 , 2 , 5 , 4 , 8 , 9 1,3,7,13,16,18,17,12,15,6,10,14,2,5,4,8,9 1,3,7,13,16,18,17,12,15,6,10,14,2,5,4,8,9

维护以及在线操作

修改操作

1. 对于修改节点 x x x 到节点 y y y 的最短路径上的所有节点 / / /边的权值,可以像求 L C A \rm LCA LCA一样边跳便更改权值。
2. 至于修改以节点 x x x 为根的子树中的所有节点 / / /边的权值,只需要修改 d f n x \rm dfn_x dfnx d f n x + s i z x − 1 \rm dfn_x+siz_x-1 dfnx+sizx1

查询操作

与修改操作类似,只是把修改变为查询。

时间复杂度?

用线段树来维护树链剖分的时间复杂度是 O ( n + q log ⁡ 2 2 n ) {\rm O}(n +q\log_2^2n) O(n+qlog22n)
为什么是 O ( log ⁡ 2 2 n ) {\rm O}(\log_2^2n) O(log22n)
由于求 L C A \rm LCA LCA O ( log ⁡ 2 n ) {\rm O}(\log_2n) O(log2n) ,而在进行操作时使用数据结构又是 O ( log ⁡ 2 n ) {\rm O}(\log_2n) O(log2n) ,所以每次操作的时间复杂度就为 O ( n + q log ⁡ 2 2 n ) {\rm O}(n +q\log_2^2n) O(n+qlog22n) 了。

<think>好的,我现在要介绍一下重链剖分的模板。首先,我需要回忆一下重链剖分的基本概念和应用场景。重链剖分(Heavy-Light Decomposition,HLD)是一种将树结构分解成多条链的技术,常用于解决树上的路径查询问题,比如最近公共祖先(LCA)、路径上的最大值或区间和等。这种分解方法通过将树分割成重链和轻边,使得可以在这些链上使用线段树或树状数组等数据结构进行高效操作。 接下来,用户需要的是一个模板的介绍,可能包括实现步骤和代码结构。首先,我应该考虑重链剖分的几个关键步骤: 1. **第一次DFS**:计算每个节点的子树大小(size),并确定每个节点的重儿子(即子树最大的子节点)。 2. **第二次DFS**:根据重儿子进行优先遍历,为每个节点分配所在链的顶部(top),并处理DFS序(dfn),以便后续数据结构处理。 3. **数据结构构建**:通常使用线段树或树状数组维护每个链上的信息,比如区间查询或更新。 然后,我需要考虑如何将这些步骤转化为代码。首先,定义必要的数组,比如父节点数组(fa)、深度数组(dep)、子树大小数组(siz)、重儿子数组(hson)、链顶数组(top)、DFS序数组(dfn)等。然后,第一次DFS遍历树,计算siz和hson。第二次DFS分配top和dfn。最后,构建线段树。 在实现过程中,需要注意一些细节,例如DFS的顺序,如何判断重儿子,以及如何处理不同链之间的跳转。例如,在查询两个节点之间的路径时,需要不断将较深的节点沿着链向上跳,直到两个节点处于同一链上,然后处理剩余部分。 可能还需要考虑线段树或其他数据结构的实现,如何将dfn序映射到线段树的索引,以及如何处理区间操作。例如,路径上的最大值可能需要线段树维护区间最大值,而更新操作则对应线段树的单点修改。 另外,要注意边界情况,比如叶子节点的处理,或者当两个节点已经处于同一链时的处理。此外,时间复杂度方面,重链剖分可以将树上的路径查询分解为O(log n)条链的查询,从而保证每次操作的时间复杂度为O(log n)乘以数据结构操作的时间。 现在,我需要将这些步骤结构化,形成一个模板的框架,用代码示例来说明。可能需要用C++或Python作为示例语言,但用户可能需要中文解释。考虑到算法竞赛中常用C++,但用户可能也需要其他语言的参考,但通常模板以C++为主。 最后,需要验证这个模板的正确性,比如通过举例说明如何处理LCA或路径查询。例如,在查询两个节点u和v的LCA时,通过比较它们的top,不断将较深的节点上移,直到找到共同的链顶,最终找到LCA。 总结来说,重链剖分模板的实现步骤包括两次DFS预处理,数据结构构建,以及查询时的链跳跃操作。在代码中需要处理数组的初始化和更新,以及数据结构的具体实现。需要确保各个部分之间的正确衔接,比如dfn序的正确分配和线段树的正确映射。</think>重链剖分(Heavy-Light Decomposition, HLD)是一种将树结构分解为多条链的算法,常用于高效处理树上路径查询(如区间和、最大值、LCA 等)。以下是其核心实现步骤和模板代码: --- ### **一、核心思想** 1. **重儿子**:每个节点的子节点中,子树大小最大的子节点。 2. **重链**:由重儿子连接形成的链,其余边为轻边。 3. **分解目标**:将树分解为若干重链,使得任意节点到根的路径上经过的链数为 $O(\log n)$。 --- ### **二、实现步骤** #### **1. 预处理(两次DFS)** - **第一次DFS**:计算子树大小 `siz`,标记重儿子 `hson`。 $$ \text{siz}[u] = 1 + \sum_{v \in \text{children}(u)} \text{siz}[v] $$ - **第二次DFS**:分配DFS序 `dfn`,标记链顶 `top`,优先遍历重儿子。 #### **2. 数据结构** 用线段树/树状数组维护 `dfn` 序列上的区间操作。 --- ### **三、代码模板(C++)** ```cpp #include <vector> using namespace std; const int N = 1e5 + 10; vector<int> g[N]; // 邻接表 int fa[N], dep[N], siz[N], hson[N]; // 父节点、深度、子树大小、重儿子 int top[N], dfn[N], rnk[N], cnt=0; // 链顶、DFS序、映射、计数器 // 第一次DFS:计算siz和hson void dfs1(int u, int f) { fa[u] = f; dep[u] = dep[f] + 1; siz[u] = 1; hson[u] = -1; int max_size = 0; for (int v : g[u]) { if (v == f) continue; dfs1(v, u); siz[u] += siz[v]; if (siz[v] > max_size) { max_size = siz[v]; hson[u] = v; } } } // 第二次DFS:分配dfn和top void dfs2(int u, int t) { top[u] = t; dfn[u] = ++cnt; rnk[cnt] = u; // 线段树中的位置 if (hson[u] == -1) return; dfs2(hson[u], t); // 优先处理重儿子 for (int v : g[u]) { if (v == fa[u] || v == hson[u]) continue; dfs2(v, v); // 轻儿子作为新链的起点 } } // 初始化入口 void init(int root) { dfs1(root, 0); dfs2(root, root); // 此处初始化线段树(根据rnk数组) } // 查询u和v的LCA 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; } // 路径查询示例:u到v的最大值(需结合线段树) int query_path(int u, int v) { int res = -INF; while (top[u] != top[v]) { if (dep[top[u]] < dep[top[v]]) swap(u, v); res = max(res, query_segment(dfn[top[u]], dfn[u])); // 查询链 u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); res = max(res, query_segment(dfn[u], dfn[v])); // 最后一条链 return res; } ``` --- ### **四、关键操作** 1. **初始化**:调用 `init(root)`。 2. **LCA查询**:直接调用 `lca(u, v)`。 3. **路径查询**:结合线段树,分段查询每条链。 --- ### **五、复杂度分析** - **时间复杂度**:预处理 $O(n)$,每次路径操作 $O(\log n)$。 - **空间复杂度**:$O(n)$。 --- ### **六、应用场景** 1. 树链修改/查询(区间和、最值)。 2. 求最近公共祖先(LCA)。 3. 子树操作(利用DFS序的连续性)。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值