Unverified Commit 98fecd6d authored by ir1d's avatar ir1d Committed by GitHub
Browse files

Feat: 补充树链剖分 (#1272)

Feat: 补充树链剖分
parents a15988b6 ffa1fd37
Loading
Loading
Loading
Loading
+99 −12
Original line number Diff line number Diff line
## 树链剖分的思想及能解决的问题

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。

具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。

**树链剖分**(树剖/链剖)有多种形式,如 **重链剖分****长链剖分** 和用于 Link/cut Tree 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分” 都指 “重链剖分”,本文所讲的也是 “重链剖分”。

重链剖分可以将树上的任意一条路径划分成不超过 $\mathcal O(\log n)$ 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 $lca$ 为链的一个端点)。
@@ -13,27 +17,64 @@

除了配合数据结构来维护树上路径信息,树剖还可以用来 $\mathcal O(\log n)$(且常数较小)地求 $lca$。在某些题目中,还可以利用其性质来灵活地运用树剖。

## 一些定义
## 重链剖分

我们给出一些定义:

 $fa(x)$ 表示节点 $x$ 在树上的父亲
定义**重子节点**表示其子节点中子树最大的子结点。如果有相同的,任意取。如果没有子节点,就没有

 $dep(x)$ 表示节点 $x$ 在树上的深度
定义**轻子节点**表示剩余的子结点

 $siz(x)$ 表示节点 $x$ 的子树的节点个数
从这个结点到重子节点的边叫**重边**

 $son(x)$ 表示节点 $x$ 的 **重儿子** ,即所有儿子中子树大小最大的一个。若有多个儿子的子树大小相同,可任选一个
到其他轻子节点的边叫**轻边**

定义 **重边** 表示连接一个点及其重儿子的边
若干条首尾衔接的重边构成**重链**

相对地,**轻儿子** 表示一个节点除了重儿子外的儿子。**轻边** 表示连接一个点及其轻儿子的边
把落单的结点也当作重链,那么整棵树就被剖分成若干条重链

定义 **重链** 表示重边连成的一条链。为轻儿子的叶子节点可以视作一条只有一个节点的重链,这样,**树上每个节点都属于且仅属于一条重链**
看一张图就明白了

 $top(x)$ 表示节点 $x$ 所在 **重链** 的顶部节点(深度最小)。
![HLD](./images/hld.png)

 $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)
```

给一个具体的代码实现吧。

我们先给出一些定义:

-  $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 求出 $fa(x),dep(x),siz(x),son(x)$ ,第二次 DFS 求出 $top(x),tid(x),rnk(x)$ 。

@@ -66,11 +107,57 @@ void dfs2(int o, int t) {

## 重链剖分的性质

**树上每个节点都属于且仅属于一条重链**

重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的)

所有的重链将整棵树**完全剖分**

重链一定是链状结构;重边不会连成一棵树。

在剖分时**重边优先遍历**,最后树的 DFN 序上,重链内的 DFN 序是连续的。按 DFN 排序后的序列即为剖分后的链

一颗子树内的 DFN 序是连续的

可以发现,当我们向下经过一条 **轻边** 时,所在子树的大小至少会除以二。

因此,对于树上的任意一条路径,把它拆分成从 $lca$ 分别向两边往下走,分别最多走 $\mathcal O(\log n)$ 次,因此,树上的每条路径都可以被拆分成不超过 $\mathcal O(\log n)$ 条重链。

## 例题:[「ZJOI2008」树的统计](https://www.luogu.org/problemnew/show/P2590)
## 常见应用

### 路径上维护

用树链剖分求树上两点路径权值和,伪代码如下:
```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.lydsy.com/JudgeOnline/problem.php?id=1036)

### 题目大意

+77.3 KiB
Loading image diff...