最近公共祖先 lca (施工ing)
咳咳,进入重难点的图论算法之一 : lca!(敲黑板)
题目: 洛谷 P3379
一、定义
LCA 指的是最近公共祖先(Least Connon Ancestors)。具体地,给定一棵有根树,若节点 z 既是 x 的祖先,也是节点 y 的祖先,则称 z 是 x , y 的公共祖先。在 x , y 的公共祖先中,深度最大的一个节点称为 x , y 的最近公共祖先,记为 LCA(x , y)。
我们来举个例子,如图所示:LCA(4 , 5)= 2,LCA(5 , 6)= 1,LCA(2 , 3)= 1。



二、如何求 LCA
我们考虑“暴力”要怎么实现找两点的 LCA。
举个例子,如图: 7 , 5 的 LCA 是 2。

先 DFS 一遍找出每个点的 DEP (深度)。然后先从深度大的 7 往上跳,跳到和 5 深度相同的点 4,发现还是没有到同一个点,那么 4、5 继续往上跳,直到跳到 2 位置,发现点一样了,那么 2 就是它们的 LCA 了。
三、如何优化这个方法
这里一共有两种方法:
1.离线 tarjan 求 LCA: 这里就贴一贴别人的博客吧
var
i,j,k,n,m,s,t:longint;
first,next,en,qfirst,qnext,qen,b,f,ans:array[..] of longint;
procedure add(k,x,y:longint);
begin
next[k]:=first[x];
first[x]:=k;
en[k]:=y;
end;
procedure qadd(k,x,y:longint);
begin
qnext[k]:=qfirst[x];
qfirst[x]:=k;
qen[k]:=y;
end;
function find(x:longint):longint;
begin
if f[x]=x then exit(x) else begin f[x]:=find(f[x]); exit(f[x]) end;
end;
procedure tarjan(x:longint);
var
j,k,t:longint;
begin
b[x]:=;
t:=first[x];
while t> do
begin
if b[en[t]]= then
begin
tarjan(en[t]);
j:=find(x);
k:=find(en[t]);
f[k]:=j;
end;
t:=next[t];
end;
t:=qfirst[x];
while t> do
begin
if b[qen[t]]= then
begin
ans[t]:=find(qen[t]);
if t mod = then ans[t+]:=ans[t]
else ans[t-]:=ans[t];
end;
t:=qnext[t];
end;
b[x]:=;
end;
begin
readln(n,m,s);
for i:= to n- do
begin
readln(j,k);
add(i*-,j,k);
add(i*,k,j);
end;
for i:= to m do
begin
readln(j,k);
qadd(i*-,j,k);
qadd(i*,k,j);
end;
for i:= to n do
f[i]:=i;
tarjan(s);
for i:= to n do
writeln(ans[i*]);
end.
再附上我以前的 Pascal 标程 (虽然现在不太理解)
2.倍增求 LCA:我们考虑这个方法慢在哪里,当然是对于每个点,一次往上跳一步,导致了效率低。那么如何优化呢?只要一次能向上跳多步,效率自然就高了。
树上倍增法
树上倍增法是一个很重要的算法。设 f [ x , k ] 表示 x 的 2 k 辈祖先,即从 x 向根节点走 2 k 步到达的节点。特别地,若该节点不存在,则令 f [ x , k ] = 0 。f [ x , 0 ] 就是 x 的父节点。
因为 x 向根节点走 2 k 步 ⇔ 向根走 2 k-1 步,再走 2 k-1 步(2 k = 2 k-1 + 2 k-1)。所以对于 k ∈ [ 1 , log n ],有 f [ x , k ] = f [ f [ x , k-1 ] , k-1 ]。
这类似于一个动态规划的过程,“阶段”就是节点的深度。因此,我们可以对树进行遍历,由此得到 f [ x , 0 ] ,再计算 f 数组的所有值。
以上部分是预处理,时间复杂度为 O(n log n)。之后可以多次对不同的 x , y 计算 LCA,每次询问的时间复杂度为 O(log n)。
然后基于 f 数组计算 LCA(x , y)分为以下几步:
① 设 dep [ x ] 表示 x 的深度。不妨设 dep [ x ] ≥ dep [ y ](否则,可交换 x , y)。
② 用二进制拆分思想,把 x 向上调整到与 y 同一深度。
具体来说,就是依次尝试从 x 向上走 k = 2 log n……2 1,2 0 步,若到达的节点比 y 深,则令 x = f [ x , k ] 。
③ 若此时 x = y,说明已经找到了 LCA,LCA 就等于 y 。
④ 若此时 x ≠ y,那么 x , y 就继续往上跳,用二进制拆分思想,把 x , y 同时向上调整,并保持深度一致且两者不相会。
具体来说,就是依次尝试把 x , y 同时向上走 k = 2 log n……2 1,2 0 步,若 f [ x , k ] ≠ f [ y , k ](即仍未相会),则令 x = f [ x , k ],y = f [ y , k ] 。
⑤ 此时 x , y 必定只差一步就相会了,所以它们的父节点 f [ x , 0 ](或 f [ y , 0 ]) 就是它们的 LCA !
【代码实现】
预处理:
void deal_first(int u,int father)
{
dep[u]=dep[father]+;
for (int i=;i<=;i++)
f[u][i+]=f[f[u][i]][i];
for (int e=first[u];e;e=next[e])
{
int v=en[e];
if (v==father) continue;
f[v][]=u; // v 向上跳 20=1 就是 u
deal_first(v,u);
}
}
查询 x , y 的 LCA:
int lca(int x,int y)
{
if (dep[x]<dep[y]) swap(x,y); //让 x 的深度较大
//我们用“暴力”的思想:先将 x,y 跳到一个深度,然后一起往上跳
for (int i=;i>=;i--) //一定要倒着 for
{
if (dep[f[x][i]]>=dep[y]) x=f[x][i]; //先跳到同一层
if (x==y) return x;
}
for (int i=;i>=;i--) //此时 x,y 已跳到同一层
if (f[x][i]!=f[y][i]) //如果 f[x][i]和 f[y][i] 不同才跳
{
x=f[x][i];
y=f[y][i];
}
return f[x][];
// x,y 是深度最浅且不同的点,即 lca 的子节点
}
然后附完整的 Pascal 标程(以前写的,步骤排列不太一样,但总体思路是差不多的):
var
rmq:array[..,..] of longint;
first,next,en,one,b:array[..] of longint;
deep,a:array[..] of longint;
i,j,k,m,n,s,t,l,r:longint;
procedure add(k,x,y:longint);
begin
next[k]:=first[x];
first[x]:=k;
en[k]:=y;
end;
procedure getrmq;
begin
for i:= to k do
rmq[i,]:=i;
for j:= to do
for i:= to k do
if i+(<<j)-<=k then
if deep[rmq[i,j-]]<deep[rmq[i+(<<(j-)),j-]]
then rmq[i,j]:=rmq[i,j-]
else rmq[i,j]:=rmq[i+(<<(j-)),j-];
end;
function que:longint;
begin
t:=trunc(ln(r-l+)/ln());
if deep[rmq[l,t]]<deep[rmq[r-(<<t)+,t]] then exit(a[rmq[l,t]])
else exit(a[rmq[r-(<<t)+,t]]);
end;
procedure dfs(x,y:longint);
var
t:longint;
begin
inc(k);
deep[k]:=y;
b[x]:=;
a[k]:=x;
t:=first[x];
while t> do
begin
if b[en[t]]= then
begin
dfs(en[t],y+);
inc(k);
a[k]:=x;
deep[k]:=y;
end;
t:=next[t];
end;
b[x]:=;
end;
begin
readln(n,m,s);
for i:= to n- do
begin
readln(j,k);
add(i*-,j,k);
add(i*,k,j);
end;
k:=;
dfs(s,);
for i:= to k do
if b[a[i]]= then
begin
one[a[i]]:=i;
b[a[i]]:=;
end;
getrmq;
for i:= to m do
begin
readln(j,k);
l:=one[j];
r:=one[k];
if l>r then
begin
j:=l;
l:=r;
r:=j;
end;
writeln(que);
end;
end.
自己现在不理解的 Pascal 标程
四、习题
1 . 洛谷 P3258:松鼠的新家(LCA+树上差分,当然也可以用树链剖分做)
#include<cstdio>
#include<algorithm>
#include<cmath> using namespace std; int n,x,y,tot,s,t;
int first[],next[],en[];
int ans[],dep[],f[][],a[]; void deal_first(int u,int father)
{
dep[u]=dep[father]+;
for (int i=;i<=;i++)
f[u][i+]=f[f[u][i]][i];
for (int e=first[u];e;e=next[e])
{
int v=en[e];
if (v==father) continue;
f[v][]=u;
deal_first(v,u);
}
} int lca(int x,int y)
{
if (dep[x]<dep[y]) swap(x,y);
for (int i=;i>=;i--)
{
if (dep[f[x][i]]>=dep[y]) x=f[x][i];
if (x==y) return x;
}
for (int i=;i>=;i--)
if (f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
return f[x][];
} void add(int x,int y)
{
next[++tot]=first[x];
first[x]=tot;
en[tot]=y;
} void updata(int u,int father)
{
for (int e=first[u];e;e=next[e])
{
int v=en[e];
if (v==father) continue;
updata(v,u);
ans[u]+=ans[v];
}
} int main()
{
scanf("%d\n",&n);
for (int i=;i<=n;i++)
scanf("%d",&a[i]);
for (int i=;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
deal_first(,);
for (int i=;i<n;i++)
{
int LCA=lca(a[i],a[i+]);
ans[a[i]]++;
ans[a[i+]]++;
ans[LCA]--;
ans[f[LCA][]]--;
}
updata(,); //每条路的终点会同时作为下条路的起点重复+1
//最后的终点不会重复,但题目说不要+1
//于是每条路的终点我们都加多了一次
//所以现在将每条路的终点都-1
for (int i=;i<=n;i++)
ans[a[i]]--; for (int i=;i<=n;i++)
printf("%d\n",ans[i]);
return ;
}
C++ 标程(差分时记得去重)
以上内容部分借鉴于:《信息学奥赛一本通 · 提高篇(第一版)》
最近公共祖先 lca (施工ing)的更多相关文章
- Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集)
Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集) Description sideman做好了回到Gliese 星球的硬件准备,但是sideman的导航系统还没有完全设计好.为 ...
- POJ 1470 Closest Common Ancestors(最近公共祖先 LCA)
POJ 1470 Closest Common Ancestors(最近公共祖先 LCA) Description Write a program that takes as input a root ...
- POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA)
POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA) Description A ...
- [模板] 最近公共祖先/lca
简介 最近公共祖先 \(lca(a,b)\) 指的是a到根的路径和b到n的路径的深度最大的公共点. 定理. 以 \(r\) 为根的树上的路径 \((a,b) = (r,a) + (r,b) - 2 * ...
- 【lhyaaa】最近公共祖先LCA——倍增!!!
高级的算法——倍增!!! 根据LCA的定义,我们可以知道假如有两个节点x和y,则LCA(x,y)是 x 到根的路 径与 y 到根的路径的交汇点,同时也是 x 和 y 之间所有路径中深度最小的节 点,所 ...
- POJ 1470 Closest Common Ancestors (最近公共祖先LCA 的离线算法Tarjan)
Tarjan算法的详细介绍,请戳: http://www.cnblogs.com/chenxiwenruo/p/3529533.html #include <iostream> #incl ...
- 【Leetcode】查找二叉树中任意结点的最近公共祖先(LCA问题)
寻找最近公共祖先,示例如下: 1 / \ 2 3 / \ / \ 4 5 6 7 / \ ...
- 最近公共祖先LCA(Tarjan算法)的思考和算法实现
LCA 最近公共祖先 Tarjan(离线)算法的基本思路及其算法实现 小广告:METO CODE 安溪一中信息学在线评测系统(OJ) //由于这是第一篇博客..有点瑕疵...比如我把false写成了f ...
- 查找最近公共祖先(LCA)
一.问题 求有根树的任意两个节点的最近公共祖先(一般来说都是指二叉树).最近公共祖先简称LCA(Lowest Common Ancestor).例如,如下图一棵普通的二叉树. 结点3和结点4的最近公共 ...
随机推荐
- olivehc--百度开源的cdn cache
github 地址:http://git.baidu.com/olivehc/olivehc 主要是为了方便管理,百度cdn承载了全百度40%的流量,但是cdn团队只有几个人(一次培训中提到只有4个) ...
- 深入剖析php执行原理(4):函数的调用
本章开始研究php中函数的调用和执行,先来看函数调用语句是如何被编译的. 我们前面的章节弄明白了函数体会被编译生成哪些zend_op指令,本章会研究函数调用语句会生成哪些zend_op指,等后面的章节 ...
- js字符串和数组
sustr substring slice的联系与区别 str.substr(2,5) //从索引2开始截取5个字符,原有字符串str不变 str.substring(2,5) //从索引2开始截 ...
- 关于Mysql查询varchar类型错误问题
因为后台所有表ID都是按照雪花算法生成的18位数字,需要对接到Android,Ios和H5,此时H5会出现字符超长溢出,所以直接把ID改为varchar类型. 如我的一张表ID为varchar(18) ...
- P1081 开车旅行
题目描述 小 A 和小 B 决定利用假期外出旅行,他们将想去的城市从 1 到 N 编号,且编号较小的 城市在编号较大的城市的西边,已知各个城市的海拔高度互不相同,记城市 i 的海拔高度为 Hi,城市 ...
- UVA10125 Sumsets
嘟嘟嘟 很简单的折半搜索. 把式子变一下型,得到\(a + b = d - c\). 然后枚举\(a, b\),存到\(map\)里,再枚举\(c, d\)就好了. \(map\)以\(a,b\)两数 ...
- [转]MBTiles移动存储简介
首先奉上官网地址http://mapbox.com/developers/mbtiles/#storing_tiles 由于英文水平有限,看资料很费眼睛,特将它翻译成中文 存储瓦片 地图制作者面对一个 ...
- [转].NET设计模式系列文章
最初写探索设计模式系列的时候,我只是想把它作为自己学习设计模式的读书笔记来写,可是写到今天,设计模式带给我的震撼,以及许多初学者朋友的热心支持,让我下定决心要把这个系列写完写好,那怕花上再多的时间也无 ...
- Springboot时间参数格式化
@Configuration public class DateTimeFormatConfiguration extends WebMvcConfigurerAdapter { @Value(val ...
- vue每次请求加头部(shiro+vue)
前后台分离,全局请求加头部 设置全局请求为ajax请求 _axios.interceptors.request.use( function(config) { var accessToken = lo ...