从 LCA 谈树上倍增与重链剖分
Zskioaert1106
·
·
算法·理论
最近公共祖先
最近公共祖先(Lowest Common Ancestor,LCA),指的是在一颗有根树中距两个结点最近的公共祖先。比如说 t=\operatorname{LCA}(x,y),当且仅当 t 在所有 x 和 y 的公共祖先中,且 t 是深度最大(离根最远)的一个。
模板:P3379 【模板】最近公共祖先(LCA)
朴素求法
先考虑一下 \operatorname{LCA}(x,y) 的条件:
那如何朴素求出 LCA 呢?可以重复使 x \leftarrow f\!a_{x},y \leftarrow f\!a_{y},直到 f\!a_{x}=f\!a_{y} 即求出了。
因为 deep_{\operatorname{LCA}(x,y)}=deep_{\operatorname{LCA}(x,y)},所以可以得出我们要先将 x 和 y 提到同一深度。
代码实现:
int lca(int x,int y){
while(deep[x]>deep[y])x=fa[x];
while(deep[y]>deep[x])y=fa[y];
while(x!=y)x=fa[x],y=fa[y];
return x;
}
容易构造出数据(如一条链,x,y 分别是链头和链尾),使得单次求 LCA 的复杂度达到 O(n)。
期望得分:TLE 100pts。
树上倍增
我们知道倍增就是存储关于 2 的幂的状态,所以单结点的时空都是 \log,从而实现 O(n \log n) 的可接受预处理范围。
对于 LCA,我们需要的信息是结点的祖先。所以可以建立二维数组,一维长度 n,一维长度 \log_2 n。定义 f_{i,j} 表示结点 i 的第 2^j 个祖先,则可以用 dp:
-
边界:f_{i,0}=f\!a_{i}(i 的第 2^0=1 个祖先)
-
状态转移:我们按 j 从小到大的顺序处理,则 f_{i,j}=f_{f_{i,j-1},j-1}。
还是将过程分为提到同层、跳到 LCA 两部分,对于第一部分,我们将 x 和 y 的深度较大者提到深度与较小者相同:
如何用已有的 2 的幂的信息求出应该怎么跳?
任何正整数都可以表现为二进制数即一些 2^i 之和(其中 i 各不相同),因此我们可以将 i 从 \log_2 n 开始,往小遍历到 0——如果 deep_{f_{x,i}}\geqslant deep_{y},那么就可以将 x 向上提 2^i 个,即 x\leftarrow f_{x,i}。
由于 deep_x 与 deep_y 的深度差可以表示为二进制数,所以最终一定能提到指定位置。
if(deep[x]<deep[y])swap(x,y);
for(int i=l;i>=0;i--)//l 为 log2(n)
if(deep[f[x][i]]>=deep[y])x=f[x][i];
接下来就要同时提 x 和 y,可以继续照着上面的方法提——如果 f_{x,i}\neq f_{y,i},则将两者同时上提 2^i。但是一定要注意:
-
-
最终求出来的结果是 f_{x,0}=f_{y,0} 的情况,LCA 是 f_{x,0}(或 f_{y,0})!
-
在开始同时提之前一定要特判如果 x=y,直接返回 x。(不然容易跑出根节点了,很显然不符合上面说的性质)
```cpp
int n,m,s,l,f[N][30],deep[N];
vector<int>e[N];
void dfs(int x){
deep[x]=deep[f[x][0]]+1;
for(int j=1;j<=l;j++)
f[x][j]=f[f[x][j-1]][j-1];
for(int u:e[x]){
if(u==f[x][0])continue;
f[u][0]=x;
dfs(u);
}
}
int lca(int x,int y){
if(deep[x]<deep[y])swap(x,y);
for(int i=l;i>=0;i--)
if(deep[f[x][i]]>=deep[y])x=f[x][i];
if(x==y)return x;
for(int i=l;i>=0;i--)
if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
```
预处理的时间复杂度是 $O(n \log n)$,每次求 LCA 的时间复杂度是 $O(\log n)$ 的。
期望得分:[AC 100pts](https://www.luogu.com.cn/record/205442883)。
倍增是严格的 $\log$,不带一丝松放。那~~主播主播~~有没有常数更小的算法推荐一下呢?
### 重链剖分
~~有点兄弟,有的。~~
树链剖分是什么?
说白了就是把树按链剖开,分割、分配到序列上。它将树反映为序列,使得很多序列工具能在树上应用,如线段树。
良好的剖分方法能使这些工具加个 $\log$ 就用在树上,比如**重链剖分**。
你可以理解为一种 dfs 方法得到的 dfs 序,具体地,对于每个结点,定义它所有孩子里子树大小最大的为它的“重孩子”(有多个取一个,其它的为轻孩子)。dfs 时总遵循第一个搜重孩子的原则。
如图所示,这是对一棵树进行的重链剖分,其中连向重孩子的边(黑边)组成的链被称为重链,而连向轻孩子的边(红边)被称为轻边。

以最上面的结点为根,则图中序号是一种合法的重剖序。不难发现,每个重链上的所有结点在生成的序列中仍然是挨在一起的。这意味着什么?我们可以对它们进行一些整体操作!
更重要一点的是,可以证明在任意一个结点向根的路径上,重链和轻边条数,均不超过 $\log n$。
考虑一颗满二叉树,此时轻孩子的子树大小和重孩子的是一样的,而这时树高是 $\log n$,且有叶子到根的路径上全是轻边。而当树改变时,由于重剖会优先选择子树大的,所以轻边的数量只会更少。
然后还有一条结论,就是结点到根的路径上,重链的条数不会超过轻边条数加 $1$。试想当每两条重链有一个轻边隔开,如果少一条轻边则就会有两条重链连到一起。
我们定义 $top_x$ 代表结点 $x$ 所在重链的链头(不在重链上则为 $x$,特别地,根结点的链头初始要设为本身),每次都跳到链头,跑的次数不就可以 $n \rightarrow \log$ 了吗?
通常我们对树进行重链剖分可以用两遍 dfs。第一遍可以求出每个结点的深度、子树大小以及重孩子,第二遍则可以先搜重孩子形成重链,记录每个点的链头。这时记录下来的 dfs 序便是重剖序,求 LCA 用不着,但是树上转序列操作要用。
```cpp
vector<int>a;
void dfs1(int x){
deep[x]=deep[fa[x]]+1;
siz[x]=1;
for(int u:e[x]){
if(u==fa[x])continue;
fa[u]=x;
dfs1(u);
siz[x]+=siz[u];
if(siz[u]>siz[wc[x]])wc[x]=u;//重孩子
}
}
void dfs2(int x){
a.push_back(x);//记录 dfs 序
if(!wc[x])return;//说明没有子结点了
top[wc[x]]=top[x];
dfs2(wc[x]);
for(int u:e[x]){
if(u==fa[x]||u==wc[x])continue;
top[u]=u;
dfs2(u);
}
}
dfs1(s);
top[s]=s;
top2(s);
```
聪明的同学已经能想到了求 $\operatorname{LCA}(x,y)$ 经过的路径(即 $x$ 到 $y$)可以转化为从 $x$ 到 $\operatorname{LCA}(x,y)$ 和从 $y$ 到 $\operatorname{LCA}(x,y)$ 两条路径。所以重剖求 LCA 的时间复杂度也是 $\log$ 的。而且这只是上界,实际中随机出现的树很难顶到 $\log$ 的级别。
如果你恰巧会线段树,你可以去尝试一下板子题 [P3384 【模板】重链剖分/树链剖分](https://www.luogu.com.cn/problem/P3384)。
什么你问我怎么处理子树,你没发现 dfs 序下的子树内所有结点的编号是连续的吗?
重链剖分求 LCA 的方法如下:
1. 如果 $top_x=top_y$,则 $x$ 和 $y$ 在同一重链上。$\operatorname{LCA}(x,y)$ 即为深度较小者。
2. 选择 $x$ 和 $y$ 中链头深度较大的(此处以 $x$ 为例),使 $x \leftarrow f\!a_{top_x}$;
3. 重复操作 1、2。
非常的简洁啊,感觉不太需要代码了。就是记住一点,是链头的父节点不是链头,不然就会死循环。
期望得分:[AC 100pts](https://www.luogu.com.cn/record/205222498)。
### LCA 的拓展题目
- [P8805 [蓝桥杯 2022 国 B] 机房](https://www.luogu.com.cn/problem/P8805)
简单的树上前缀和,求出 LCA 后即可得到路径上的和。注意别重或漏掉 LCA 本身。
- [P3398 仓鼠找 sugar](https://www.luogu.com.cn/problem/P3398)
求两段路径有没有重合,可以转化为有没有一条路径两个端点的 LCA 在另一条路径上。
- [P4281 [AHOI2008] 紧急集合 / 聚会](https://www.luogu.com.cn/problem/P4281)
易得(详证可以写成题解了)答案一定在三个点中某两个的 LCA 上,所以把三种情况统计出最小值即可。
- [P1967 [NOIP 2013 提高组] 货车运输](https://www.luogu.com.cn/problem/P1967)
本题要先求出图的最大生成树,不难发现剩下的边都没有用。
除去祖先外还要记录另一个信息,就是向前的最小值。
跟处理祖先的方法一样,如果你是倍增就存上向前 $2^j$ 个结点的最小值,如果是树剖就存链上的最小值。然后在向上提的过程中每次都要将 $ans$ 更新。
注意:图不一定连通!一定要当森林跑每一个点。~~另外这题有三倍经验~~