当我们处理树上点与点关系的问题时(例如,最简单的,树上两点的距离),常常需要获知树上两点的最近公共祖先(Lowest Common Ancestor,LCA)。如下图所示:

2号点是7号点和9号点的最近公共祖先

我们先来讨论朴素的做法。

首先进行一趟dfs,求出每个点的深度:

int dep[MAXN];
bool vis[MAXN];
void dfs(int cur, int fath = 0)
{
if (vis[cur])
return;
vis[cur] = true;
dep[cur] = dep[fath] + 1; // 每个点的深度等于父节点的深度+1
for (int eg = head[cur]; eg != 0; eg = edges[eg].next)
dfs(edges[eg].to, cur);
}

现在A点的深度比B点深,所以我们先让B点往上“爬”,爬到与A点深度相等为止。然后A点和B点再一起往上爬,直到两点相遇,那一点即为LCA:

这样下来,每次查询LCA的最坏时间复杂度是 的。


有时候,我们需要进行很多次查询,这时朴素的 复杂度就不够用了。我们考虑空间换时间的倍增算法。

倍增的思想直观体现就在 ST表 中提及过。我们用一个数组fa[i][k]存储 号点的 级祖先。(父节点为1级祖先,祖父结点为2级祖先……以此类推)

那么这可以在dfs途中动态规划得出:

// 在dfs中...
fa[cur][0] = fath;
for (int i = 1; i <= Log2[dep[cur]]; ++i) // Log2的预处理参见ST表的笔记
fa[cur][i] = fa[fa[cur][i - 1]][i - 1]; // 这个DP也参见ST表的笔记

这样,往上爬的次数可以被大大缩短(现在变成“跳”了)。

首先还是先让两点深度相等:

if (dep[a] > dep[b]) // 不妨设a的深度小于等于b
swap(a, b);
while (dep[a] != dep[b]) // 跳到深度相等为止
b = fa[b][Log2[dep[b] - dep[a]]]; // b不断往上跳

例如,a和b本来相差22的深度,现在b不用往上爬22次,只需要依次跳16、4、2个单位,3次便能达到与a相同的距离。

两者深度相等后,如果两个点已经相遇,那么问题就得以解决。如果尚未相遇,我们再让它们一起往上跳。问题在于,如何确定每次要跳多少?正面解决也许不太容易,我们逆向思考:如何在a、b不相遇的情况下跳到尽可能高的位置?如果找到了这个位置,它的父亲就是LCA了。

说来也简单,从可能跳的最大步数Log2[dep[a]](这样至多跳到0号点,不会越界)开始,不断减半步数(不用多次循环):

for (int k = Log2[dep[a]]; k >= 0; k--)
if (fa[a][k] != fa[b][k])
a = fa[a][k], b = fa[b][k];

以刚刚那棵树为例,先尝试Log2[4]=2,A、B点的 级祖先都是0(图中未画出),所以不跳。然后尝试1,A、B的 祖先都是2,也不跳。最后尝试0,A、B的1级祖先分别是4和5,跳。结束。

这样下来,再往上一格所得到的2号点就是所求的最近公共祖先。


主要代码如下:

int Log2[MAXN], fa[MAXN][20], dep[MAXN]; // fa的第二维大小不应小于log2(MAXN)
bool vis[MAXN];
void dfs(int cur, int fath = 0)
{
if (vis[cur])
return;
vis[cur] = true;
dep[cur] = dep[fath] + 1;
fa[cur][0] = fath;
for (int i = 1; i <= Log2[dep[cur]]; ++i)
fa[cur][i] = fa[fa[cur][i - 1]][i - 1];
for (int eg = head[cur]; eg != 0; eg = edges[eg].next)
dfs(edges[eg].to, cur);
}
int lca(int a, int b)
{
if (dep[a] > dep[b])
swap(a, b);
while (dep[a] != dep[b])
b = fa[b][Log2[dep[b] - dep[a]]];
if (a == b)
return a;
for (int k = Log2[dep[a]]; k >= 0; k--)
if (fa[a][k] != fa[b][k])
a = fa[a][k], b = fa[b][k];
return fa[a][0];
}
int main()
{
// ...
for (int i = 2; i <= n; ++i)
Log2[i] = Log2[i / 2] + 1;
// ...
dfs(s); // 无根树可以随意选一点为根
// ...
return 0;
}

至于树上两点 的距离,有公式 (很好推)。 预处理, 查询,空间复杂度为

当然,以上都是针对无权树的,如果有权值,可以额外记录一下每个点到根的距离,然后用几乎相同的公式求出。

算法学习笔记:最近公共祖先(LCA问题)的更多相关文章

  1. 学习笔记--最近公共祖先(LCA)的几种求法

    前言: 给定一个有根树,若节点\(z\)是两节点\(x,y\)所有公共祖先深度最大的那一个,则称\(z\)是\(x,y\)的最近公共祖先(\(Least Common Ancestors\)),简称\ ...

  2. [一本通学习笔记] 最近公共祖先LCA

    本节内容过于暴力没什么好说的.借着这个专题改掉写倍增的陋习,虽然写链剖代码长了点不过常数小还是很香. 10130. 「一本通 4.4 例 1」点的距离 #include <bits/stdc++ ...

  3. Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集)

    Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集) Description sideman做好了回到Gliese 星球的硬件准备,但是sideman的导航系统还没有完全设计好.为 ...

  4. POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA)

    POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA) Description A ...

  5. [模板] 最近公共祖先/lca

    简介 最近公共祖先 \(lca(a,b)\) 指的是a到根的路径和b到n的路径的深度最大的公共点. 定理. 以 \(r\) 为根的树上的路径 \((a,b) = (r,a) + (r,b) - 2 * ...

  6. [知识点]最近公共祖先LCA

    UPDATE(20180822):重写部分代码. 1.前言 最近公共祖先(LCA),作为树上问题,应用非常广泛,而求解的方式也非常多,复杂度各有不同,这里对几种常用的方法汇一下总. 2.基本概念和暴力 ...

  7. 【lhyaaa】最近公共祖先LCA——倍增!!!

    高级的算法——倍增!!! 根据LCA的定义,我们可以知道假如有两个节点x和y,则LCA(x,y)是 x 到根的路 径与 y 到根的路径的交汇点,同时也是 x 和 y 之间所有路径中深度最小的节 点,所 ...

  8. C / C++算法学习笔记(8)-SHELL排序

    原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...

  9. POJ 1470 Closest Common Ancestors(最近公共祖先 LCA)

    POJ 1470 Closest Common Ancestors(最近公共祖先 LCA) Description Write a program that takes as input a root ...

  10. Manacher算法学习笔记 | LeetCode#5

    Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...

随机推荐

  1. 关于Haskell计算斐波那契数列的思考

    背景 众所周知,Haskell语言是一门函数式编程语言.函数式编程语言的一大特点就是数值和对象都是不可变的,而这与经常需要对状态目前的值进行修改的动态规划算法似乎有些"格格不入", ...

  2. Python Ethical Hacking - WEB PENETRATION TESTING(1)

    WHAT IS A WEBSITE Computer with OS and some servers. Apache, MySQL ...etc. Cotains web application. ...

  3. JavaScript动画实例:粒子文本

    1.粒子文本的实现原理 粒子文本的实现原理是:使用两张 canvas,一张是用户看不到的canvas1,用来绘制文本:另一张是用户看到的canvas2,用来根据canvas1中绘制的文本数据来生成粒子 ...

  4. 掌握Redis分布式锁的正确姿势

    本文中案例都会在上传到git上,请放心浏览 git地址:https://github.com/muxiaonong/Spring-Cloud/tree/master/order-lock 本文会使用到 ...

  5. 数据库-SQL查询语言(一)

    SQL数据定义 DDL sql的DDL不仅能定义一组关系,还能定义每个关系的信息,包括: 每个关系的模式 每个属性的取值类型 完整性约束 每个关系的维护的索引集合 每个关系的安全性和权限信息 每个关系 ...

  6. 一个通用的两级Makefile例子

    目的 进行如项目的顶层目录后,运行make,即可直接编译项目中所有的源文件,并生成最终的可执行文件 实现头文件自动依赖 添加源文件不用修改Makefile,且可以自动编译新文件 顶层目录下添加文件夹, ...

  7. Java中static、final和static final(final static)的区别(转)

    大佬的总结(大赞!) final可以修饰:属性,方法,类,局部变量(方法中的变量) final修饰的属性的初始化可以在编译期,也可以在运行期,初始化后不能被改变. final修饰的属性跟具体对象有关, ...

  8. WebApiClientCore简约调用百度AI接口

    WebApiClientCore WebApiClient.JIT/AOT的netcore版本,集高性能高可扩展性于一体的声明式http客户端库,特别适用于微服务的restful资源请求,也适用于各种 ...

  9. 洛谷p1120小木棍(剪枝优化)

    #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> ...

  10. Markdown画图(mermaid)学习

    简介 目前博客园支持mermaid的graph,subgraph,sequenceDiagram,gantt,classDiagram mermaid(美人鱼), 是一个类似markdown,用文本语 ...