当我们处理树上点与点关系的问题时(例如,最简单的,树上两点的距离),常常需要获知树上两点的最近公共祖先(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. 【Nginx】如何封禁IP和IP段?看完这篇我会了!!

    写在前面 Nginx不仅仅只是一款反向代理和负载均衡服务器,它还能提供很多强大的功能,例如:限流.缓存.黑白名单和灰度发布等等.在之前的文章中,我们已经介绍了Nginx提供的这些功能.小伙伴们可以到[ ...

  2. python实现图片文字提取,准确率高达99%,强无敌!!!

    上次我使用的百度AI开放平台的API接口实现图片的转化,后来有许多小伙伴都私信问我,怎么获取百度AI平台的AK和SK.为了统一回答大家的问题,今天我又使用百度API实现了一个从图片中提取文字和识别身份 ...

  3. Qt_IO系统_二进制读写

    目录 Qt中的读写 QDataStream QDataStream Qt 是如何存储数据的 如何保证读取数据的正确性? --> 魔术数字,文件版本,Qt版本 魔术数字 文件版本 Qt 版本 读取 ...

  4. Go Pentester - HTTP CLIENTS(4)

    Interacting with Metasploit msf.go package rpc import ( "bytes" "fmt" "gopk ...

  5. 老司机带你玩转面试(5):Redis 集群模式 Redis Cluster

    前文回顾 建议前面文章没看过的同学先看下前面的文章: 「老司机带你玩转面试(1):缓存中间件 Redis 基础知识以及数据持久化」 「老司机带你玩转面试(2):Redis 过期策略以及缓存雪崩.击穿. ...

  6. Vuex与axios的封装和调用

    Vuex状态管理 状态就是数据.    在react里有个Flux的数据流管理(单向数据流) 作用1:实现组件之间的数据共享. 作用2:用于缓存.(避免当用户频繁点击,页面不断调接口)     先安装 ...

  7. Azure Traffic Manager(二) 基于权重与基于优先级的路由策略为我们的Web项目提供负载均衡

    一,引言 上一片文章我们使用 Azure Traffic Manager 分发用户请求,同时演示了两种路由策略,“Performance”,“Geographic”的两种方式,今天我们继续讲解 Tra ...

  8. coding如何绑定二次验证码_虚拟MFA_两步验证_身份验证?

    Coding.net 是一个面向开发者的云端开发平台,提供 Git/SVN 代码托管.任务管理.在线 WebIDE.Cloud Studio.开发协作.文件管理.Wiki 管理.提供个人服务及企业管理 ...

  9. Java继承之面向对象

    面向对象与面向过程: 面向对象(OOP)与面向过程 二者都是一种思想,面向对象是相对于面向过程而言的. 面向过程,强调的是功能行为.面向对象,将功能封装进对象,强调具备了功能的对象. 面向对象更加强调 ...

  10. 数据库(二):初识sql语句

    进击のpython ***** 数据库--初识sql语句 前面提到了说,数据库管理系统就像我们曾经做过的输入命令返回结果的socket通信差不多 那既然提到了命令,在MySQL中,有一些基本的语句,就 ...