LCA模板题https://www.luogu.com.cn/problem/P3379题意理解

对于有根树T的两个结点u、v,最近公共祖先LCA(u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能小。另一种理解方式是把T理解为一个无向无环图,而LCA(u,v)即u到v的最短路上深度最小的点。
例如,对于下面的树,结点4和结点6的最近公共祖先LCA(T,4,6)为结点2。

那么这里提供四种思路。

1.暴力至上主义

首先计算出结点u和v的深度d1和d2。如果d1>d2,将u结点向上移动d1-d2步,如果d1<d2,将v结点向上移动d2-d1步,现在u结点和v结点在同一个深度了。下面只需要同时将u,v结点向上移动,直到它们相遇(到达同一个结点)为止,相遇的结点即为u,v结点的最小公共祖先。
但这个算法对于多次询问的题目不能解决。

2.倍增法

思路:倍增法其实是在上一种方法的基础上进行了优化,我们希望向上查找更快,可以事先预处理出p[i,j],表示i往上移动2^j步到达的结点,这样在查找时将大大节约时间。利用P数组可以快速的将结点i向上移动n步,方法是将n表示为2进制数。比如n=6,二进制为110,那么利用P数组先向上移动4步(2^2), 然后再继续移动2步(2^1),即P[ P[i][2] ][1]。
那么首先深搜预处理出所有的p[i][j],并计算每个点的深度d[i]:

void dfs(int u,int fa)//从根结点u开始dfs,u的父亲是fa{
d[u]=d[fa]+; //u的深度为它父亲的深度+1
p[u][]=fa; //u向上走2^0步到达的结点是其父亲
for(int i=;(<<i)<=d[u];i++) p[u][i]=p[p[u][i-]][i-];
//预处理p时,保证能从u走到p[u][i]
for(int i=head[u];i!=-;i=e[i].next)//对于u的每个儿子{
int v=e[i].v;
if(v!=fa)dfs(v,u); //递归处理以v为根结点的子树}
}

然后在主函数中调用dfs(1,0)即可。接下来是查询结点a,b的LCA:

int lca(int a,int b)
{
if(d[a]>d[b]) swap(a,b) ; //保证a点在b点上面
for(int j=;j>=;j--) //将b点向上移动到和a的同一深度
if(d[a]<=d[b]-(<<j)) b=p[b][j] ;
if(a==b) return a ;//如果a和b相遇
for(int j=;j>=;j--)//a,b同时向上移动
{
if(p[a][j]==p[b][j]) continue ;//如果a,b的2^j祖先相同,则不移动
a=p[a][j],b=p[b][j];//否则同时移动到2^j处
}
return p[a][] ;//返回最后a的父亲结点
}

时间复杂度分析
预处理:对每一个结点找到它向上走2^logN步到达的点,所以时间是NlogN
一组询问复杂度:O(logn)。
所以总复杂度为NlogN+QlogN
空间复杂度:O(nlogn)。
完整代码:

#include<stdio.h>
#include<iostream>
#define maxn 500000
using namespace std; int n,m,s,k=;
int dep[maxn+],f[maxn+][];
int head[maxn+];
struct edge{int u,v,next;}e[maxn*+]; inline int read()
{
int s=,w=;
char ch=getchar();
while(ch<''||ch>''){if(ch=='-')w=-;ch=getchar();}
while(ch>=''&&ch<='') s=s*+ch-'',ch=getchar();
return s*w;
} void addedge(int u,int v)
{
e[++k].u=u;
e[k].v=v;
e[k].next=head[u];
head[u]=k;
} 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 i=head[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==father) continue;
f[v][]=u;
deal_first(v,u);
}
} int LCA(int x,int y)//查询x,y的LCA
{
if(dep[x]<dep[y]) swap(x,y);//保证x的深度更大
//先暴力,将x和y的深度调至一样
for(int i=;i>=;i--)//x先跳的幅度大一点
{
if(dep[f[x][i]]>=dep[y]) x=f[x][i];
//x向上移动,使得x,y在同一深度
if(x==y) return x;
//如果恰好x的祖先就是y,直接返回x,不过一般不可能
}
for(int i=;i>=;i--)//x,y同时向上移动,找公共祖先
if(f[x][i]!=f[y][i])//x,y的父亲不同才往上跳
x=f[x][i],y=f[y][i];
return f[x][];//最后x和y的父亲一样,返回x的父亲
} int main()
{
// ios::sync_with_stdio(false);
n=read(),m=read(),s=read();
int x,y;
for(int i=;i<=n-;i++)
{
x=read(),y=read();
addedge(x,y);
addedge(y,x);
}
deal_first(s,);//从根节点开始预处理
for(int i=;i<=m;i++)
{
x=read(),y=read();
printf("%d\n",LCA(x,y));
}
return ;
}

3.转为RMQ问题

思路:DFS遍历树T,将遍历到的结点按照顺序记下,得到一个长度为2N – 1的序列,称之为T的欧拉序列F。每个结点都在欧拉序列中出现,我们记录结点u在欧拉序列中第一次出现的位置为pos(u)。如下图:

根据DFS的性质,对于结点u、v,从pos(u)遍历到pos(v)的过程中经过LCA(u, v)至少一次,且是深度序列B[pos(u)…pos(v)]中值最小的元素在F中对应的节点。那么我们只需要去找在深度序列B中,pos(u)到pos(v)中最小的位置的欧拉序列值。
即LCA( u, v) = RMQ( pos(u), pos(v))。

用occur[]记录T的欧拉序列,depth[]记录深度,first[]记录各结点第一次出现在欧拉序列中的位置,深搜时:

void dfs(int u,int deep)
{
occur[++cnt]=u;//进入该点时进行记录
depth[cnt]=deep;
if(!first[u]) first[u]=cnt;
for(int i=head[u];i!=-;i=e[i].next)
{
dfs(e[i].v,deep+);
occur[++cnt]=u;//访问子树也要标记
depth[cnt]=deep;
}
}

在这之后还需要进行一次预处理,打一次ST表。
时间复杂度分析:dfs时间为2N,RMQ预处理时间为NlogN,询问Q次,所以总时间复杂度为NlogN+Q
完整代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<cstring>
#define maxn 500000
using namespace std; int n,m,s,cnt=,k=,head[maxn+];
struct edge{int u,v,next;}e[maxn*+];
int occur[maxn*+],depth[maxn*+],first[maxn+];
int minnum[maxn*+][],mindep[maxn*+][];
//mindep记录的是区间内深度最小值
//minnum记录的是区间内深度最小值的编号 inline int read()//快读
{
int s=,w=;
char ch=getchar();
while(ch<''||ch>''){if(ch=='-')w=-;ch=getchar();}
while(ch>=''&&ch<='') s=s*+ch-'',ch=getchar();
return s*w;
} void addedge(int x,int y)//建边
{
e[++k].u=x;
e[k].v=y;
e[k].next=head[x];
head[x]=k;
} void dfs(int u,int deep)//深搜预处理
{
cnt++;
occur[cnt]=u;
depth[cnt]=deep;
first[u]=cnt;//记录首次出现的位置
for(int i=head[u];i!=-;i=e[i].next)
if(!first[e[i].v])
{
dfs(e[i].v,deep+);//深搜该节点的枝
occur[++cnt]=u;//回溯后,仍要将该节点放入欧拉数列
depth[cnt]=deep;//记录深度
}
} void st()//st表求RMQ
{
for(int i=;i<=cnt;i++)
minnum[i][]=occur[i],mindep[i][]=depth[i];
for(int j=;j<=log(cnt)/log();j++){
for(int i=;i<=cnt-(<<j)+;i++)
{
if(mindep[i][j-]<mindep[i+(<<(j-))][j-])
mindep[i][j]=mindep[i][j-],minnum[i][j]=minnum[i][j-];
else
mindep[i][j]=mindep[i+(<<(j-))][j-],minnum[i][j]=minnum[i+(<<(j-))][j-];
}
}
} int main()
{
memset(head,-,sizeof(head));
n=read(),m=read(),s=read();
int x,y;
for(int i=;i<=n-;i++)
{
x=read(),y=read();
addedge(y,x);
addedge(x,y);
}
dfs(s,);
st();
for(int i=;i<=m;i++)
{
x=read(),y=read();
int op=min(first[x],first[y]),ed=max(first[x],first[y]);
int k=;
k=log(ed-op+)/log();
if(mindep[op][k]<mindep[ed-(<<k)+][k])
printf("%d\n",minnum[op][k]);
else printf("%d\n",minnum[ed-(<<k)+][k]);
}
return ;
}

4.Tarjan

别说了,说多了都是泪。思路:先把所有的询问存下来,然后在DFS的过程中根据点被访问的状态求出每个询问的答案,DFS完后按询问顺序输出。需要用到并查集。
相比前两个算法,这是离线算法。优势在于稳定,且时间复杂度适中。

实现过程
DFS遍历树,利用并查集,当某个节点u及其子树遍历完成后,处理所有与u相关的查询。
1)当遍历到u时,创建u的并查集U={u},集合U的代表为u
2)对u的每一个子树进行DFS遍历,每搜索完一棵子树,将子树标记为checked,把子树所形成的集合与集合U合并,且集合U的代表仍为u
3)按步骤2)继续遍历u的下一棵子树,直到u的所有子树都遍历完,此时将u设为checked
4)处理所有与u相关的查询(u,v)
若v的状态为checked,则LCA(u,v)=present(v)
若v的状态不是checked,跳过这个查询(u,v),当遍历到节点v时,u必然为checked,同样可以求得lca(u,v)

void tarjan(int u)
{
father[u]=u;//以u作为父亲建立一个独立并查集
vis[u]=true;
for(int i=head[u];i!=-;i=e[i].next)
{
int v=e[i].v;
if(!vis[v])
{
tarjan(v);
father[v]=u;
}
}
//深搜寻找叶子节点,再向上回溯
for(int i=;i<q[u].size();i++)
if(vis[q[u][i].first]&&!ans[q[u][i].second])
//如果这个点的并查集已被查询完,并且还没有得出答案
ans[q[u][i].second]=find(q[u][i].first);
//第i号询问就是q[u][i].first的父亲
//同时对并查集的祖先进行刷新
}

时间复杂度分析:Tarjan的时间为N,每个询问O(1),所以总时间为N+Q
完整代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#define maxn 500000
using namespace std; int n,m,s,cnt=,k=,head[maxn+];
struct edge{int u,v,next;}e[maxn*+];
int ans[maxn+],father[maxn+];
bool vis[maxn+];
typedef pair<int,int> pii;
vector<pii> q[maxn+]; inline int read()
{
int ans=,flag=;
char ch=getchar();
while(ch<''||ch>'')
{
if(ch=='-') flag=-;
ch=getchar();
}
while(ch>=''&&ch<='')
ans=ans*+ch-'',ch=getchar();
return ans*flag;
} void addedge(int u,int v)
{
e[++k].u=u;
e[k].v=v;
e[k].next=head[u];
head[u]=k;
} int find(int x){return father[x]==x?x:father[x]=find(father[x]);} void tarjan(int u)
{
father[u]=u;//以u作为父亲建立一个独立并查集
vis[u]=true;
for(int i=head[u];i!=-;i=e[i].next)
{
int v=e[i].v;
if(!vis[v])
{
tarjan(v);
father[v]=u;
}
}
//深搜寻找叶子节点,再向上回溯
for(int i=;i<q[u].size();i++)
if(vis[q[u][i].first]&&!ans[q[u][i].second])
//如果这个点的并查集已被查询完,并且还没有得出答案
ans[q[u][i].second]=find(q[u][i].first);
//第i号询问就是q[u][i].first的父亲
//同时对并查集的祖先进行刷新
} int main()
{
memset(head,-,sizeof(head));
n=read(),m=read(),s=read();
int u,v;
for(int i=;i<=n-;i++)
{
u=read(),v=read();
addedge(u,v);
addedge(v,u);
}
for(int i=;i<=m;i++)
{
u=read(),v=read();
q[u].push_back(make_pair(v,i));
q[v].push_back(make_pair(u,i));
}//不定长数组存储询问
tarjan(s);
for(int i=;i<=m;i++)
printf("%d\n",ans[i]);
return ;
}

【图论算法】LCA最近公共祖先问题的更多相关文章

  1. 『图论』LCA最近公共祖先

    概述篇 LCA(Least Common Ancestors),即最近公共祖先,是指这样的一个问题:在一棵有根树中,找出某两个节点 u 和 v 最近的公共祖先. LCA可分为在线算法与离线算法 在线算 ...

  2. 『图论』LCA 最近公共祖先

    概述篇 LCA (Least Common Ancestors) ,即最近公共祖先,是指这样的一个问题:在一棵有根树中,找出某两个节点 u 和 v 最近的公共祖先. LCA 可分为在线算法与离线算法 ...

  3. Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载)

    Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载) 转载自:http://hi.baidu.com/lydrainbowcat/blog/item/2 ...

  4. POJ 1986 Distance Queries (Tarjan算法求最近公共祖先)

    题目链接 Description Farmer John's cows refused to run in his marathon since he chose a path much too lo ...

  5. LCA近期公共祖先

    LCA近期公共祖先 该分析转之:http://kmplayer.iteye.com/blog/604518 1,并查集+dfs 对整个树进行深度优先遍历.并在遍历的过程中不断地把一些眼下可能查询到的而 ...

  6. LCA 近期公共祖先 小结

    LCA 近期公共祖先 小结 以poj 1330为例.对LCA的3种经常使用的算法进行介绍,分别为 1. 离线tarjan 2. 基于倍增法的LCA 3. 基于RMQ的LCA 1. 离线tarjan / ...

  7. lca 最近公共祖先

    http://poj.org/problem?id=1330 #include<cstdio> #include<cstring> #include<algorithm& ...

  8. LCA(最近公共祖先)模板

    Tarjan版本 /* gyt Live up to every day */ #pragma comment(linker,"/STACK:1024000000,1024000000&qu ...

  9. CodeVs.1036 商务旅行 ( LCA 最近公共祖先 )

    CodeVs.1036 商务旅行 ( LCA 最近公共祖先 ) 题意分析 某首都城市的商人要经常到各城镇去做生意,他们按自己的路线去做,目的是为了更好的节约时间. 假设有N个城镇,首都编号为1,商人从 ...

随机推荐

  1. 基于MySQL Binlog的Elasticsearch数据同步实践

    一.为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品.订单等数据的多维度检索. 使用 Elasticsearch 存储业务数 ...

  2. pytorch seq2seq闲聊机器人加入attention机制

    attention.py """ 实现attention """ import torch import torch.nn as nn im ...

  3. VUE前端项目配置代理解决跨域问题

    VUE前端项目配置代理解决跨域问题 问题如下,经常在本地调试接口出现这种问题 解决方式1:Chrome 的扩展插件 以前使用Chrome 的扩展插件,但是有时候还是会出现莫名其妙的问题. 需要梯子才行 ...

  4. 通过纯css实现图片居中的多种实现方式

    html结构: <div class="demo" style="width: 800px;height: 600px; border:1px solid #ddd ...

  5. 2019-2020-1 20199326《Linux内核原理与分析》第二周作业

    本周总结:本周的学习内容主要是庖丁解牛Linux的第一章,然后看完书后,又跟着云班课加深学习了一下第一章的内容.第一章主要讲述了linux里的汇编指令的一些指令,比如movl,pushl,popl等等 ...

  6. linux抓包的实现

    工具: wireshark tcpdump 在这里仅仅介绍后者: 在网络问题的调试中,tcpdump应该说是一个必不可少的工具,和大部分linux下优秀工具一样,它的特点就是简单而强大. 默认情况下, ...

  7. Web性能优化之-深入理解TCP Socket

    什么是Socket?    大家都用电脑上网,当我们访问运维社区https://www.unixhot.com的时候,我们的电脑和运维社区的服务器就会创建一条Socket,我们称之为网络套接字.那么既 ...

  8. Redis(一):数据结构与对象

    前言 本书是Redis设计与实现的读书笔记,旨在对Redis底层的数据结构及实现有一定了解.本书所有的代码基于Redis 3.0. 简单动态字符串 SDS Redis没有直接使用C语言中的字符串,而是 ...

  9. Netty(四):AbstractChannel源码解析

    首先我们通过一张继承关系的图来认识下AbstractChannel在Netty中的位置. 除了Comaprable接口来自java自带的包,其他都是Netty包中提供的. Comparable接口定义 ...

  10. 一个简易的SocketIM

    今天做了一个简易的socketIM的小示例.基本思想是开启两个winform,每个winform既充当服务器也充当客户端.一个监听8000端口,另外一个监听8001端口,两个winform接收到信息之 ...