Loading docs/graph/heavy-light-decomposition.md +98 −11 Original line number Diff line number Diff line ## 树链剖分的思想及能解决的问题 树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。 具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。 **树链剖分**(树剖/链剖)有多种形式,如 **重链剖分**,**长链剖分** 和用于 Link/cut Tree 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分” 都指 “重链剖分”,本文所讲的也是 “重链剖分”。 重链剖分可以将树上的任意一条路径划分成不超过 $\mathcal O(\log n)$ 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 $lca$ 为链的一个端点)。 Loading @@ -13,27 +17,64 @@ 除了配合数据结构来维护树上路径信息,树剖还可以用来 $\mathcal O(\log n)$(且常数较小)地求 $lca$。在某些题目中,还可以利用其性质来灵活地运用树剖。 ## 一些定义 ## 重链剖分 我们给出一些定义: $fa(x)$ 表示节点 $x$ 在树上的父亲。 定义**重子节点**表示其子节点中子树最大的子结点。如果有相同的,任意取。如果没有子节点,就没有。 $dep(x)$ 表示节点 $x$ 在树上的深度。 定义**轻子节点**表示剩余的子结点。 $siz(x)$ 表示节点 $x$ 的子树的节点个数。 从这个结点到重子节点的边叫**重边**。 $son(x)$ 表示节点 $x$ 的 **重儿子** ,即所有儿子中子树大小最大的一个。若有多个儿子的子树大小相同,可任选一个。 到其他轻子节点的边叫**轻边**。 定义 **重边** 表示连接一个点及其重儿子的边。 若干条首尾衔接的重边构成**重链**。 相对地,**轻儿子** 表示一个节点除了重儿子外的儿子。**轻边** 表示连接一个点及其轻儿子的边。 把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。 定义 **重链** 表示重边连成的一条链。为轻儿子的叶子节点可以视作一条只有一个节点的重链,这样,**树上每个节点都属于且仅属于一条重链**。 看一张图就明白了 $top(x)$ 表示节点 $x$ 所在 **重链** 的顶部节点(深度最小)。  $tid(x)$ 表示节点 $x$ 的 **时间戳** ,也是其在线段树中的编号。 ## 实现 $rnk(x)$ 表示时间戳所对应的节点编号,有 $rnk(tid(x))=x$ 。 树剖的实现分两个DFS的过程。伪代码如下: 第一个 DFS 记录每个结点的深度(deep)、子树大小(size)。 ``` TREE-BUILD-DFS(u,dep) u.deep=dep // 记录深度 u.size=1 for v is u's son u.size+=TREE-BUILD-DFS(v) return u.size // 返回该节点对应的子树的大小 ``` 第二个 DFS 记录每个结点的重子结点(heavy-son)、重边优先遍历时的 DFN 序、所在链的链顶(top,且应初始化为结点本身)。 ``` TREE-DECOMPOSITION-DFS(u,top) u.top=top // 记录所在重链的链顶 u.dfn=++tot // 记录结点的DFN序 for v is u's son //找重儿子 if(v.size>hvs)hvs=v.size,p=v TREE-DECOMPOSITION-DFS(v,top) // 重边优先遍历 for v is u's son // 遍历轻边时,以自己为链顶 if(v!=p)TREE-DECOMPOSITION-DFS(v,v) ``` 给一个具体的代码实现吧。 我们先给出一些定义: 1. $fa(x)$ 表示节点 $x$ 在树上的父亲。 2. $dep(x)$ 表示节点 $x$ 在树上的深度。 3. $siz(x)$ 表示节点 $x$ 的子树的节点个数。 4. $son(x)$ 表示节点 $x$ 的 **重儿子** 。 5. $top(x)$ 表示节点 $x$ 所在 **重链** 的顶部节点(深度最小)。 6. $tid(x)$ 表示节点 $x$ 的 **时间戳** ,也是其在线段树中的编号。 7. $rnk(x)$ 表示时间戳所对应的节点编号,有 $rnk(tid(x))=x$ 。 我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 $fa(x),dep(x),siz(x),son(x)$ ,第二次 DFS 求出 $top(x),tid(x),rnk(x)$ 。 Loading Loading @@ -66,10 +107,56 @@ void dfs2(int o, int t) { ## 重链剖分的性质 **树上每个节点都属于且仅属于一条重链**。 重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的) 所有的重链将整棵树**完全剖分** 重链一定是链状结构;重边不会连成一棵树。 在剖分时**重边优先遍历**,最后树的 DFN 序上,重链内的 DFN 序是连续的。按 DFN 排序后的序列即为剖分后的链 一颗子树内的 DFN 序是连续的 可以发现,当我们向下经过一条 **轻边** 时,所在子树的大小至少会除以二。 因此,对于树上的任意一条路径,把它拆分成从 $lca$ 分别向两边往下走,分别最多走 $\mathcal O(\log n)$ 次,因此,树上的每条路径都可以被拆分成不超过 $\mathcal O(\log n)$ 条重链。 ## 常见应用 ### 路径上维护 用树链剖分求树上两点路径权值和,伪代码如下: ```cpp TREE-PATH-SUM(u,v) while u,v 不在同一条链上 if u 所在链的链顶的深度小于 v 所在链的链顶的深度 swap(u,v) 将 u 到 u 所在链的链顶 之间的结点权值求和,累加到计数器中 u = u 所在链链顶的父节点 将 u,v 之间的结点的权值求和累加,返回计数器的值 ``` 链上的 DFN 序是连续的,可以使用线段树,树状数组维护。 每次选择深度较大的链往上跳,直到两点在同一条链上。 同样的跳链结构适用于维护、统计路径上的其他信息。 ### 子树维护 有时会要求,维护子树上的信息,譬如将以 x 为根的子树的所有结点的权值增加 v。 在 DFS 搜索的时候,子树中的结点的 DFN 序是连续的。 每一个结点记录 bottom 表示所在子树连续区间末端的结点。 这样就把子树信息转化为连续的一段区间信息。 ### 求最近公共祖先 不断向上跳链,当跳到同一条链上时,返回深度较小的结点即为 LCA。 ## 例题:[「ZJOI2008」树的统计](https://www.luogu.org/problemnew/show/P2590) ### 题目大意 Loading docs/graph/images/hld.png 0 → 100644 +77.3 KiB Loading image diff... Loading
docs/graph/heavy-light-decomposition.md +98 −11 Original line number Diff line number Diff line ## 树链剖分的思想及能解决的问题 树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。 具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。 **树链剖分**(树剖/链剖)有多种形式,如 **重链剖分**,**长链剖分** 和用于 Link/cut Tree 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分” 都指 “重链剖分”,本文所讲的也是 “重链剖分”。 重链剖分可以将树上的任意一条路径划分成不超过 $\mathcal O(\log n)$ 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 $lca$ 为链的一个端点)。 Loading @@ -13,27 +17,64 @@ 除了配合数据结构来维护树上路径信息,树剖还可以用来 $\mathcal O(\log n)$(且常数较小)地求 $lca$。在某些题目中,还可以利用其性质来灵活地运用树剖。 ## 一些定义 ## 重链剖分 我们给出一些定义: $fa(x)$ 表示节点 $x$ 在树上的父亲。 定义**重子节点**表示其子节点中子树最大的子结点。如果有相同的,任意取。如果没有子节点,就没有。 $dep(x)$ 表示节点 $x$ 在树上的深度。 定义**轻子节点**表示剩余的子结点。 $siz(x)$ 表示节点 $x$ 的子树的节点个数。 从这个结点到重子节点的边叫**重边**。 $son(x)$ 表示节点 $x$ 的 **重儿子** ,即所有儿子中子树大小最大的一个。若有多个儿子的子树大小相同,可任选一个。 到其他轻子节点的边叫**轻边**。 定义 **重边** 表示连接一个点及其重儿子的边。 若干条首尾衔接的重边构成**重链**。 相对地,**轻儿子** 表示一个节点除了重儿子外的儿子。**轻边** 表示连接一个点及其轻儿子的边。 把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。 定义 **重链** 表示重边连成的一条链。为轻儿子的叶子节点可以视作一条只有一个节点的重链,这样,**树上每个节点都属于且仅属于一条重链**。 看一张图就明白了 $top(x)$ 表示节点 $x$ 所在 **重链** 的顶部节点(深度最小)。  $tid(x)$ 表示节点 $x$ 的 **时间戳** ,也是其在线段树中的编号。 ## 实现 $rnk(x)$ 表示时间戳所对应的节点编号,有 $rnk(tid(x))=x$ 。 树剖的实现分两个DFS的过程。伪代码如下: 第一个 DFS 记录每个结点的深度(deep)、子树大小(size)。 ``` TREE-BUILD-DFS(u,dep) u.deep=dep // 记录深度 u.size=1 for v is u's son u.size+=TREE-BUILD-DFS(v) return u.size // 返回该节点对应的子树的大小 ``` 第二个 DFS 记录每个结点的重子结点(heavy-son)、重边优先遍历时的 DFN 序、所在链的链顶(top,且应初始化为结点本身)。 ``` TREE-DECOMPOSITION-DFS(u,top) u.top=top // 记录所在重链的链顶 u.dfn=++tot // 记录结点的DFN序 for v is u's son //找重儿子 if(v.size>hvs)hvs=v.size,p=v TREE-DECOMPOSITION-DFS(v,top) // 重边优先遍历 for v is u's son // 遍历轻边时,以自己为链顶 if(v!=p)TREE-DECOMPOSITION-DFS(v,v) ``` 给一个具体的代码实现吧。 我们先给出一些定义: 1. $fa(x)$ 表示节点 $x$ 在树上的父亲。 2. $dep(x)$ 表示节点 $x$ 在树上的深度。 3. $siz(x)$ 表示节点 $x$ 的子树的节点个数。 4. $son(x)$ 表示节点 $x$ 的 **重儿子** 。 5. $top(x)$ 表示节点 $x$ 所在 **重链** 的顶部节点(深度最小)。 6. $tid(x)$ 表示节点 $x$ 的 **时间戳** ,也是其在线段树中的编号。 7. $rnk(x)$ 表示时间戳所对应的节点编号,有 $rnk(tid(x))=x$ 。 我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 $fa(x),dep(x),siz(x),son(x)$ ,第二次 DFS 求出 $top(x),tid(x),rnk(x)$ 。 Loading Loading @@ -66,10 +107,56 @@ void dfs2(int o, int t) { ## 重链剖分的性质 **树上每个节点都属于且仅属于一条重链**。 重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的) 所有的重链将整棵树**完全剖分** 重链一定是链状结构;重边不会连成一棵树。 在剖分时**重边优先遍历**,最后树的 DFN 序上,重链内的 DFN 序是连续的。按 DFN 排序后的序列即为剖分后的链 一颗子树内的 DFN 序是连续的 可以发现,当我们向下经过一条 **轻边** 时,所在子树的大小至少会除以二。 因此,对于树上的任意一条路径,把它拆分成从 $lca$ 分别向两边往下走,分别最多走 $\mathcal O(\log n)$ 次,因此,树上的每条路径都可以被拆分成不超过 $\mathcal O(\log n)$ 条重链。 ## 常见应用 ### 路径上维护 用树链剖分求树上两点路径权值和,伪代码如下: ```cpp TREE-PATH-SUM(u,v) while u,v 不在同一条链上 if u 所在链的链顶的深度小于 v 所在链的链顶的深度 swap(u,v) 将 u 到 u 所在链的链顶 之间的结点权值求和,累加到计数器中 u = u 所在链链顶的父节点 将 u,v 之间的结点的权值求和累加,返回计数器的值 ``` 链上的 DFN 序是连续的,可以使用线段树,树状数组维护。 每次选择深度较大的链往上跳,直到两点在同一条链上。 同样的跳链结构适用于维护、统计路径上的其他信息。 ### 子树维护 有时会要求,维护子树上的信息,譬如将以 x 为根的子树的所有结点的权值增加 v。 在 DFS 搜索的时候,子树中的结点的 DFN 序是连续的。 每一个结点记录 bottom 表示所在子树连续区间末端的结点。 这样就把子树信息转化为连续的一段区间信息。 ### 求最近公共祖先 不断向上跳链,当跳到同一条链上时,返回深度较小的结点即为 LCA。 ## 例题:[「ZJOI2008」树的统计](https://www.luogu.org/problemnew/show/P2590) ### 题目大意 Loading