【算法】浅学 LCA
参考资料
一、概念
最近公共祖先称为 LCA (Lowest Common Ancestor)
它指的是在一颗树中,离两个节点最近的公共祖先
如下图,
节点 7 和节点 5 的最近公共祖先是 2
节点 8 和节点 3 的最近公共祖先是 1
节点 4 和节点 2 的最近公共祖先是 2

那么求 LCA 有哪些方法呢?
二、实现
• 暴力
我们不难想到一种很暴力的想法
如上图,现在我们要求 7 和 5 的 LCA
令 x = 7 , y = 5
首先我们先让 x 和 y 两个节点在同一层,如果深度不一的话,浅的那个节点很有可能就会超过他们的 LCA
x 变为它的父节点,通俗点说就是跳到它父节点的位置
现在 x 和 y 深度相等了,就一起跳到它们各自的父节点,直到 x 和 y 跳到了同一个节点。而这个节点就是它们的 LCA
很显然,时间爆掉了,我们需要一点优化
• 倍增
上面的想法一格一格地跳太慢了,那么可不可以让它们一次跳一大块呢?
这里我们就可以运用倍增思想,不了解倍增思想的可以先看看参考资料那
先定义几个数组
\(fa_{[i][j]}\) 表示 i 节点的第 \(2^j\) 个祖先
\(deep_{[i]}\) 表示 i 节点的深度
首先还是一样的,将 x 和 y 跳到同一深度,这里也要用倍增法
每次考虑跳 \(2^n\) 次,但是不能超过 LCA 的深度
也就是每次跳的时候,判定一下如果跳了 \(2^n\) 后,两个节点的父亲会不会相同,如果相同,就表示跳到 LCA 或者跳过头了
最后输出跳完之后 x 和 y 的父节点即可
预处理 fa 数组时,我们可以运用一个显而易见的结论,i 的 \(2^j\) 个祖先 = i 的 \(2^{j-1}\) 个祖先的 \(2^{j-1}\) 个祖先
表示出来就是这样的:
\]
为什么呢?
首先不难得出,\(2^{j-1}=2^j\div2\) (初中的幂运算)
这在树上可以表现为将 i 上面的 \(2^j\) 层平均分成了两份,跳了一半,再跳一半,当然就可以跳到层的最顶端了
三、代码
• 暴力
• 倍增
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,s;
int tot,head[N<<1];
int ln,fa[N][35],deep[N];
struct node{
int nex,to;
}edge[N<<1];
void add(int x,int y){
edge[++tot].to=y;
edge[tot].nex=head[x];
head[x]=tot;
}
void dfs(int x,int fx){
fa[x][0]=fx;
deep[x]=deep[fx]+1;
for(int i=1;i<=ln;i++){
fa[x][i]=fa[fa[x][i-1]][i-1];
}
for(int i=head[x];i;i=edge[i].nex){
if(edge[i].to==fx) continue;
dfs(edge[i].to,x);
}
}
int lca(int x,int y){
if(deep[x]>deep[y]) swap(x,y);
for(int i=ln;i>=0;i--){
if((deep[y]-deep[x])>>i&1!=0) y=fa[y][i];
}
if(x==y) return x;
for(int i=ln;i>=0;i--){
if(fa[x][i]!=fa[y][i]){
x=fa[x][i];y=fa[y][i];
}
}
return fa[x][0];
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>m>>s;
ln=log2(n);
for(int i=1;i<=n-1;i++){
int x,y;
cin>>x>>y;
add(x,y);add(y,x);
}
dfs(s,0);
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
cout<<lca(x,y)<<"\n";
}
return 0;
}
四、时间复杂度
• 暴力
| 操作 | 时间复杂度 |
|---|---|
| 预处理 | O(n) |
| 查询 | O(n) |
| 随机树查询 | O(log n) |
• 倍增
| 操作 | 时间复杂度 |
|---|---|
| 预处理 | O(n log n) |
| 查询 | O(log n) |
五、例题
• 斐波那契
problem
Solve
看到题面,一眼 LCA
但这颗树和普通的树不一样,关键在于它编码之间的父子关系,所以我们不妨先观察一下这颗树:

看一下父子节点之间的差值(以 1 和它的孩子们为例)
| 孩子 | 差值 |
|---|---|
| 2 | 1 |
| 3 | 2 |
| 4 | 3 |
| 6 | 5 |
| 9 | 8 |
如果还是没能看出规律的话,不妨在最前面加一个 1,数列就变成了 1 1 2 3 5 8,这就是斐波那契数列
于是我们可以通过这个关系来找到子节点的父亲,与 LCA 的算法思想一样,假设现在要求 x 和 y 两个节点的 LCA
如果 x = y,就意味着它们已经找到了最近公告祖先,直接跳出循环
否则找到 x,y中编号更大的一个,跳到它的父节点
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+50;
const int M=60;
int fli[N];
inline int lca(int x,int y){
while(x!=y){
if(x<y) swap(x,y);
int l=0,r=M+1;
while(l+1<r){
int mid=(l+r)>>1;
if(fli[mid]<x) l=mid;
else r=mid;
}
x-=fli[l];
}
return x;
}
signed main(){
ios::sync_with_stdio(false);
int t;
cin>>t;
fli[1]=fli[2]=1;
for(int i=3;i<=M;i++) fli[i]=fli[i-1]+fli[i-2];
while(t--){
int x,y;
cin>>x>>y;
cout<<lca(x,y)<<"\n";
}
return 0;
}
• 紧急集合
Problem
Solve
三个节点的最近公共祖先问题,可能会想到两两求最近公共祖先的想法,但其实这样路径并不会最小,比如下面这图,要求的三个点分别为5 7 8:

按照之前的思路,它们应该在 1 集合,此时的花费是 8。但如果在 6 集合的话,花费只有 6,明显更优
我们分别对三个点取 LCA,此时:
| 节点 A | 节点 B | LCA |
|---|---|---|
| 5 | 8 | 1 |
| 5 | 7 | 1 |
| 7 | 8 | 6 |
此时有两个 LCA 重合,而更优的是取不重合的那个节点,我们就可以就此写出代码了
Code
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,ln;
int tot,head[N<<1];
int deep[N],fa[N][35];
int ansi,money;
struct node{
int to,nxt;
}edge[N<<1];
void add(int x,int y){
edge[++tot].to=y;
edge[tot].nxt=head[x];
head[x]=tot;
}
void dfs(int x,int fx){
deep[x]=deep[fx]+1;
fa[x][0]=fx;
for(int i=1;i<=ln;i++) fa[x][i]=fa[fa[x][i-1]][i-1];
for(int i=head[x];i;i=edge[i].nxt){
int son=edge[i].to;
if(son==fx) continue;
dfs(son,x);
}
}
int lca(int x,int y){
if(deep[x]<deep[y]) swap(x,y);
for(int i=ln;i>=0;i--){
if(((deep[x]-deep[y])>>i&1)!=0){
x=fa[x][i];
// money+=(1>>i);
}
}
if(x==y) return x;
for(int i=ln;i>=0;i--){
if(fa[x][i]!=fa[y][i]){
x=fa[x][i];y=fa[y][i];
// money+=(1<<i);
}
}
// money++;
return fa[x][0];
}
int main(){
ios::sync_with_stdio(false);
int t;
cin>>n>>t;
ln=log2(n);
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
add(x,y);add(y,x);
}
dfs(1,0);
while(t--){
//money=0;
int x,y,z,a,b,c;
cin>>x>>y>>z;
// cout<<lca(lca(x,y),lca(y,z))<<" "<<money<<"\n";
a=lca(x,y),b=lca(y,z),c=lca(x,z);
if(a==b) ansi=c;
else if(b==c) ansi=a;
else ansi=b;
money=deep[x]+deep[y]+deep[z]-deep[a]-deep[b]-deep[c];
cout<<ansi<<" "<<money<<"\n";
}
return 0;
}
【算法】浅学 LCA的更多相关文章
- junit浅学笔记
JUnit是一个回归测试框架(regression testing framework).Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(Wh ...
- 从最大似然到EM算法浅解
从最大似然到EM算法浅解 zouxy09@qq.com http://blog.csdn.net/zouxy09 机器学习十大算法之中的一个:EM算法.能评得上十大之中的一个,让人听起来认为挺NB的. ...
- 浅学JavaScript
JavaScript是互联网上最流行的脚本语言,可广泛用于服务器.PC.笔记本电脑智能手机等设备: 对事件的反应: <!DOCTYPE html> <html> <hea ...
- Kmp算法浅谈
Kmp算法浅谈 一.Kmp算法思想 在主串和模式串进行匹配时,利用next数组不改变主串的匹配指针而是改变模式串的匹配指针,减少大量的重复匹配时间.在Kmp算法中,next数组的构建是整个Kmp算法的 ...
- Java实现 蓝桥杯VIP 算法训练 学做菜
算法训练 学做菜 时间限制:1.0s 内存限制:256.0MB 问题描述 涛涛立志要做新好青年,他最近在学做菜.由于技术还很生疏,他只会用鸡蛋,西红柿,鸡丁,辣酱这四种原料来做菜,我们给这四种原料标上 ...
- 0算法基础学算法 搜索篇第二讲 BFS广度优先搜索的思想
dfs前置知识: 递归链接:0基础算法基础学算法 第六弹 递归 - 球君 - 博客园 (cnblogs.com) dfs深度优先搜索:0基础学算法 搜索篇第一讲 深度优先搜索 - 球君 - 博客园 ( ...
- Tarjan算法离线 求 LCA(最近公共祖先)
本文是网络资料整理或部分转载或部分原创,参考文章如下: https://www.cnblogs.com/JVxie/p/4854719.html http://blog.csdn.net/ywcpig ...
- 【算法】RMQ LCA 讲课杂记
4月4日,应学弟要求去了次学校给小同学们讲了一堂课,其实讲的挺内容挺杂的,但是目的是引出LCA算法. 现在整理一下当天讲课的主要内容: 开始并没有直接引出LCA问题,而是讲了RMQ(Range Min ...
- 利用Tarjan算法解决(LCA)二叉搜索树的最近公共祖先问题——数据结构
相关知识:(来自百度百科) LCA(Least Common Ancestors) 即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先. 例如: 1和7的最近公共祖先为5: 1和5的 ...
随机推荐
- Redis入门到实战
一.Redis基础 Redis所有的命令都可以去官方网站查看 1.基本命令 keys * 查找所有符合给定模式pattern(正则表达式)的 key .可以进行模糊匹配 del key1,key2,. ...
- 新一代分布式实时流处理引擎Flink入门实战之先导理论篇-上
@ 目录 概述 定义 为什么使用Flink 应用行业和场景 应用行业 应用场景 实时数仓演变 Flink VS Spark 架构 系统架构 术语 无界和有界数据 流式分析基础 分层API 运行模式 作 ...
- oracle 怎么查看用户对应的表空间
oracle 怎么查看用户对应的表空间? 查询用户: 查看数据库里面所有用户,前提是你是有 dba 权限的帐号,如 sys,system: select * from dba_users; 查看你能管 ...
- 解决:Uncaught TypeError: $ is not a function
本来好好的,突然就出现的错误,不过这并不是什么难解决的错误: 我的问题是:在js文件里我定义了一个var $;变量,只要把这个去掉就没问题了. 出现这种错误的解决方法: 1,先看看你的jq文件是否已经 ...
- [开源精品] C#.NET im 聊天通讯架构设计 -- FreeIM 支持集群、职责分明、高性能
FreeIM 是什么? FreeIM 使用 websocket 协议实现简易.高性能(单机支持5万+连接).集群即时通讯组件,支持点对点通讯.群聊通讯.上线下线事件消息等众多实用性功能. ImCore ...
- Dubbo源码(九) - 服务调用过程
1. 前言 本文基于Dubbo2.6.x版本,中文注释版源码已上传github:xiaoguyu/dubbo 源码分析均基于官方Demo,路径:dubbo/dubbo-demo 如果没有看过之前Dub ...
- 第十一篇:vue.js监听属性(大作业进行时)
这个知识点急着用所以就跳过<计算属性>先学了 首先理解一下什么是监听:对事件进行监控,也就是当我进行操作(按了按钮之类的事件)时,会有相应的事情发生 上代码 <div id = &q ...
- ABC266.
D 设 \(f_{t,p}\) 代表在 \(t\) 时间点时人在 \(p\) 点的最大收益,在这一步他可以 \(p\) 增加,不动,\(p\) 减少.于是得出状态转移方程:\(f_{t,p} = \m ...
- 记一次 .NET 某数控机床控制程序 卡死分析
一:背景 1. 讲故事 前段时间有位朋友微信上找到我,说它的程序出现了卡死,让我帮忙看下是怎么回事? 说来也奇怪,那段时间求助卡死类的dump特别多,被迫训练了一下对这类问题的洞察力 ,再次声明一下, ...
- KingbaseES V8R6C5单实例sys_backup.sh备份案例
案例说明: KingbaseES V8R6C5版本中使用了securecmdd工具,用于主机节点间的通讯,默认端口8890.备份工具sys_backup.sh默认使用了securecmdd工具,对 ...