最近公共祖先 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的最近公共 ...
随机推荐
- mysql 基础学习2
1.修改表字段顺序 在 字段增加和修改语法(ADD/CHANGE/MODIFY)中,都有一个可选项first|after column_name,这个选项可以用来修改字段在表中的位置 默认ADD增加的 ...
- scala当中的类
1.类的定义与创建 创建一个scala class来定义我们的一个类.类当中可以定义各种属性或者方法,或者函数都可以 class Person { //定义一个属性,叫做name的 ...
- 英语的各种 n. adj. vt. vi. 等词性解释
n. 名词 v. 动词(既可作及物动词,也可作不及物动词的就用这个表示) pron. 代词 adj. 形容词(后接名词) adv. 副词(修饰动词.形容词或其他副词) abbr. (这是一个缩写符号) ...
- Spark Streaming和Kafka集成深入浅出
写在前面 本文主要介绍Spark Streaming基本概念.kafka集成.Offset管理 本文主要介绍Spark Streaming基本概念.kafka集成.Offset管理 一.概述 Spar ...
- domain是什么
一:domain表达式 domain表达式:通常用来筛选数据记录.它们使用特殊的语法,以便于Odoo ORM 将它们解析后生成对应的SQL WHERE数据库筛选语句. 二:domain的写法 doma ...
- canvas二三事之签名板与视频绘制
今天,不知道怎么的就点开了语雀,然后就看到了<HTML5 Canvas 教程>,开始了canvas的研究(学习)之旅. 首先,想到的第一个东西就是签名板,上代码: <canvas i ...
- Gradle Goodness: Excluding Tasks for Execution
In Gradle we can create dependencies between tasks. But we can also exclude certain tasks from those ...
- Linux---关闭Elasticsearch进程,并重新启动
有时候,当我们启动elasticsearch之后, 经过很长一段时间没有操作, 自己已经忘了是否已经启动了elasticsearch, 这时候我们可以通过下面的方式验证是否启动,并重新启动: step ...
- Linux-- 查看文件 cat/tac
cat 查看文件 用法:cat 文件名 1.显示非空白行行号 -b 2.显示所有行号 -n 3.将[Tab]按键以 ^T 显示出来 -T 4.显示出特殊字符 -V 5.将结尾的断行字符$显示出来 -E ...
- 升级MAC OS到10.13, 10.14系统后UNITY工程无法加载资源的解决办法
升级MAC OS到10.13, 10.14系统后,出现UNITY工程无法加载资源的情况: Unity项目中Asset目录显示为空! 解决办法一: 打开Launchpad中的磁盘工具 (也就是实用工具下 ...