# **目录**
* [0. 前言](#0)
* [1. 附题单:](#1)
* [2. 重链剖分基础](#2)
* [2.1. 重剖求 LCA](#21)
* [2.1.1. 引入](#211)
* [2.1.2. 定义](#212)
* [2.1.3. 时间复杂度](#213)
* [2.1.4. 实现](#214)
* [2.2. 重剖维护树上修改查询](#22)
* [2.2.1. 如何转化](#221)
* [2.2.2. 实现](#222)
* [3. 进阶提高篇(重链剖分的一些套路)](#3)
* [3.0. 边转点技巧](#30)
* [3.1. P5838 [USACO19DEC] Milk Visits G 动态开点+多颗线段树](#31)
* [3.2. Jamie and Tree 换根树剖](#32)
* [3.3. P3401 洛谷树 拆位统计](#33)
* [3.4. P7735 [NOI2021] 轻重边 染色+计数](#34)
* [3.5. Dynamic Diameter 线段树上的奇怪 pushup](#35)
* [3.6. Drivers Dissatisfaction MST 相关](#36)
* [3.7. P2542 [AHOI2005] 航线规划 图转树+离线操作](#37)
* [3.8. ...Wait for it... 树上拆点维护](#38)
* [3.9. P5138 fibonacci 树上点权套斐波那契数列套矩阵快速幂](#39)
* [3.10.P4175 [CTSC2008] 网络管理 重剖+整体二分](#310)
* [3.11.Noble Knight's Path 重剖+主席树+二分](#311)
* [3.12.Moonwalk challenge 重剖+二分+hash](#312)
* [3.13.Tourists 圆方树上重剖+multiset 存信息维护](#313)
* [4. 长链剖分](#4)
* [4.1. 定义](#41)
* [4.2. 实现](#42)
* [4.3. 应用(板子)](#43)
* [4.4. 应用(长剖优化dp)](#44)
* [4.5. 后记(个人关于长剖看法)](#45)
* [5. 平衡树部分(Splay)](#5)
* [5.1. 二叉搜索树](#51)
* [5.1.1 定义](#511)
* [5.1.2 应用](#512)
* [5.1.3. 时间复杂度](#513)
* [5.2. 平衡树](#52)
* [5.2.1. 定义](#521)
* [5.2.2. 如何维护平衡](#522)
* [5.2.3. 操作实现](#523)
* [6. 实链剖分(LCT)](#6)
* [6.1. 定义](#61)
* [6.2. 实现](#62)
* [6.3. 应用(板子)](#63)
* [6.4. 应用(技巧)](#64)
* [6.4.0.P3203 [HNOI2010] 弹飞绵羊 轻量化 LCT](#640)
* [6.1.1./6.1.2.P4172 [WC2006] 水管局长 LCT 维护边权+MST+倒序操作](#611/612)
* [6.1.3.P4319 变化的道路 线段树分治+动态 MST](#613)
* [6.1.4.P2542 [AHOI2005] 航线规划 LCT 维护边双联通分量](#614)
* [6.1.5.P4219 [BJOI2014] 大融合 LCT 维护子树信息](#615)
* [7. 资料来源鸣谢](#7)
**P.S.:洛谷博客不支持页内跳转,所以如果想要享受跳转功能的欢迎来 cnbolgs 踩爆我(目前 cnblogs 还未完工)。**
------------
# -1.Update
2024.1.17. LCT 技巧部分更新了一些。
upd:给大部分我自己写过的题附上了代码。
完工后会删掉这个部分。
------------
# 0. 前言
- **Warning:打开这篇文章前最好确保自己已经学过了倍增,树上差分,dfs序等知识点。一定要学过线段树!!!**
写这篇文章的主要目的是为了介绍树链剖分以及树链剖分的一些模型。
可能你们也注意到了,这篇文章的标题是 **『从入门到入土』树链剖分**。
而不是重链剖分或 LCT。旨在一次性从重链剖分开始讲到 LCT 结束。
那么,在正式开始之前,先写下致谢吧:
- 带我进树剖坑的大佬 [lzyqwq](https://www.luogu.com.cn/user/539211)。这里提供的题单的前身也正是他的博客中的树剖题单捏。
- 同时感谢让我学会了长剖的[树剖姐姐 Ynoi ](https://www.luogu.com.cn/user/124721)的[博客](https://www.luogu.com.cn/blog/Ynoi/zhang-lian-pou-fen-xue-xi-bi-ji)。
- 还有 LCT 的题单前身正是大佬 [zhiyangfan](https://www.luogu.com.cn/user/137603) 的 LCT题单。
- 最后鸣谢下让我学会了 LCT 的大佬 [FlashHu](https://www.luogu.com.cn/user/61325) 的博客
#### 希望大家都能在看完后学会树剖!
**P.S.:本文的分隔方式如下:**
- 一整大块的分隔,采取一级标题+加粗字体。
- 同一大块中的一小块分隔,采取三级标题的方式。
- 一小块中的分隔,采取分隔线的方式。
------------
# **1. 附题单:**
[『基础篇』重链剖分](https://www.luogu.com.cn/training/440256)
[『应用(折磨)篇』重链剖分](https://www.luogu.com.cn/training/440258)
[『究极折磨篇』LCT 题单](https://www.luogu.com.cn/training/440260)
------------
# **2. 重链剖分基础**
## 2.1. 重剖求 LCA
### 2.1.1. 引入
重剖,与树相关,在树上的他,有一个最基础也是最重要的作用:[**求 LCA**](https://www.luogu.com.cn/problem/P3379)。
求 LCA 可谓是树上最基础也是最重要的部分了,基本所有的树上询问与路径有关的问题,都会考虑拆成 **$u\longrightarrow lca,lca\longrightarrow v$** 的方式来进行计算。
想必大家都会倍增求 LCA 吧,他的方法就是尝试往上跳 $2^j$ 步,然后从大到小枚举 $j$。像二进制拆分一样把需要跳的步数变为二进制下的 $0,1$ 来跳,就可以保证 $\log{n}$ 的复杂度。
对于重链剖分而言,则是需要找到另一种跳跃的方式,使得跳跃的步数可以压缩,使得最终复杂度为 $O(n\log{n})$。
正如他的名字一般,重剖所采取的方法,就是把一颗树剖成一条条的重链,以一次至少跳掉一条重链的方式来保证其复杂度。
### 2.1.2. 定义
了解了重链剖分的原因之后,我们就可以正式开始学习重链剖分了。
先给出几个定义以及一张图(参考资料 [OI Wiki](https://oi-wiki.org/graph/hld/)):
- 定义 **重子节点** 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。
- 定义 **轻子节点** 表示剩余的所有子结点。
- 从这个结点到重子节点的边为 **重边**。
- 到其他轻子节点的边为 **轻边**。
- 若干条首尾衔接的重边构成 **重链**。
- 把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。
![](https://oi-wiki.org/graph/images/hld.png)
个人认为,对于上面把落单的节点也看做重链还有一种解释方式,即:
- 重链的定义为若干条首尾相连的重边顶端,接上一条轻边。(除了根节点上接出的第一条重链)
那么落单的节点的情况便变为了『若干』 为 $0$ 的情况。
### 2.1.3. 时间复杂度
这么定义是有原因的,我们容易证明,一个点一直往上跳,最多只会经过 $\log{n}$ 条重链。
下面给出证明:
- 考虑从最上面的根节点开始往下走。
- 最开始的子树大小即为 $n$。因为共有 $n$ 个节点。
- 而当我们每走过一条 **轻边** 的时候,子树大小至少会减半(不然这条边就变成重边了)。
- 那么从子树大小 $n$ 开始减小至 $1$ 的复杂度便是 $\log{n}$,也就是最多只会经过 $\log{n}$ 条轻边。
- 证毕。
### 2.1.4. 实现
接下来,考虑顺推的思路,既然经过轻边的次数已经被保证了,也就意味着,一条路径最多只会被拆分为 $\log{n}$ 条重链,接下去,我们只需要保证路过一条重链的复杂度为 $O(1)$ 即可。
先把上面那张图拿下来,方便讲下。
![](https://oi-wiki.org/graph/images/hld.png)
P.S.:下文所提之编号指的是图中的 dfn 序
比如对于节点 $2,4$ 的 LCA。因为他们在同一条重链中,显然就是其中深度较浅的一个节点。写成代码的形式就是。
```cpp
if(top[x]==top[y])
{
if(dep[x]<dep[y]) return x;
else return y;
}
```
这里的 $top$ 指的是当前节点的重链顶端,因为每个点所属的重链显然不能重复,(也就是重链将树完全剖分。)所以我们可以直接用这个点所属重链顶端的节点编号是否相同来判断是否属于同一重链。
而 $dep$ 指的便是这个节点在树中的深度。
接着,我们再来考虑当 $u,v$ 不属于同一条重链时的情况,结合前面所提到的
- 『我们只需要保证路过一条重链的复杂度为 $O(1)$ 即可』
我们很容易发现,当 $u,v$ 不在同一条重链时,这两条重链中至少有一条是没有贡献了的。
- **也就是 LCA 至多只会出现在其中的一条重链中。**
那么我们便可以直接跳过其中的一条重链。
那么到底是跳过那一条呢?
显然不是根据当前节点的深度判断的,而是根据
- **目前重链所连接的上一条重链于这条重链所连的第一个节点**的深度决定的。
说起来有点抽象,但是我可以举一个图里面的例子。
如下图(没想到吧还是他):
![](https://oi-wiki.org/graph/images/hld.png)
其中编号为 $10$ 的节点**所属重链所连接的上一条重链于这条重链所连的第一个节点**便是 $2$ 号节点。
写成代码的形式也就是:
```cpp
fa[top[x]]
```
其中 $fa$ 数组代表当前节点的父亲节点。
跳了一次之后怎么考虑呢?
- 回到刚刚开头判断是否在同一条重链中的情况即可,如果找到了就结束循环,否则继续判断。
下面给出树剖求 LCA 的完整代码:
```cpp
int LCA(int x,int y)
{
while(top[x]!=top[y])
{
if(dep[fa[top[x]]]<dep[fa[top[y]]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}
```
现在,我们已经学会了树剖求 LCA 的方法,但是,如果光光使用上面这个代码,我们会发现一个非常严重的问题。
就是:
- 我们的 $fa,dep,top$ 数组都未曾定义求过值。
考虑如何进行预处理,得出我们所需要的这些值。
------------
因为是重链剖分,所以我们肯定要求出每个节点的子树大小,以及他所连的重儿子是谁。
数组的名称与意义说明:
- $\mathrm{fa_i}$ 代表 $i$ 节点的父亲节点,特殊的,根节点没有父亲。
- $\mathrm{dep_i}$ 代表 $i$ 节点的深度,特殊的,根节点的深度为 $1$。
- $\mathrm{si_i}$ 代表 $i$ 节点的子树大小,特殊的,叶子节点的子树大小为 $1$。
- $\mathrm{son_i}$ 代表 $i$ 节点的重儿子编号,特殊的,叶子节点没有重儿子。
- $\mathrm{top_i}$ 代表 $i$ 节点的重链顶端节点编号,特殊的,重链顶端节点的 $\mathrm{top}$ 数组值为他本身。
实现时,我们考虑使用 dfs,第一次处理出 $\mathrm{fa,dep,si,son}$,第二次处理出 $\mathrm{top}$。
正常情况下,我们考虑以 $1$ 节点为根节点进行剖分。
写成代码就是(这里使用的是邻接表就不多做解释了):
```cpp
void dfs1(int u,int ff)//ff是当前节点的父亲
{
fa[u]=ff,si[u]=1,dep[u]=dep[ff]+1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(v==ff) continue;
dfs1(v,u);si[u]+=si[v];
if(si[son[u]]<si[v]) son[u]=v;
}
}
void dfs2(int u,int topf)//topf是当前重链顶端
{
top[u]=topf;
if(son[u]) dfs2(son[u],topf);
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(v==fa[u]||v==son[u])continue;
dfs2(v,v);
}
}
```
注意到这里的代码都挺好理解的,除了 dfs2 中的
```cpp
if(son[u]) dfs2(son[u],topf);
```
显然,判重儿子这一步可以放进循环里来单独判断,但这里为什么要拿出来呢?
这里我先卖个关子先,这和之后树剖结合数据结构有密切联系。
那么有了两遍 dfs 的预处理后,树剖便能完成 $O(m\log{n})$ 的复杂度完成[**求 LCA**](https://www.luogu.com.cn/problem/P3379) 了。
[『模板』求 LCA 的 std](https://www.luogu.com.cn/paste/daml9iuj)
------------
## 2.2. 重剖维护树上修改查询
### 2.2.1. 如何转化
上一块引入部分,我们讲了重剖求 LCA 的方式,但是,重剖的用途远不止此。
要是重剖只能求 LCA 的话,还不如魏老师的**好写好调且小常** $O(n\log{n})$ dfs 序大法呢。
所以接下来,我们要在树剖上套上数据结构,具体的例题便是[重剖模板](https://www.luogu.com.cn/problem/P3384)。
在这题里面,我们发现询问的东西,有了了 $u,v$ 路径上的点权和。
而操作里面,也增加了修改一条链上的点权值。
(另外两种操作我们先暂且不考虑,优先考虑这两种。)
这个时候我们会想到什么呢?
要是询问的变成了一个区间,修改的也是一个区间和的话,就可以用线段树方便处理了。
那么我们怎么把树上路径转化为与区间相关的东西呢?
链不就能压为是一个区间吗,那我们只需要把树上路径转化为一条条链即可。
那怎么转化为链呢,这不就是重剖吗。
至此转化的问题已经解决了,但是还有一个问题:我们的线段树不是有一个序列的编号吗,那我们从树中拿下来的链编号怎么定呢?
这个时候就要涉及到 dfs 序了,还记得之前卖的那个关子吗(不记得回去看一下)。
- 我们把重儿子的遍历放在前面就是为了保证**同一条重链的编号连续!**
那么,我们的 dfs2 就要加上两个数组,他们是:
- $\mathrm{dfn_i}$ 代表的是 编号 $i$ 的这个节点他的 dfs 序。
- $\mathrm{id_i}$ 代表的是 dfs 序为 $i$ 所对应的节点编号。
写成代码也就是:
```cpp
void dfs2(int u,int topf)
{
top[u]=topf;dfn[u]=++cnt,id[cnt]=u;
if(son[u]) dfs2(son[u],topf);
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(v==fa[u]||v==son[u])continue;
dfs2(v,v);
}
}
```
至此,我们就保证了同一条重链上的 dfs 序连续,可以用这个序来作为线段树的区间下标了。
也就如上面那个图一样:
![](https://oi-wiki.org/graph/images/hld.png)
------------
### 2.2.2. 实现
首先考虑实现路径转链的操作,根据之前的理解和观察不难发现,其实我们求 LCA 时跳过的一条条链就是我们分割出来的一条条链。所以,写成代码就是:
```cpp
int query(int u,int v)
{
int res=0;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
res+=query(1,dfn[top[u]],dfn[u]);res%=mod;
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
res+=query(1,dfn[u],dfn[v]);res%=mod;
return res;
}
```
其实这个代码和 LCA 出入不大了,我们放一块对比一下试试:
```cpp
int LCA(int x,int y)
{
while(top[x]!=top[y])
{
if(dep[fa[top[x]]]<dep[fa[top[y]]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}
```
会发现,其实我们的询问只是把在跳的过程中每条重链的值累加上来了罢了。
- **Warning:这里尤其要注意在线段树查询操作中的询问区间一定是小的写在前面,也就是深度小的点写在前面!**
接着的修改操作 modify 与其区别不大,唯一的区别只是把 query 改为了 modify 罢了。
回归原题目,想起来还有两种操作是子树权值加和子树权值和查询,比起前面的路径操作,这个明显简单了许多,因为是 dfs 序,所以子树里的序号显然是连续的,直接区间修改查询即可。
最后是[重剖模板 std](https://www.luogu.com.cn/paste/intvy0te)。
#### 截止这里,你已经学会了最基础的树链剖分了,接下去,打开上面题单中的基础篇进行一些练习吧!
树剖主要就是码量比较逆天,还是很需要练习的。
------------
# 3. 进阶提高篇(重链剖分的一些套路)
看到这里说明你已经理解了重剖而且写过了一定的基础码量题,现在,你可以开始真正应用重剖了。
本章节将会讲一些重剖中比较模板的套路或是好题,方式是结合例题分析。
- **Warning:从此段开始难度直线上升,至少是上位蓝。**
- **P.S.:如果找不到锅了可以对下我的[出锅合集](https://www.luogu.com.cn/paste/ahc7lu2i)。**
题目讲解排序按照难度不一定单调递增。
那就开始吧:
------------
### **3.0. 边转点技巧**
在写重剖的时候我们会发现所查询与修改的都是点权,但是在题目中我们可能会碰到边权的问题,那怎么办呢?
显然,我们可以考虑把边权转化为点权考虑。
那对于一条边而言,我们的边权应该放到哪个节点上呢?
应该是下面的那个节点吧,那么我们就完成了边转点的操作。
对于查询呢?
这个时候要注意一个判断,因为我们把边转点了。所以 $u,v$ 的 LCA 需要特判。
LCA 的点权是他上面的一条边的边权。
显然这个是查询不到的,所以**千万不要加上!**
那么边转点就算是说完了,可以先写个练习下。
------------
### **3.1. [P5838 [USACO19DEC] Milk Visits G](https://www.luogu.com.cn/problem/P5838) 动态开点+多颗线段树**
这题有个弱化版,可以先考虑下[弱化版](https://www.luogu.com.cn/problem/P5836)。
##### **题目大意**
给定一颗树,树上每个点有个点权,询问 $u,v$ 的路径上有没有给定的点权出现过。
##### **思路分析**
首先考虑下弱化版先,发现弱化版点权只有两种,所以直接考虑分类讨论,不同的查找分类讨论一下处理。
或者是直接暴力做,给线段树里面加入这个区间是否有其中一种奶牛的标记就可以简单秒杀了。
接着来考虑金组的加强版,有 $10^5$ 种点权,处理便困难起来了,如果像之前那样直接暴力做,想都不用像就会挂了。
所以我们考虑使用 STL 大法,因为题目只让我们判断有没有而不问个数,所以我们可以直接套上一个 set。
题目中也没有修改操作,所以 set 十分好维护,只要在造的时候加进来就可以了。
复杂度 $O(n\log^3{n})$。
[附代码](https://www.luogu.com.cn/paste/mxt6l17d)
显然这种做法不是很优,所以我们考虑一种优化的思想:
对着每一种颜色开一颗线段树,查询便变成了在对应颜色的线段树上查找是否出现过。
显然如果我们直接开 $10^5$ 颗线段树空间会一言难尽,所以我们考虑用动态开点线段树维护,每个颜色也开个根节点。
(因为我懒所以就不再自己写代码了,可以参考下题解中神犇 lzyqwq 的代码。)
##### **练习题**
[P3313 [SDOI2014] 旅行](https://www.luogu.com.cn/problem/P3313)
这个题挺显然的,只是有点难写。
[std](https://www.luogu.com.cn/paste/qubeyzat)。
[[ABC133F] Colorful Tree](https://www.luogu.com.cn/problem/AT_abc133_f)
这个题只需要注意下每次并不是真的修改。
所以每次只要查找**给定颜色的边的总权值**。
然后用原本的权值,减去**给定颜色的边的总权值**再加上**给定颜色的边的数量**乘以**更改后的边权**即可。
[std](https://www.luogu.com.cn/paste/5fgry5oj)。
[GOT - Gao on a tree](https://www.luogu.com.cn/problem/SP11985)
刚刚讲的那题,但是多组数据,卡掉了 $O(n\log^3{n})$ 的做法。
这边建议写成刚刚讲的那种方法,而不是 $O(n+m)$ 的强大方式,当然这个方式最好也写下。
[Santa's Gift](https://www.luogu.com.cn/problem/CF960H)
期望题,先展开下式子,然后因为颜色限制,所以考虑多颗线段树开拆即可。
[std](https://www.luogu.com.cn/paste/id2ofcy4)。
[Caisa and Tree](https://www.luogu.com.cn/problem/CF463E)
> - 「保证 $2$ 操作的数量不超过 $50$」的限制和 $10$ 秒的时限太不牛了。让我们去掉这个限制并将时限改为 $2$ 秒,再考虑怎么做。
> - by lzyqwq
这里使用和神犇相同的做法谢谢喵。
考虑把质因数拆出来,不同质因数的树开一颗就行了。
[std](https://www.luogu.com.cn/paste/grd0254h)。
------------
### **3.2. [Jamie and Tree](https://www.luogu.com.cn/problem/CF916E) 换根树剖**
这道题是一个非常典的案例,这种题我称为**换根树剖**。
和他的名字一样,他的主要操作就是**换根树剖**。
也就是,这棵树的根可能会变,而他的查询,也是和子树有关。
碰到这种没有碰过的套路题一定不要慌,仔细观察一下很容易得出:
这不就**分类讨论**吗?
我们肯定不能每次换根每次重构,所以我们设定的那个根不妨称之为假根(正常应该都设置为 $1$ 吧),那真正的根和假根会导致什么误差呢?
只有子树会有部分不同吧,那分类讨论即可:
(这里复制了[神犇 Farkas_W ](https://www.luogu.com.cn/blog/Farkas/guan-yu-shu-lian-pou-fen-huan-gen-cao-zuo-bi-ji)里面的讲解图片(自己花真的要累死的)分隔线内是复制内容+更改捏)
------------
一.当 $lca(x,y)=x$ (这里满足 $x$ 的深度更小)
![](https://cdn.luogu.com.cn/upload/image_hosting/hk78elxs.png)
$\quad$ $1$. 情况 $1$ :$root$ 在 $x$ 的子树中,也在 $y$ 的子树中,即 $lca(x,root)=x$ && $lca(y,root)=y$ ,此时 $LCA(x,y)$ 是 $y$ ,因为图要反过来看(以 $root$ 为根)
$\quad$ $2$. 情况 $2$ : $root$ 在 $x$ 的子树中,但不在 $y$ 的子树中,即 $lca(x,root)$ ,此时 $LCA(x,y)$ 是 $lca(y,root)$。
$\quad$ $3$. 情况 $3$ :其他情况下, $LCA(x,y)$ 就是 $x$ 。
二.当 $lca(x,y)!=x$ (因为 $x$ 的深度更小,所以 $lca(x,y)!=y$,且 $x,y$ 在不同子树上)
![](https://cdn.luogu.com.cn/upload/image_hosting/jg4eo3ji.png)
$\quad$ 1. 情况1:( $lca(x,root)=x$ )||( $lca(x,root)=x$ ),root在x(或y)的子树中时, $LCA(x,y)$ 为 $x$ (或 $y$ ),显然。
$\quad$ 2. 情况2:( $lca(x,root)=root$ && $lca(x,y)=lca(y,root)$ )||( $lca(y,root)=root$ && $lca(x,y)=lca(x,root)$),即 $root$ 在 $x$ 到 $y$ 的简单路径上时,答案为 $root$ 。(也可以用深度判断, ( $lca(x,root)=root$ && $dep[root]>=dep[lca(x,y)]$ )||( $lca(y,root)=root$ && $dep[root]>=dep[lca(x,y)]$ ))
$\quad$ 3. 情况3: $lca(x,root)=lca(y,root)$ ,即 $root$ 在上方时,$LCA(x,y)$ 为 $lca(x,y)$ 。
$\quad$ 4. 情况4:当 $root$ 在$x$,$y$ 的链上节点的子树中时, $LCA(x,y)$ 为那个链上节点。
$\quad$这样就把树上所有 $root$ 位置的情况都考虑到了,不重不漏。
$$\text{子树修改(查询)}$$
![](https://cdn.luogu.com.cn/upload/image_hosting/4ar4m3w5.png)
$\quad$ 情况 $1$ :当 $x=root$ 时, $x$ 就是此时整棵树的根,那么就是全局修改(查询)。
$\quad$ 情况 $2$ :当 $root$ 在x子树中时,就需要特别判断了,根据图像我们可以发现此时x的真正子树是包括除了 $root$ 方向上的子树之外其他所有节点。
$\quad$ 情况 $3$ :其他情况下 $x$ 的子树以 $root$ 为根和以 $1$ 为根是一样的。
------------
分类讨论之后这题就直接秒杀为正常树剖了,附上[ std](https://www.luogu.com.cn/paste/v02sccvn)。
##### **练习题**
[P3979 遥远的国度](https://www.luogu.com.cn/problem/P3979)
这个是真的基本没区别,也没什么好说了,自己好好写一遍应该就能真正记住了。
[std](https://www.luogu.com.cn/paste/uyws54un)。
------------
### **3.3. [P3401 洛谷树](https://www.luogu.com.cn/problem/P3401) 拆位统计**
这题也是一个比较套路的操作,就是拆位。
考虑把每一位的 $0,1$ 情况拆出来考虑,就成了正常的题目了。
附上[ std](https://www.luogu.com.cn/paste/kuunmriz)。
##### **练习题**
[P5354 [Ynoi2017] 由乃的 OJ](https://www.luogu.com.cn/problem/P5354)
不过这题用拆位的方法不能过(或许可以卡常卡过去?),纯当练习好了。
------------
### **3.4. [P7735 [NOI2021] 轻重边](https://www.luogu.com.cn/problem/P7735) 染色+计数**
最折磨的典中典,这题是真的好,他应该不能算是一个固定的套路,但是好题还是讲一下吧。
- 我们考虑给每一次操作的两个端点染上独一无二的颜色。
没有太懂的话,具体的讲一讲,因为要将一条路径上的边修成重边,那么路径点染同色的操作就可以保证这条路径上**点同色且所有边两端点同色**,这样这些边就符合重边的判定了。
由于这些点被染成的是一种**独一无二的颜色**,是一定不与前面的颜色重复的,所以所有染色点连边的另一端一定与它的颜色不同,这样子这些边就自然而然的变成了轻边。
所以就美妙转化掉了,成了如何统计树的一段路径上**同色相邻点对**的数量。
这个时候就来考虑线段树维护了,但是由于重链相连接的地方可能会产生贡献,所以我们写的时候要分开写,也就是左边跳左边的,右边跳右边的。
给个代码看看吧:
```cpp
int query(int x,int y)
{
bool flag=0;
tree h,ans1=(tree){0,0,0,0,0,0},ans2=(tree){0,0,0,0,0,0};
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) flag=!flag,swap(x,y);
h=query(1,dfn[top[x]],dfn[x]);//cout<<h.sum<<" "<<x<<" "<<y<<endl;
if(flag) ans2=(tree){0,0,h.cl,ans2.cr,ans2.sum+h.sum+(ans2.cl==h.cr),0};
else ans1=(tree){0,0,ans1.cl,h.cl,ans1.sum+h.sum+(ans1.cr==h.cr),0};
x=fa[top[x]];
}
if(dep[x]<dep[y]) swap(x,y),flag=!flag;
h=query(1,dfn[y],dfn[x]);
if(flag) ans2=(tree){0,0,h.cl,ans2.cr,ans2.sum+h.sum+(ans2.cl==h.cr),0};
else ans1=(tree){0,0,ans1.cl,h.cl,ans1.sum+h.sum+(ans1.cr==h.cr),0};
return ans1.sum+ans2.sum+(ans1.cr==ans2.cl);
}
```
也就是左边是 $ans1$ 右边是 $ans2$。
最后附上代码[ std](https://www.luogu.com.cn/paste/qhuuq43t)。
#### 练习题
[P2486 [SDOI2011] 染色](https://www.luogu.com.cn/problem/P2486)
这个是双倍经验吧。
------------
### **3.5. [Dynamic Diameter](https://www.luogu.com.cn/problem/CF1192B) 线段树上的奇怪 pushup**
**这题,真的,是,太痛苦了!**
我不知道刚学重剖的我当时怎么想的写了这个题,这个题是真的很痛苦。
题意很显然的,求直径的话我们可以弄两颗线段树,一个是维护的节点间距离,一个维护的是 $dfn$ 在这个区间内的树的直径大小。
很显然对吧,接着就是痛苦实现了,这题的唯一痛苦就在实现上。
考虑下如何合并第二颗线段树中的值,也就是答案线段树中的值。
一颗树的一条直径是两个点之间的一条路径对吧,那两个子树就有两条直径四个点对吧。
这边可以证明出来最后树的直径肯定是在这四个点中两两组合中取一个最大。
请读者自行尝试证明一二。
接着有了这个之后我们只需要算出来两点距离就行了,拿另一颗线段树查询就完了。
##### 真的很暴力!
附上 [ std](https://www.luogu.com.cn/paste/c5r2crro)。
##### **练习题**
[P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并](https://www.luogu.com.cn/problem/P4556)
线段树合并板子,可以学下挺好的。
[std](https://www.luogu.com.cn/paste/lefojdxa)。
[P4679 [ZJOI2011] 道馆之战](https://www.luogu.com.cn/problem/P4679)
这个合并的东西有点不一样,是二维的好玩。
[std](https://www.luogu.com.cn/paste/yij6cff9)。
[P9555 「CROI · R1」浣熊的阴阳鱼](https://www.luogu.com.cn/problem/P9555)
这题合并的时候**注意顺序!注意顺序!!注意顺序!!!**
[std](https://www.luogu.com.cn/paste/m0gx4bg5)。
[Tavas on the Path](https://www.luogu.com.cn/problem/CF536E)
这题折磨的,一定要做好心理准备。
[std](https://www.luogu.com.cn/paste/gn00zmzk)。
[Moonwalk challenge](https://www.luogu.com.cn/problem/CF1045J)
这题使用奇怪 pushup 的做法可以直接把整段的字符串弄出来然后再数次数。
虽然过不了,但是练习码力还是很好的。
- 如果说你用的是 find 查询次数,那写到最好情况应该是 TLE on #15。
- 如果你写的是 hash 查询次数的话,那最好应该是写到 RE on #4。
亲身实践得到的/cf。
[std(使用 find 实现的 TLE on #15)](https://www.luogu.com.cn/paste/268oot4l)。
[std(使用 hash 实现的 RE on #4)](https://www.luogu.com.cn/paste/xaizjxb6)。
------------
### **3.6. [Drivers Dissatisfaction](https://www.luogu.com.cn/problem/CF733F) 重剖+MST**
这题看到后我们会有一个比较显然的想法,把钱全部花在一条边上,因为如果我花在两条边上,不如花在一条代价更小的边上。
他相当于是固定了一条边肯定要在树中的最小生成树,参考[板子严格次小生成树](https://www.luogu.com.cn/problem/P4180)中的做法,先求最小生成树然后考虑换边即可。
##### **练习题**
[Best Edge Weight](https://www.luogu.com.cn/problem/CF827D)
讲解见[文章](https://www.luogu.com.cn/blogAdmin/article/edit/694725)了,其实我感觉这题挺像动态 MST 的?
[MST Unification](https://www.luogu.com.cn/problem/CF1108F)
[std](https://www.luogu.com.cn/paste/0vtpzwd1)。
------------
### **3.7. [P2542 [AHOI2005] 航线规划](https://www.luogu.com.cn/problem/P2542) 图转树+离线操作**
里面保证了这么一句话在整个数据中,任意两个星球之间最多只可能存在一条直接的航线。题目保证不存在重边,而且互相连通,又是无向图...想到了什么?
**图可以变为树啊**!也就是说,我们需要动态的维护树上点之间的距离。
然后树剖维护树上的边权,这样就可以轻而易举的求出树上两点之间的距离了。
直接将一个环内的点之间的边权赋为0不就行了!
读入询问,逆序处理。
先随便在原图中求出一棵生成树。
然后用那些没被删除的非树边先更新一遍当前的边权。
最后树剖猛猛做。
注意经典边转点细节,不要修改 **LCA!!!!!!**
附上 [ std](https://www.luogu.com.cn/paste/2nu9yt12)。
##### **练习题**
[OTOCI - OTOCI](https://www.luogu.com.cn/problem/SP4155)
[P4312 [COI2009] OTOCI](https://www.luogu.com.cn/problem/P4312)
两题通用[std](https://www.luogu.com.cn/paste/1pze1pfl)。
**P.S.:这两题是没有强制在线,原题是用交互保证在线的,现在可以离线!**
[P3950 部落冲突](https://www.luogu.com.cn/problem/P3950)
[std](https://www.luogu.com.cn/paste/tgzbawuv)。
------------
### **3.8. [...Wait for it...](https://www.luogu.com.cn/problem/CF696E) 树上拆点维护**
考虑第一篇题解里的做法,把每个点拆成出点和入店,权值为无穷大。
接着一个物品单独造一个点,和他前面后面的点连起来。
对于查询而言的话,就是暴力每次扫一个删一个。
复杂度因为是删掉物品,所以可以保证。
**很简单对吧,特别难调!**
附上 [ std](https://www.luogu.com.cn/paste/07zjxbdp)。
##### **练习题**
[P5478 [BJOI2015] 骑士的旅行](https://www.luogu.com.cn/problem/P5478)
[std](https://www.luogu.com.cn/paste/xxqtscp8)。
[Duff in the Army](https://www.luogu.com.cn/problem/CF587C)
这三题好像没有什么区别/kk。
------------
### **3.9. [P5138 fibonacci](https://www.luogu.com.cn/problem/P5138) 树上点权套斐波那契数列套矩阵快速幂**
这题的话,我认为主要的重点是一个结论的背诵。
也就是:
$$f_{m+n}=f_{m-1}\times f_n+f_m\times f_{n+1}$$
这个结论非常重要,基本涉及 fibonacci 数列的题大概率用到吧。建议熟背。
有了结论之后我们就可以开拆式子了。
首先把 $u,v$ 的距离弄出来,考虑下用与根的距离算,也就是 $dis_u-dis_v+k$。然后用上面那个公式给式子拆开做即可。
也就是令 $m=dis_u,n=k-dis_v$ 之后换算式子,左右当做两个 $tag$ 写就可以了。
附上[ std](https://www.luogu.com.cn/paste/nx9b8q15)。
------------
### **3.10.[P4175 [CTSC2008] 网络管理](https://www.luogu.com.cn/problem/P4175) 重剖+整体二分**
这道题是我第一次接触整体二分/kk。
首先概括下题意:带修改树链第 $k$ 大问题。
应该算是一个经典问题了。
首先说下整体二分,就是用分治的思想一次性完成所有的操作,其中包括了修改与查询等。最后一次输出。
这道题也就是相当于把整体二分上树。
考虑把每个操作都标记一个时间戳 $cntt$,保存在操作记录数组 $t$ 中。
由于树初始带有点权,所以就相当于初始单点修改了 $n$ 次,把每个点改为对应的点权。
更改点权就相当于先删除后增加点权了。
剩下的就是整体二分的套路了。
附上[ std](https://www.luogu.com.cn/paste/2ledkyub)。
##### **练习题**
[P3250 [HNOI2016] 网络](https://www.luogu.com.cn/problem/P3250)
------------
### **3.11.[Noble Knight's Path](https://www.luogu.com.cn/problem/CF226E) 重剖+主席树+二分**
首先这题要保存多颗树的状态返回查询,所以考虑使用主席树。
其次,这道题要找到路径上的第 $k$ 个没有被入侵过的城堡。
发现这个查询有单调性,也就是排名单调性。
所以考虑二分,只要在对应版本的主席树上二分即可。
附上[ std](https://www.luogu.com.cn/paste/0n28vk9n)。
------------
### **3.12.[Moonwalk challenge](https://www.luogu.com.cn/problem/CF1045J) 重剖+二分+hash**
折磨的黑,前面的练习题里面提到过了,所以我还是讲下好了。
首先讲下部分的做法,考虑直接暴力开搞。
给路径上的字符串直接整出来,然后对着 hash 开跑匹配。
很好的是,hash 要的空间是 $2.6\times 10^{11}$。而且至少得是 $long long$,而且容易被卡。
所以考虑优化。
预处理出每条重链向上和向下的 hash 值,然后像之前一样。
对于跨越重链的部分就用 $u\rightarrow lca,lca\rightarrow v$ 的方法拆分即可。
发现这个 hash 他具有可减性与单调性,所以考虑套上二分。
附上[ std](https://www.luogu.com.cn/paste/0jmkxcjl)。
##### **练习题**
[Misha and LCP on Tree](https://www.luogu.com.cn/problem/CF504E) hash+重剖。
[std](https://www.luogu.com.cn/paste/8aqqdbuy)。
[P6088 [JSOI2015] 字符串树](https://www.luogu.com.cn/problem/P6088) 可以 hash+重剖捏
------------
### **3.13.[Tourists](https://www.luogu.com.cn/problem/CF487E) 圆方树上重剖+multiset 存信息维护**
这题挺板子的,但是属于一个典例了,所以还是拿出来讲下吧。
首先发现这题是个图,所以没法做了。
我们考虑使用广义圆方树,把图变为树。
然后再树上使用树链剖分。
但是要注意的是,这题不能在圆点改权值时,改周围所有方点的权值,不然会被菊花图卡到 $n^2$。
所以对于一个方点,multiset 里面存它所有子节点的权值。然后修改一个圆点时,就只需要动它父亲的 multiset(它的父亲必然是一个方点)。询问时,仍然可以正常询问,只不过如果 $lca$ 是一个方点,那还要额外计算 $fa[lca]$ 的权值对答案的贡献。
这样就做完了。
std 就不附了,这题我写的太抽象了。
------------
### **3.14.[GSS7 - Can you answer these queries VII](https://www.luogu.com.cn/problem/SP6779) 重剖+最大子段和**
这个很经典啊,但是我忘记给他拿出来了,现在补下讲解。
首先是最大子段和这个经典操作。
考虑线段树维护几个信息:
- $\mathrm{ans}$ 表示这个节点的最大子段和
- $\mathrm{sl}$ 表示从左开始的最大连续子段和
- $\mathrm{sr}$ 表示从右开始的最大连续子段和
- $\mathrm{s}$ 表示这个节点维护区间的和
- $\mathrm{lz}$ 代表这个节点的懒标记的值
- $\mathrm{f}$ 代表这个节点是否有懒标记
那就很好处理了,这边我就讲下 merge 应该就够了:
```cpp
tree merge(tree l,tree r)
{
tree res;res.lz=res.f=0;
res.sl=max(l.sl,r.sl+l.s);res.sr=max(r.sr,l.sr+r.s);
res.s=l.s+r.s;res.l=l.l;res.r=r.r;
res.ans=max(l.ans,max(r.ans,l.sr+r.sl));
return res;
}
```
合并的时候,考虑
$$res.ans=max(l.ans,max(r.ans,l.sr+r.sl));$$
即可。
对于树链上的查询,我们考虑把 $u\rightarrow v$,分为 $u\rightarrow lca,lca\rightarrow v$,然后拿 $resl$ 维护左边答案,$resr$ 维护右边的答案,最后合并即可。
注意,这里合并的时候要把 $resl$ 或者 $resr$ 的 $sl,sr$ 翻转一下,因为树上路径不是从 $lca$ 出发向 $u,v$ 走的,而是从 $u$ 到 $v$ 的。
附上[ std](https://www.luogu.com.cn/paste/migf2vzo)。
------------
终于写完了,难受的。
(其实还有很多的模板,只不过我还没写所以随缘更新了。)
接下去的练习就要来到我的折磨题单里了/cf。
[『应用(折磨)篇』重链剖分](https://www.luogu.com.cn/training/440258)
共 $85$ 道紫黑/cf。
------------
## **3.15.后记**
这里说说我自己写了这么多重剖题后的感受吧。
首先是有关于出锅的方面:感觉每道题都**五彩斑斓**的,但出锅最多的总是**剖**。
比如我个人而言,就写过随机剖分,$si_v+=si_u$ 这种逆天东西。
然后是有关于重剖的用途,重剖其实是可以把一些序列上的问题强制放到树上的。
但是实际上我认为的好树剖题都有一个崭新的部分,比如[P3925 aaa被续](https://www.luogu.com.cn/problem/P3925)就挺好的。
还有[P7735 [NOI2021] 轻重边](https://www.luogu.com.cn/problem/P7735) 这样的好题。
个人感觉,这种题,才是重剖存在的意义吧,而不是单纯把操作搬上树的恶心~~傻逼~~题。
虽然说重剖真的不是很常考了,但是既然都已经看到这篇文章,学到这了,那就练完吧!
------------
# 4. **长链剖分**
讲长剖前先丢上[长剖模板题](https://www.luogu.com.cn/problem/P5903)吧。
对于树链剖分而言,长剖和树剖可以说是比较相近。(隔壁那个实剖不一样一点。)
所以我们考虑使用与重剖相近的方法讲长剖。
------------
## 4.1. 定义
- 定义 **长儿子** 表示其子节点中子树深度最深的子节点。如果有多个子树深度最深的子结点,取其一。如果没有子节点,就无长儿子。
- 从这个结点到长子节点的边为 **实边**。
- 到其他子节点的边为 **虚边**。
- 若干条首尾衔接的实边构成 **长链**。
- 把落单的结点也当作长链,那么整棵树就被剖分成若干条长链。
- 对于一条长链
- 定义链底为这条链深度最大的节点
- 定义链顶为这条链深度最小的节点
附图(真的不想手画所以拿了一张树剖姐姐的):
![](https://cdn.luogu.com.cn/upload/pic/73806.png)
其中绿色边为实边,蓝色边为虚边。
------------
## 4.2. 实现
根据上面的定义,我们可以得到长剖的预处理 dfs 代码。
**P.S.:注意链顶链底的定义**
```cpp
void dfs1(int x,int f)
{
dep[x]=dep[f]+1;fa[x][0]=f;h[x]=-1;
for(int i=1;i<20;i++) fa[x][i]=fa[fa[x][i-1]][i-1];
for(int i=0;i<g[x].size();i++)
{
dfs1(g[x][i],x);
if(h[g[x][i]]>h[x]) son[x]=g[x][i],h[x]=h[g[x][i]];
}
h[x]++;if(!h[x]) h[x]=1;
}
void dfs2(int x,int top)
{
dn[top].push_back(x);tp[x]=top;
if(g[x].empty()) return;
dfs2(son[x],top);
for(int i=0;i<g[x].size();i++)
if(g[x][i]!=son[x])
{
up[g[x][i]].push_back(g[x][i]);
for(int j=1;j<=h[g[x][i]];j++) up[g[x][i]].push_back(fa[up[g[x][i]].back()][0]);
dfs2(g[x][i],g[x][i]);
}
}
```
------------
## 4.3. 应用(板子)
例题:[P5903 【模板】树上 K 级祖先](https://www.luogu.com.cn/problem/P5903)。
倍增出每个节点向上 $2^p$ 个节点的父亲,对于每次查询的 $k$,我们先把 $x$ 跳到 $k$ 第一个二进制位代表的数的位置,这样就可以把剩下的距离缩减到 $\lfloor \frac{k}{2} \rfloor$ 以下,这样,当前这个节点的链长肯定比剩下要跳的长度更大了,所以直接向上跳即可。
更清楚的描述见图([来源](https://www.luogu.com.cn/blog/b6e0/solution-P5903)):
![](https://cdn.luogu.com.cn/upload/image_hosting/2vio1tjy.png)
附上 [std](https://www.luogu.com.cn/paste/qxl4lxh8)。
**P.S.:这里注意不要全开 long long,但记得给快读快输里开 long long!!!(不信看我提交记录)**
------------
## 4.4. 应用(长剖优化dp)
例题:[Dominant Indices](https://www.luogu.com.cn/problem/CF1009F)。
题意已经很清楚了,这里不再过多阐述。
首先考虑朴素 dp:
状态设计:$dp_{u,dep}$ 表示 u 的子树中与 u 距离为 dep 的点的个数。
转移方程如下:
$$dp_{u,dep}=\sum_{v \in son_u} dp_{v,dep-1}$$
显然这个时空复杂度是 $O(n^2)$ 的。
再看眼数据范围,好,真就再看一眼就会爆炸:$n\le 10^6$。
所以来考虑优化吧。
结合一下我们的标题,考虑下用长剖优化。
我们会发现,长剖中的长链有着非常美妙的性质,就是长儿子肯定是当前子树最长的那条链上的。(这不是废话吗,但是就是这个美妙性质。)
对于一个节点 $u$,我们先对它的长儿子做 dp,让长儿子把 dp 出来的东西直接存到 $dp_u$ 里面去(当然观察 dp 式可以发现这边需要错一位)。
其他的儿子直接暴力合并上去就好了。
显然每条长链最多被合并一次,所以就得到了美妙的 $O(n)$ 复杂度。
再采取指针写法弄下 dp 数组,就可以得到时空复杂度 $O(n)$ 的美妙 dp 了。
附代码[ std](https://www.luogu.com.cn/paste/yqo6jmlm)。
### **练习题**
[P3899 [湖南集训] 更为厉害](https://www.luogu.com.cn/problem/P3899)
------------
## 4.5. 后记(个人关于长剖看法)
个人目前没有发现长剖除了这两种用途外的别的用途,感觉强度不是很高,有点太偏了。用途不是很广。
而且上面举到的长剖优化 dp 的题都可以用线段树合并做,所以这个方法纯当掌握好了。(虽然这两题中的长剖的复杂度和码量都可以吊打线段树合并。)
实际用上的概率应该很少,但是有备无患吗。
------------
# 5. **平衡树部分(Splay)**
大家可能会好奇为什么突然在树剖中插入一个平衡树的章节。
别问,问就是后面的实链剖分,也就是 LCT 他里面要用(其实可以写 FHQ 的,但是有点麻烦)。
所以不学也得学 Spaly 喽!(Splay and play S/cf)
(这里的平衡指的是非严格平衡捏。)
------------
## 5.1. 二叉搜索树
在学习平衡树前,我们要先学习一个东西叫做二叉搜索树。
先给出定义。
### 5.1.1 定义
1. 空树是二叉搜索树。
2. 若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。
3. 若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。
4. 二叉搜索树的左右子树均为二叉搜索树。
由此我们可以得到二叉搜索树的性质,就是:
$$w_{ls}<w_{rt}<w_{rs}$$
$ls,rt,rs$ 的意思分别是左儿子,根,右儿子。
$w$ 数组是指点权。
所以由此我们可以得到第二个性质,二叉搜索树的中序遍历可以得到一个权值单调上升的序列。
### 5.1.2 应用
结合下二叉搜索树的名字,肯定是为了搜索而生的了。
根据前面所提到的他所拥有的性质,我们可以造出一颗二叉搜索树可以回答以下询问:
1. 询问权值 $k$ 在序列中的的排名
2. 询问排名为 $k$ 的权值的大小
考虑如何回答这两个询问:
- 对于第一种询问,我们考虑将权值与 $w_{rt}$ 的权值对比
- 如果 $k<w_{rt}$,那么这个值就出现在 $rt$ 的左子树中,向左子树走。
- 如果 $k=w_{rt}$,那么这个值就出现在这个点上,考虑这个点的排名即可。
- 如果 $k>w_{rt}$,那么这个值就出现在他的右子树中,向右子树走。
- 相似的,对第二种询问考虑,还是考虑将权值与 $cnt_{rt}$ 对比,考虑这个点的排名
- 如果 $k\le size_{ls}$ 则出现左子树中
- 如果 $k>size_{ls}\&\&k\le cnt_{rt}+size_{ls}$ 就出现在这个点上,返回这个点的权值。
- 如果 $k>cnt_{rt}+size_{ls}$ 就说明出现在右子树上,向右子树走。
以此考虑,便可完成上述的两种询问的查询。
接着,我们来考虑如何构造出一颗二叉搜索树。
考虑和上面相似的流程。
- 如果 $k<w_{rt}$ 则往左走。
- 如果 $k=w_{rt}$ 则给 $size_{rt}+1$
- 如果 $k>w_{rt}$ 则往右走。
直到结束或者到达一个空节点时,把这个信息录进去。
除此之外他还能支持删除操作等。
### 5.1.3. 时间复杂度
知道了如何构造和使用二叉搜索树后,我们来考虑下二叉搜索树的时间复杂度。
显然,这个搜索次数与树高有关,也就是 $O(h)$。
最优情况应是树高最低时,也就是满二叉树时,复杂度为 $O(\log{n})$。
最劣情况则考虑下上面所提到的构造过程,只要输入是一个单调上升/下降的序列,就可以让二叉搜索树退化为一条链。
那个时候复杂度就变成了 $O(n)$。
所以二叉搜索树的搜索时间复杂度并不稳定。
这个时候我们就要考虑优化了。
------------
## 5.2. 平衡树
上面提到了二叉搜索树的搜索时间复杂度并不稳定,所以实际使用时我们肯定不能用这么不稳定的数据结构了。
那么就得考虑下优化了,优化是什么呢?
优化后就成为了平衡树。
了解到之前搜索复杂度不稳定的原因是因为树高不稳定,随时都有可能退化为链。
所以我们考虑一种可以控制树高的二叉搜索树,那他就成了平衡树。
考虑到这篇是树剖博客,所以我们这里指讲解和树剖有关的 Splay。
### 5.2.1. 定义
- $\mathrm{rt}$ 代表根节点
- $\mathrm{rt}$ 代表节点个数
- $\mathrm{fa_i}$ 代表 $i$ 节点的父亲节点,特殊的,根节点没有父亲。
- $\mathrm{son_{i,0/1}}/\mathrm{ch_{i,0/1}}$ 代表 $i$ 节点的左右儿子编号。
- $\mathrm{si_i}$ 代表 $i$ 节点的子树大小,特殊的,叶子节点的子树大小为 $1$。
- $\mathrm{w_i}$ 代表 $i$ 节点的权值大小。
- $\mathrm{s_i}$ 代表 $i$ 节点上这种权值的个数。
- 每次操作后都要把操作的节点转到根上。
### 5.2.2. 维护平衡
二叉搜索树之所以会被卡,就是因为不平衡导致的。
所以我们考虑如何维护 Splay 的平衡性。
首先根据上面定义的最后一条,我们得到 Splay 是通过旋转来维护整体平衡性的。
那么如何旋转呢?
旋转有两种,分别为左旋和右旋,给张示意图吧:
![](https://oi-wiki.org/ds/images/splay-rotate.svg)
从这张图里面我们可以看出,右旋就是把左儿子转上去,把自己转到右儿子那。
左旋则恰恰相反。
那么,对于节点的旋转,便成了这样:
- 如果他是左儿子,就右旋。
- 如果他是右儿子,就左旋。
学会了旋转的方法,我们再来考虑下上面的最后一条:
- 每次操作后都要把操作的节点转到根上。
但这个时候又会有一个问题,我们的节点转一次只能和上一个节点换位置啊,怎么转到根呢?
难不成暴力转吗?
答案是肯定的。
- 我们只需要直接暴力给目前这个节点一层一层转上去直到转到根就行了。
那这样的话 Splay 就写完了,那就可以去写题喽。
等等,再考虑下之前卡掉二叉搜索树的单调数据,用现在的 Splay 可以保证树高为 $\log{n}$ 吗?
显然不行。
如果要深刻理解这种情况为什么会被卡,我们要先了解 Splay 能达到 $\log{n}$ 树高的原因。
- 每次操作后都要把操作的节点转到根上。
这个操作的意义在哪里呢?
其实仔细观察我们会发现,在他路径上的点的深度,至少有 $-1$。
如图所示:
![](https://oi-wiki.org/ds/images/splay-rotate5.svg)
![](https://oi-wiki.org/ds/images/splay-rotate6.svg)
也就是保证了树高肯定会因为操作而趋向于 $\log{n}$。
再考虑下这种经典的错误,也就是单旋 Splay 被卡掉的原因,结合一张图来理解一下:
![](https://oi-wiki.org/ds/images/splay-rotate3.svg)
考虑这种情况,当爷父子三点共线的时候,我们转了半天只是给 $x$ 转上去了,没有改变树高,所以导致最后会被卡掉。
那如何改变这种情况呢?
先转下中间的父节点 $y$,再转下要操作的节点 $x$ 即可。
像这种判断三点共线并且考虑转两次的 Splay 被称为 双旋 Splay。
而前面那个会被卡的被称为 单旋 Splay。
那么 Splay 操作代码即为:
```cpp
int bh(int i)
{
if(t[t[i].f].son[0]==i) return 0;
else return 1;
}
void rotate(int x)
{
int f=t[x].f,gfa=t[f].f;
int x_=bh(x),fa=bh(f);
t[t[x].son[!x_]].f=f;
t[f].son[x_]=t[x].son[!x_];
t[x].son[!x_]=f;t[gfa].son[fa]=x;
t[f].f=x;t[x].f=gfa;
update(f);update(x);
}
void splay(int x,int y)
{
int f,gfa;
if(x==y) return;
while(t[x].f!=y)
{
f=t[x].f;gfa=t[f].f;
if(gfa!=y)
if(bh(f)==bh(x)) rotate(f);
else rotate(x);
rotate(x);
}
if(!y) rt=x;
}
```
### 5.2.3. 操作实现
首先给出板子题:[P3369 【模板】普通平衡树](https://www.luogu.com.cn/problem/P3369)
在这题里面,我们要实现 $6$ 种操作,分别是:
1. 插入一个数 $x$。
2. 删除一个数 $x$(若有多个相同的数,应只删除一个)。
3. 定义排名为比当前数小的数的个数 $+1$。查询 $x$ 的排名。
4. 查询数据结构中排名为 $x$ 的数。
5. 求 $x$ 的前驱(前驱定义为小于 $x$,且最大的数)。
6. 求 $x$ 的后继(后继定义为大于 $x$,且最小的数)。
了解了刚刚有关 Splay 最重要的操作 Splay 之后,这些询问只需要按照正常的二叉搜索树的形式来回答就行了。
**但是注意每一次操作完后 Splay 这个节点在根上。**
附上[ std](https://www.luogu.com.cn/paste/huugwblx)。
#### **练习题**
[P3391 【模板】文艺平衡树](https://www.luogu.com.cn/problem/P3391)。
------------
# 6. 实链剖分(LCT)
讲了这么久终于来到最折磨的一块了,打开这块之前请确保您已经深刻理解 Splay 并且不会写挂。
------------
## 6.1. 定义
首先我们回忆一下前面的树链剖分,他们都采取了一种方式将树剖分为了链,那么对于 LCT,我们也要找到一种剖分的方式。
怎么做肯定取决于他的用途,所以在开头,我列举些 LCT 能维护的东西:
- 查询、修改链上的信息(最值,总和等)
- 换根
- 动态连边、删边
- 合并两棵树、分离一棵树
- 动态维护连通性
- 更多意想不到的操作
具体的大部分操作可以参考下 LCT 经典超强板子综合题:[P5649 Sone1](https://www.luogu.com.cn/problem/P5649)。
结合上面如此强的灵活性,我们得到了**机具灵活性的实链剖分**:
- 对于一个点连向它所有儿子的边,我们自己选择一条边进行剖分。
- 我们称被选择的边为**实边**,其他边则为**虚边**。
- 对于**实边**,我们称它所连接的儿子为**实儿子**。
- 对于一条由**实边**组成的链,我们同样称之为**实链**。
- 请记住我们选择实链剖分的最重要的原因:它是我们选择的,**灵活且可变**。
- 正是它的这种灵活可变性,我们采用 **Splay** 来维护这些**实链**。
在实链剖分之后,我们就得到了 LCT 的重要部分:**辅助树**。
先给出辅助树的一些定义:
1. 辅助树由多棵 Splay 组成,每棵 Splay 维护原树中的一条路径,且中序遍历这棵 Splay 得到的点序列,从前到后对应原树「从上到下」的一条路径。
2. 原树每个节点与辅助树的 Splay 节点**一一对应**。
3. 辅助树的各棵 Splay 之间并不是独立的。每棵 Splay 的根节点的父亲节点本应是空,但在 LCT 中每棵 Splay 的根节点的父亲节点指向原树中 这条链 的父亲节点(即链最顶端的点的父亲节点)。**这类父亲链接与通常 Splay 的父亲链接区别在于儿子认父亲,而父亲不认儿子**,对应原树的一条 **虚边**。因此,每个连通块恰好有一个点的父亲节点为空。
4. 由于辅助树的以上性质,我们维护任何操作都不需要维护原树,辅助树可以在任何情况下拿出一个唯一的原树,我们只需要维护辅助树即可。
因为真的懒的画图,但是这个部分没图是真的很抽象,所以我只好拿 OI Wiki 上的了/kk。
若原树为:
![](https://oi-wiki.org/ds/images/lct-atree-1.svg)
其中加粗为实边,虚线为虚边。
则辅助树可能为:
![](https://oi-wiki.org/ds/images/lct-atree-2.svg)
考虑原树和辅助树的关系,可以得出以下几点:
- 原树中的实链 : 在辅助树中节点都在一棵 Splay 中。
- 原树中的虚链 : 在辅助树中,子节点所在 Splay 的 Father 指向父节点,但是父节点的两个儿子都不指向子节点。
- 原树的 Father 指向不等于辅助树的 Father 指向(这一点一定要注意,自己的父亲数组不要搞混了)。
**P.S.: Splay 其实是 Tarjan 为了 LCT 所创造的,所以 Splay 对于 LCT 的需求可以完全满足。**
下面给出我代码中的几个定义:
- $\mathrm{son_{i,0/1}}$ 分别表示左儿子和右儿子
- $\mathrm{st_i}$ 表示栈中的元素
- $\mathrm{w_i}$ 表示这个点的点权
- $\mathrm{s_i}$ 表示这个点的答案
- $\mathrm{f_i}$ 表示这个点的父亲
- $\mathrm{r_i}$ 表示翻转标记
- $\mathrm{ls}$ 表示 $\mathrm{son_{i,0}}$
- $\mathrm{rs}$ 表示 $\mathrm{son_{i,1}}$
------------
## 6.2. 实现
有了定义我们就可以开始实现一颗 LCT 了。
LCT 主要由几个核心操作组成,所以我选择将每个核心操作拆开讲。
首先是有关于 Splay 的部分:
这个部分我觉得其实没什么好讲的(前面已经讲过了捏,忘了的回去看!),看下代码应该就能找到不同并注意到了:
```cpp
void rotate(int x)
{
int y=f[x],z=f[y],k=(son[y][1]==x),w=son[x][!k];
if(ntrt(y)) son[z][son[z][1]==y]=x;son[x][!k]=y;son[y][k]=w;
if(w) f[w]=y;f[y]=x;f[x]=z;
pushup(y);
}
void Splay(int x)
{
int y=x,tot=0;st[++tot]=y;
while(ntrt(y)) st[++tot]=y=f[y];
while(tot) pushdown(st[tot--]);
while(ntrt(x))
{
y=f[x];int z=f[y];
if(ntrt(y)) rotate((son[y][0]==x)^(son[z][0]==y)?x:y);
rotate(x);
}
pushup(x);
}
```
这里,我们发现有个函数叫做 ntrt,他的全称应该是 not root,因为属于 LCT 部分所以代码在下面了。他的意思是是否为当前 Splay 的根。
接着就是 LCT 部分了。
首先讲下比较基础的几个函数:
ntrt:他的意思同上,代码也容易实现,考虑 LCT 中的 Splay 他的根节点是儿子认父亲,父亲不认儿子,使用这个特性即可:
```cpp
bool ntrt(int x){return son[f[x]][1]==x||son[f[x]][0]==x;}
```
pushup:这个就和普通 Splay 中的差不多,用于得到这个点的答案值,不同的题目写法不同,这里给出 LCT 模板中的写法:
```cpp
void pushup(int x){s[x]=s[ls]^s[rs]^w[x];}
```
然后是有关懒标记与下传的 pushson 与 pushdown:
LCT 所有的懒标记是翻转标记(为什么要用到翻转后面有解释,这边先讲讲怎么处理这个翻转),其实很好处理,每层下传即可,详情见代码:
```cpp
void pushson(int x){swap(ls,rs),r[x]^=1;}
void pushdown(int x)
{
if(r[x])
{
if(ls) pushson(ls);
if(rs) pushson(rs);
r[x]=0;
}
}
```
------------
接着是核心操作 **Access**:
他要实现的就是把从根到 $x$ 的所有点放在一条实链里,使根到 $x$ 成为一条实路径,并且在同一棵 Splay 里。**只有此操作是必须实现的,其他操作视题目而实现。**
先放上代码,再结合 OI Wiki 中的图讲下:
```cpp
void access(int x){for(int y=0;x;x=f[y=x]) Splay(x),rs=y,pushup(x);}
```
考虑这样一颗原树:
![](https://oi-wiki.org/ds/images/lct-access-1.svg)
他的辅助树可能长成这样:
![](https://oi-wiki.org/ds/images/lct-access-2.svg)
现在我们要 $Access(N)$。
实现的方法就是一步步转上去。
首先把 $N$ 转为当前 Splay 的根,再根据辅助树的性质,要将 $N,O$ 变为虚边。
由于父不认子,所以我们只要在转完后把 $N$ 的右儿子设为空即可。
![](https://oi-wiki.org/ds/images/lct-access-4.svg)
接着继续旋转,把 $N$ 向上转一层,就得到了这张图:
![](https://oi-wiki.org/ds/images/lct-access-5.svg)
重复上述操作,最后可以得到:
![](https://oi-wiki.org/ds/images/lct-access-6.svg)
再到:
![](https://oi-wiki.org/ds/images/lct-access-7.svg)
这样 $A,N$ 的路径就全为实边了,Access 操作也完成了。
其实考虑层层的转还是比较好理解的。建议自己画图手模下再理解下。
------------
接着是另一个关键的操作 makeroot:将一个点变为当前树的根。
照例,先上代码:
```cpp
void makert(int x){access(x);Splay(x);pushson(x);}
```
我们会发现,makeroot 部分其实很简单:
- 首先将 $x$ 到当前根拉到一颗 Splay 上
- 然后将 $x$ 旋转到树根上
- 最后我们考虑把树当为有向图,会发现,换根其实就是将 $x$ 到根的路径反向(这里建议好好思考下为什么)
所以这样就轻松完成了 makeroot 操作。
------------
最后是 findrt,split,link,cut 操作,有了前面的 Access,Splay,makeroot,其实都非常好实现了:
```cpp
int findrt(int x)
{
access(x);Splay(x);
while(ls) pushdown(x),x=ls;
Splay(x);return x;
}
void split(int x,int y){makert(x);access(y);Splay(y);}
void link(int x,int y){makert(x);if(findrt(y)!=x)f[x]=y;}
void cut(int x,int y)
{
makert(x);
if(findrt(y)==x&&f[y]==x&&!son[y][0]) f[y]=son[x][1]=0,pushup(x);
}
```
P.S.:这里给出的 cut 操作判断了边是否存在,同理,link 操作判断了边是否存在。
个人感觉想要真正理解 LCT,就把他当做暴力来理解(数据结构都这样吧)。
比如:我们要查询 $x$ 到 $y$ 的点权和,就把 $x$ 先弄成根,然后给 $y$ 和 $x$ 弄到一颗 Splay 中,然后直接给中间那段路径拿出来,完美解决。
这些代码建议好好消化理解,LCT 的代码是很短,但是挺难理解的。
------------
## 6.3. 应用(板子)
前面的实现讲了之后,这个 [LCT 板子](https://www.luogu.com.cn/problem/P3690)是真板子了,这边直接奉上[ std](https://www.luogu.com.cn/paste/twvxdays)。
这里都是一些最基础的维护点权/判断连通性捏/kk。
### **下面给点基础练习题**
[P4312 [COI2009] OTOCI](https://www.luogu.com.cn/problem/P4312)
[OTOCI - OTOCI](https://www.luogu.com.cn/problem/SP4155)
前面是双倍经验捏。
[P1505 [国家集训队] 旅游](https://www.luogu.com.cn/problem/P1505)
这题堆码量即可。
[P4847 银河英雄传说V2](https://www.luogu.com.cn/problem/P4847)
这题好像是这些题里面最难的,还要再写个 $query$。
[P3950 部落冲突](https://www.luogu.com.cn/problem/P3950)
[P2147 [SDOI2008] 洞穴勘测](https://www.luogu.com.cn/paste/migf2vzo)
这个两个题比板子还简单。
------------
## 6.4. 应用(技巧)
看到这里说明你已经深刻理解(~~熟读并背诵~~) LCT 的板子了。
本章节将会讲一些重剖中比较模板的套路或是好题,方式是结合例题分析。
- **Warning:从此段开始难度直线上升,至少是紫。**
- **P.S.:如果找不到锅了可以对下我的[出锅合集](https://www.luogu.com.cn/paste/ahc7lu2i)。**
题目讲解排序按照难度不一定单调递增。
那就开始吧:
------------
### **6.4.0.[P3203 [HNOI2010] 弹飞绵羊](https://www.luogu.com.cn/problem/P3203) 轻量化 LCT**
这道题算是这个版块中最为简单的了。
首先我们要考虑题意的转化。
$x$ 点的弹力装置可以把绵羊弹到 $x+k_i$ 个装置。
但是如果这个值大于 $n$ 则弹飞。
所以我们考虑如果 $x+k_i\le n$,就将 $x,x+k_i$ 连一条边。
否则不连边。
这里我们并不需要超级源点 $n+1$ 的做法,因为这题可以轻量化 LCT。
解释下这题为什么可以轻量化,重点在于:
> - 题目保证造出来的是一颗树而不是森林。
所以 link 操作可以砍掉,直接连边就行。
cut 操作也可以砍掉,我们确定其父亲的位置,只要 $access(x),splay(x)$ 后,$x$ 的父亲一定在 $x$ 的左子树中,直接双向断开连接。
split 也可以砍掉,我们先 $access(x),splay(x)$,然后直接返回 $x$ 的 $size$ 即可。
所以就轻量化完成了。
附上[ std](https://www.luogu.com.cn/paste/6xbkzgml)。
------------
### **6.1.1./6.1.2.[P4172 [WC2006] 水管局长](https://www.luogu.com.cn/problem/P4172) LCT 维护边权+MST+倒序操作**
这道题挺好玩的,试了下,一遍 pass 还是很爽的。
题意简述:
> 给定一张动态图:
> - 询问 $u\rightarrow v$ 可能路径的最大边权的最小值。
> - 删除一条边。
首先这题肯定是想到 LCT 维护了,毕竟动态图。
那么考虑下删边操作就直接删,只是询问操作比较难处理。
而且给定的是动态图,LCT 解决的却是动态树问题。
再看下题意中 **可能路径的最大边权的最小值**,这句想到什么?
对了,在 MST 上,这些值都是最小的。
那么题意就变成了维护动态 MST。
还是有点难度,因为我们有着删边操作,比较难以处理。
所以考虑倒序操作,把删边变成加边,初始化时把操作不会删掉的边全部加上。
那么久变成了一个只会加边操作的动态 MST。
考虑原本已经有一颗 MST,那么我们加边的时候,一定会产生一个环。
考虑找到这个环在加上这条路径前的最长路径的长度。
删掉那条最长的边,再加上这条边即可。(注意判断下这条加上的边是否更优。)
那这题貌似就华丽的结束了?
不不不,还有一点还没考虑呢,那就是:
> - LCT 如何维护边权?
因为 LCT 没有固定的父子关系,所以不能考虑使用重剖那样的把边权下放到儿子的边转点技巧。
那这个时候我们该怎么办呢?
直接 **拆点** 呗。
> 考虑把原本 $u\rightarrow v$ 边权为 $w$ 的路径化为,$u\rightarrow x\rightarrow v$,其中 $x$ 的点权为 $w$。
这下,题目就完美解决了。
**P.S.:注意这题询问的删边操作如何寻找,我用的 map 套 pair。**
附上 [std](https://www.luogu.com.cn/paste/aq886otj)。
##### **练习题**
[P2387 [NOI2014] 魔法森林](https://www.luogu.com.cn/problem/P2387)
维护 MST 的时候记得边权有两个即可。
[P4234 最小差值生成树](https://www.luogu.com.cn/problem/P4234)
维护边权与 MST 变体的 LCT 捏。
[Best Edge Weight](https://www.luogu.com.cn/problem/CF827D)
先求 MST,然后查询链上最大值/最小值。(其实关系好像不大?)
------------
### **6.1.3.[P4319 变化的道路](https://www.luogu.com.cn/problem/P4319) 线段树分治+动态 MST**
首先看到这题我们会发现是 MST 的板子,所以我们优先考虑上个 『6.1.1.』 的动态 MST。
然后我们发现了这个美妙~~傻逼~~题目的边有存在时间。
> - 题目说了每条边只在一个时间段中出现。
所以考虑套一个线段树分治,用线段树维护时间区间上的答案。
每次在符合的时间段里,就把边连上,做完后就断开即可。
##### **练习题**
[P3206 [HNOI2010] 城市建设](https://www.luogu.com.cn/problem/P3206)
貌似没有什么区别?
------------
### **6.1.4.[P2542 [AHOI2005] 航线规划](https://www.luogu.com.cn/problem/P2542) LCT 维护边双联通分量**
首先发现是删边操作,发现不太好处理,所以考虑和前面一样离线倒序加边。
考虑先缩个点,把每个点双缩成一个点。
那么查询的时候就是链长度 $-1$(为什么是这样请读者自行推敲一二。)
然后好像就做完了?
好像的确是的。
------------
### **6.1.5.[P4219 [BJOI2014] 大融合](https://www.luogu.com.cn/problem/P4219) LCT 维护子树信息**
知周所众的是,LCT 并不擅长维护子树信息。
但是这道题需要维护动态树的子树信息。
那怎么办呢?
考虑下 LCT 为什么不能维护子树信息。
这一点是因为 LCT 中的父子关系并不确定。
也就是对于一个节点 $x$,对于他的实子树,肯定都是统计了的。
但是他的虚子树并没有统计进去。
所以我们考虑维护一个 $si2$ 数组即可。
这里给出定义:
> - $\mathrm{si2_{x}}$ 代表 $x$ 的虚子树的信息。
比如这道题,就是权值和。
那么在 $pushup$ 函数中,我们就要额外更新下这个数组。
除此之外,还有 $access$ 中我们修改了一个实儿子为虚儿子,所以也要修改下。
最后还有 $link$ 操作的额外增加。
这样就完美解决了。
------------
# 7. 资料来源鸣谢
部分图片及资料参考 [OI Wiki 树链剖分](https://oi-wiki.org/graph/hld/),[OI Wiki Splay 树](https://oi-wiki.org/ds/splay/) 以及 [OI Wiki Link Cut Tree](https://oi-wiki.org/ds/lct/)。
部分解法参考 lzyqwq 的题解。
树剖换根部分复制了[神犇 Farkas_W 的换根树剖笔记](https://www.luogu.com.cn/blog/Farkas/guan-yu-shu-lian-pou-fen-huan-gen-cao-zuo-bi-ji)。
部分长剖解法及图片参考 Ynoi 的[博客](https://www.luogu.com.cn/blog/Ynoi/zhang-lian-pou-fen-xue-xi-bi-ji)。
长剖板子中的图来自于 b6e0_ 的[博客](https://www.luogu.com.cn/blog/b6e0/solution-P5903)。