声明


  咳咳,进入重难点的图论算法之一 : 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 步,若 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)的更多相关文章

  1. Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集)

    Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集) Description sideman做好了回到Gliese 星球的硬件准备,但是sideman的导航系统还没有完全设计好.为 ...

  2. POJ 1470 Closest Common Ancestors(最近公共祖先 LCA)

    POJ 1470 Closest Common Ancestors(最近公共祖先 LCA) Description Write a program that takes as input a root ...

  3. POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA)

    POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA) Description A ...

  4. [模板] 最近公共祖先/lca

    简介 最近公共祖先 \(lca(a,b)\) 指的是a到根的路径和b到n的路径的深度最大的公共点. 定理. 以 \(r\) 为根的树上的路径 \((a,b) = (r,a) + (r,b) - 2 * ...

  5. 【lhyaaa】最近公共祖先LCA——倍增!!!

    高级的算法——倍增!!! 根据LCA的定义,我们可以知道假如有两个节点x和y,则LCA(x,y)是 x 到根的路 径与 y 到根的路径的交汇点,同时也是 x 和 y 之间所有路径中深度最小的节 点,所 ...

  6. POJ 1470 Closest Common Ancestors (最近公共祖先LCA 的离线算法Tarjan)

    Tarjan算法的详细介绍,请戳: http://www.cnblogs.com/chenxiwenruo/p/3529533.html #include <iostream> #incl ...

  7. 【Leetcode】查找二叉树中任意结点的最近公共祖先(LCA问题)

    寻找最近公共祖先,示例如下: 1 /           \ 2           3 /    \        /    \ 4    5      6    7 /    \          ...

  8. 最近公共祖先LCA(Tarjan算法)的思考和算法实现

    LCA 最近公共祖先 Tarjan(离线)算法的基本思路及其算法实现 小广告:METO CODE 安溪一中信息学在线评测系统(OJ) //由于这是第一篇博客..有点瑕疵...比如我把false写成了f ...

  9. 查找最近公共祖先(LCA)

    一.问题 求有根树的任意两个节点的最近公共祖先(一般来说都是指二叉树).最近公共祖先简称LCA(Lowest Common Ancestor).例如,如下图一棵普通的二叉树. 结点3和结点4的最近公共 ...

随机推荐

  1. 在Windows10中破解一些软件

    在Windows10中破解一些软件 一.前言   以前的windows是很好破解的,这里说的windows包含了windows的一些产品,比如说office,visio等等,可是自从到了新版的wind ...

  2. 当有多个相同的DIV时,我怎么判断我点击的是哪个嘞

    链接:https://segmentfault.com/a/1190000003480973?utm_source=tuicool&utm_medium=referral 序言 这是我曾经面试 ...

  3. Cookies的各方面知识(基础/高级)深度了解

    Cookies想必所有人都了解本文将围绕Cookies基础知识(什么是Cookies/Cookies如何传递/Cookies如何存储/Cookies如何查看)Cookies高级知识/Cookie的限制 ...

  4. day13 多线程建立方法

    #01创建多线程    继承Thread类    覆盖run方法:run方法里面运行要执行的代码    创建对象    调用start方法,start方法会开启线程,然后调用run方法 获取线程名字: ...

  5. Git版本控制 备忘录

    安装Git: 在Linux上安装Git: sudo apt-get install git 在windows上安装Git: 从https://git-for-windows.github.io下载,然 ...

  6. 【JavaScript】explode动画

    这是一个js实现的粒子聚合文字或图片的动画特效 部分程序如下 n.container = n.container[0] || n.container; /*有且仅有一个container*/ var ...

  7. linq中当生成asp.net实体模式时

    linq中当生成asp.net实体模式时 注意: 选中 工具->库程序包管理器->管理解决方案的nuget程序包  选中下面的进行下载.

  8. robotframwork的WEB功能测试(一)—切换window窗口

    selenium2library提供的切换到新窗口的关键字,只有select window,而且也只能根据title.name.url去定位.如下图所示,明显在实际使用中是不够的. 所以这里总结了一下 ...

  9. SpringBoot实战(五)之Thymeleaf

    Thymeleaf同jsp.volocity.freemarker等共同的职能是MVC模式中的视图展示层,即View. 当然了,SpringBoot中也可以用jsp,不过不推荐这种用法,比较推崇的就是 ...

  10. 近十年one-to-one最短路算法研究整理

    前言:针对单源最短路算法,目前最经典的思路即标号算法,以Dijkstra算法和Bellman-Ford算法为根本演进了各种优化技术和算法.针对复杂网络,传统的优化思路是在数据结构和双向搜索上做文章,或 ...