倍增法求lca(最近公共祖先)
倍增法求lca(最近公共祖先)
基本上每篇博客都会有参考文章,一是弥补不足,二是这本身也是我学习过程中找到的觉得好的资料
思路:
大致上算法的思路是这样发展来的。
想到求两个结点的最小公共祖先,我们可以先把两个的深度提到同一水平,在一步一步往上跳,直到两个结点有了一个公共祖先,依照算法流程,这就是least common ancestor。
但是如果这样一步步地往上未免太让人着急,为了提高一下效率,便不再每次只跳一步,而跳\(2^i\)步。一般的,先这样蹦蹦跳跳跳上去直到两个结点相平,在两个一起这样蹦上去。
怎么确定这个i该是多少合适呢?
这里我们需要预处理一个数组f,f[u] [i]来表示结点u的第i代祖先,f[u] [0]表示结点u的父亲,这个数组用链式前向星(最近怎么老是它)加上dfs来预处理产生,十分便捷。那它有什么用呢?用在提升结点的时候,我们可以借助这个数组直接将结点提到他的合适的祖先上去。怎么才算合适的祖先?现假设求lca(s,t)
分两步,第一步是将低结点提到高结点(相对于叶节点)的深度上。这时候一个for循环从s的第20代祖先开始(为什么是二十代?可能一般的树达不到这个深度),看能不能把s提上去后满足\(depth[s]>=depth[t]\),不能满足就看第十九代祖先这样一直减少i,若满足了就把s提上去,还是继续减少i,直到两个结点最终相平。这个过程有个博客里讲的很形象(见参考文章),比喻成乌鸦喝水的过程,就是乌鸦先把体积大的(这里就是i值)放进水杯里,再逐渐减小放入物品的体积直到把水杯的水升上来,而不是先填沙子这种小颗粒的东西。这就是为什么i值要从大到小。
第二步是整体提升的过程,我们不知道该几步提升,在循环中的条件就变成了\(if(f[s][i]!=f[t][i])\)。也就是只要他两的祖先不一样就提升,祖先一样有两种可能,一种可能是节点是祖先但不是最小公共祖先,另一种可能是到了公共祖先。前一种出现在循环的开始,一开始想要提的深度比较多,所以很可能提到了公共祖先。这种情况不用管,继续减小i值,之后可能会由于满足\(if\)条件而经历一些两点提升的过程,最后不满足\(if\)条件了,说明两个已经有了最小公共祖先,这时候随意输出一个节点的父结点就行了。
算法完成,看看代码。如觉得不太清楚请看参考文章。
代码:
代码里有一些优化,把for循环改成了log_2+1数组,关于这个优化,我也不知道他优化在了什么地方,是从一篇博客看来的(见参考文章),我用了优化甚至吸了氧和没用优化没吸氧是一样的((╯‵□′)╯︵┻━┻),都T了3个点,最后是加了读入优化(见上一篇博客)才过了。
#include <iostream>
#include <cstdio>
#define max_n 500005
using namespace std;
int n,m,s;//n为结点数,m为边数,s为根节点标号
int lg[max_n];//优化用到的预处理的数组,存log_2[i]-1
int f[max_n][23];//f[u][i]表示结点u的第i代祖先,其中f[u][0]为u的父结点
int depth[max_n];//节点深度
//链式前向星
int head[max_n];
struct edge
{
int v;
int next;
}e[max_n<<1];
int cnt = 0;
void add(int u,int v)
{
++cnt;
e[cnt].v = v;
e[cnt].next = head[u];
head[u] = cnt;
}
//快速读入模板
inline void read(int& x)
{
x=0;int f=0;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') f = 1;ch = getchar();}
while('0'<=ch&&ch<='9') {x = 10*x+ch-'0';ch=getchar();}
x = f?-x:x;
}
//dfs预处理出结点往上跳2^i的结点
void dfs(int u,int from)
{
depth[u] = depth[from]+1;//比父结点深度加一
for(int i = 1;1<<i<=depth[u];i++)//祖先结点要存在,不存在的默认为零
{
f[u][i] = f[f[u][i-1]][i-1];//f[u][i]的第u个结点第i-1代祖先的第i-1代祖先记为第i代祖先
}
for(int i = head[u];i;i=e[i].next)
{
int v = e[i].v;
if(v==from) continue;//因为是无向边,判断不能反回父结点
f[v][0] = u;//
dfs(v,u);
}
}
//求最近公共祖先
int lca(int s,int t)
{
if(depth[s]<depth[t]) swap(s,t);//设s比t深
while(depth[s]>depth[t])//若s比t深,不断上移s
{
s = f[s][lg[depth[s]-depth[t]]-1];//上移log_2[深度差]步,直到相平
}
if(s==t)//若t为s的祖先
{
return s;//则s,t的lca是t
}
for(int i = lg[depth[s]]-1;i>=0;i--)//待s,t相平后
{
if(f[s][i]!=f[t][i])//只要两公共祖先不等
{
s = f[s][i];//将二者上移
t = f[t][i];
}
}
/*for(int i = 20;i>=0;i--)
{
if(f[s][i]!=f[t][i])
{
s = f[s][i];
t = f[t][i];
}
}*/
return f[s][0];//直到最后两者公共祖先相等,记为lca
}
int main()
{
read(n);
read(m);
read(s);
for(int i = 1;i<=n;i++)//优化数组玄学构造法求log_2[i]+1
{
lg[i] = lg[i-1]+((1<<lg[i-1])==i);
}
for(int i = 1;i<n;i++)
{
int u,v;
read(u);
read(v);
add(u,v);
add(v,u);
}
dfs(s,0);
int ans = 0;
for(int i = 1;i<=m;i++)
{
int a,b;
read(a);
read(b);
ans = lca(a,b);
cout << ans << endl;
}
return 0;
}
参考文章:
李白莘莘学子,树上倍增求LCA详解,https://www.cnblogs.com/lbssxz/p/11114819.html(讲的好啊讲的好)
默思·朸安,题解 P3379 【【模板】最近公共祖先(LCA)】,https://www.luogu.org/blog/morslin/solution-p3379(神秘优化的来源)
倍增法求lca(最近公共祖先)的更多相关文章
- 倍增法求LCA
倍增法求LCA LCA(Least Common Ancestors)的意思是最近公共祖先,即在一棵树中,找出两节点最近的公共祖先. 倍增法是通过一个数组来实现直接找到一个节点的某个祖先,这样我们就可 ...
- HDU 2586 倍增法求lca
How far away ? Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)To ...
- 倍增法求LCA(最近公共最先)
对于有根树T的两个结点u.v,最近公共祖先x=LCA(u,v)表示一个结点x,满足x是u.v的祖先且x的深度尽可能大. 如图,根据定义可以看出14和15的最近公共祖先是10, 15和16的最近公共 ...
- 求LCA最近公共祖先的在线倍增算法模板_C++
倍增求 LCA 是在线的,而且比 ST 好写多了,理解起来比 ST 和 Tarjan 都容易,于是就自行脑补吧,代码写得容易看懂 关键理解 f[i][j] 表示 i 号节点的第 2j 个父亲,也就是往 ...
- 树上倍增法求LCA
我们找的是任意两个结点的最近公共祖先, 那么我们可以考虑这么两种种情况: 1.两结点的深度相同. 2.两结点深度不同. 第一步都要转化为情况1,这种可处理的情况. 先不考虑其他, 我们思考这么一个问题 ...
- 在线倍增法求LCA专题
1.cojs 186. [USACO Oct08] 牧场旅行 ★★ 输入文件:pwalk.in 输出文件:pwalk.out 简单对比时间限制:1 s 内存限制:128 MB n个被自 ...
- 求LCA最近公共祖先的在线ST算法_C++
ST算法是求最近公共祖先的一种 在线 算法,基于RMQ算法,本代码用双链树存树 预处理的时间复杂度是 O(nlog2n) 查询时间是 O(1) 的 另附上离线算法 Tarjan 的链接: http ...
- 求LCA最近公共祖先的离线Tarjan算法_C++
这个Tarjan算法是求LCA的算法,不是那个强连通图的 它是 离线 算法,时间复杂度是 O(m+n),m 是询问数,n 是节点数 它的优点是比在线算法好写很多 不过有些题目是强制在线的,此类离线算法 ...
- cogs 2450. 距离 树链剖分求LCA最近公共祖先 快速求树上两点距离 详细讲解 带注释!
2450. 距离 ★★ 输入文件:distance.in 输出文件:distance.out 简单对比时间限制:1 s 内存限制:256 MB [题目描述] 在一个村子里有N个房子,一 ...
随机推荐
- pidstat 命令详解
pidstat 概述 pidstat是sysstat工具的一个命令,用于监控全部或指定进程的cpu.内存.线程.设备IO等系统资源的占用情况.pidstat首次运行时显示自系统启动开始的各项统计信息, ...
- Linux 就该这么学 CH04 VIM编辑器和Shell命令脚本
0 概述 1 Vim编辑器 在linux 中一切都是文件,而配置一个服务就是修改其配置文件的参数. vim 编辑器有三种模式:命令模式,末行模式和编辑模式. 命令模式:控制光标移动,对文件进行操作. ...
- Google Adsense(谷歌网站联盟)广告申请指南
Google AdSense 是一种获取收入的快速简便的方法,适合于各种规模的网站发布商.它可以在网站的内容网页上展示相关性较高的 Google 广告,并且这些广告不会过分夸张醒目.由于所展示的广告同 ...
- ACM算法锦集
一:知识点 数据结构: 1,单,双链表及循环链表 2,树的表示与存储,二叉树(概念,遍历)二叉树的 应用(二叉排序树,判定树,博弈树,解答树等) 3,文件操作(从文本文件中读入数据并输出到文本文 件中 ...
- Java中的常量池(字符串常量池、class常量池和运行时常量池)
转载. https://blog.csdn.net/zm13007310400/article/details/77534349 简介: 这几天在看Java虚拟机方面的知识时,看到了有几种不同常量池的 ...
- Delphi Mercadopago支付【支持支持获取账户信息和余额、创建商店,商店查询、创建二维码、二维码查询、创建订单、订单查询、订单退款等功能】
作者QQ:(648437169) 点击下载➨Delphi Mercadopago支付 [Delphi Mercadopago支付]支持 支持支持获取账户信息和余额.创建商店,商店查询.创建二维码.二维 ...
- 测试代码的练习2——python编程从入门到实践
11-3 雇员:编写一个名为Employee的类,其方法__init__() 接受名.姓和年薪,并将它们都存储在属性中.编写一个名为give_raise()的方法,它默认将年薪增加5000美元,但也能 ...
- layui 上传图片 实现过程
layui.user一个页面只能有一个,写多了会实现js效果 上传图片官方文档有很多功能,但是演示的代码只是一个一个功能演示,如果要综合起来js代码不是简单的拼凑,需要放在指定位置,比如下面的限制文件 ...
- redis源码分析(五)--cluster(集群)结构
Redis集群 Redis支持集群模式,集群中可以存在多个master,每个master又可以拥有多个slave.数据根据关键字映射到不同的slot,每一个master负责一部分的slots,数据被存 ...
- Go基础编程实践(五)—— 错误和日志
自定义错误类型 Go中可以使用errors.New()创建错误信息,也可以通过创建自定义错误类型来满足需求.error是一个接口类型,所有实现该接口的类型都可以当作一个错误类型. // error类型 ...