浅谈 LCA

LCA(Least Common Ancestors),最近公共祖先,定义为两节点最近的公共祖先好像是废话

前置芝士:

  • 图论

此文章中均设 faii 的父亲,depii 的深度。

暴力

显然我们找出节点的所有祖先再 n2 比较即可。

当然你也可以一层层往上跳。

时间复杂度是 O(n2)

倍增

我们思考一次性跳多步,减少时间复杂度。

考虑二进制拆分。

考虑用 dp 预处理:

fi,ji 往上跳 2j 步到达的点,即可以使用以下转移方程:

{fi,0=faifi,j=ffi,j1,j1

所以我们设 lg2k=log2k,即 直接从 lg2i 往下枚举即可。


整理下,如果我们求 LCA(u,v)

默认 depu<depv(如果不满足根据 LCA(u,v)=LCA(v,u) 交换 u,v 即可)

过程如下:

  1. 预处理 f 数组
  2. u,v 跳至同一层
  3. 如果相等直接返回
  4. 否则继续跳,直到它们都跳到 LCA 的往下一层

这个在链上极其好用。

代码:

int LCA(int u,int v)
{
	if (dep[u]<dep[v]) swap[u][v]; //交换
	while (dep[u]>dep[v])          //预处理
		u=fa[u][lg2[dep[u]-dep[v]-1]];
	if (u==v) return u;            //跳出
	for (int i=lg2[dep[u]-1];i>=0;--i)
		if (fa[u][i]!=fa[v][i])
			u=fa[u][i],v=fa[v][i]; //继续跳
	return fa[u][0];
}

RMQ求解

RMQ(Range [Minimum/Maximum] Query),区间最值问题。

首先我们要了解一个离线求 RMQ 的数据结构——st表(Sparse Table

因为 st 表也是倍增思想,所以转移方程也会很像:

  • fi,j 表示 i 开始,2j 个元素的最值(不一定连续)

  • fi,j=min(ormax)(fi,j1,fi+2j1,j1)(切开求解)

首先考虑最值允许区间重叠:

max(a,b,c)=max(max(a,b),max(b,c))

我们即肯定能找到一个 x 使得 [l,l+2x1][r2x+1,r] 的最值与 [l,r] 的最值相等。

则这个 x 很容易想出(l,r 之间有 rl+1 个元素):

x=log2(rl+1)


我们回归 LCA。

首先我们要了解欧拉序

以此图为例:

图片.png

4 为树根:

则它的属性:

  • DFS 序:4,2,3,1,6,5
  • 带上回溯的 DFS 序:4,2,3,2,1,2,4,6,5,6,4

其中“带上回溯的 DFS 序”即为欧拉序。


我们看看此图:

图片.png

比如我们找 4,8 的 LCA:

写出欧拉序:

1,2,4,2,3,2,1,5,6,8,6,5,7,5,1

转换为深度:

1,2,3,2,3,2,1,2,3,4,3,2,3,2,1

找出 4,8 之间区域:

1,2,3,2,3,2,1,2,3,4,3,2,3,2,1

正好深度最低的点就是 1,它们的 LCA!

这样就把 LCA 转换为了 RMQ,st表求解即可。

Tarjan 算法

我们引用 rxz 的话:

一个熊孩子 Link 从一颗有根树的最左下节点灌岩浆,Link 表示很讨厌这种倒着长的树,岩浆会不断蔓延到整个树。

如果岩浆灌满了一颗子树 Link 发现树的右边有棵更深的子树,则 Link 会去灌岩浆。

岩浆只有迫不得已的情况才会升高,找新子树进行注入。

机(yu)智(chun)的 Link 发现了一个求 LCA 的好办法,即:如果两个节点都被岩浆烧掉时,它们的 LCA 即为那棵子树上岩浆最高的位置。

即按 rxz 描述的写即可,伪代码如下:

void tarjan()
{
	for (u的所有儿子v)
    {
		tarjan(v);
        merge(u,v); //并查集
    }
    for (所有与u有关的查询(u,v))
    	if (vis[v]) ans[id]=find(v);
}

树剖写法

树剖,即树链剖分,将树变为链的方法,可以应对某些毒瘤出题人将数列上问题转移到树上的情况。

我们求 LCA 用的是轻重链剖分,也就是将树变成轻链和重链。

我们首先给出一些定义:

  • 重儿子:某节点儿子中子树最大的儿子(相等随便选一个)
  • 轻儿子:除重儿子以外的所有儿子
  • 重边:爹连到重儿子的边(爹不一定是重儿子)
  • 轻边:除重边外所有边
  • 重链:重边组成的链(轻叶节点自成重链
  • 轻链:轻边组成的链

我们树剖需要的数组:

  • sizi 表示以 i 为根的子树大小。
  • hvsi 表示 i 节点的重儿子。
  • ltpi 表示 i 所在的重链头(深度最浅节点)。

树刨和莫队等等一样都是优雅的暴力 ,会被轻重链交替的数据或者全是轻链的数据卡死。

首先两次 DFS:

  • 第一次求 fadepsizhvs
  • 第二次只求 ltp

然后轻重链交替跳 LCA 即可(适时原 地 踏 步)。

posted @   yspm  阅读(284)  评论(1编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
😅​
点击右上角即可分享
微信分享提示