【图论算法】LCA最近公共祖先问题
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最近公共祖先问题的更多相关文章
- 『图论』LCA最近公共祖先
概述篇 LCA(Least Common Ancestors),即最近公共祖先,是指这样的一个问题:在一棵有根树中,找出某两个节点 u 和 v 最近的公共祖先. LCA可分为在线算法与离线算法 在线算 ...
- 『图论』LCA 最近公共祖先
概述篇 LCA (Least Common Ancestors) ,即最近公共祖先,是指这样的一个问题:在一棵有根树中,找出某两个节点 u 和 v 最近的公共祖先. LCA 可分为在线算法与离线算法 ...
- Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载)
Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载) 转载自:http://hi.baidu.com/lydrainbowcat/blog/item/2 ...
- POJ 1986 Distance Queries (Tarjan算法求最近公共祖先)
题目链接 Description Farmer John's cows refused to run in his marathon since he chose a path much too lo ...
- LCA近期公共祖先
LCA近期公共祖先 该分析转之:http://kmplayer.iteye.com/blog/604518 1,并查集+dfs 对整个树进行深度优先遍历.并在遍历的过程中不断地把一些眼下可能查询到的而 ...
- LCA 近期公共祖先 小结
LCA 近期公共祖先 小结 以poj 1330为例.对LCA的3种经常使用的算法进行介绍,分别为 1. 离线tarjan 2. 基于倍增法的LCA 3. 基于RMQ的LCA 1. 离线tarjan / ...
- lca 最近公共祖先
http://poj.org/problem?id=1330 #include<cstdio> #include<cstring> #include<algorithm& ...
- LCA(最近公共祖先)模板
Tarjan版本 /* gyt Live up to every day */ #pragma comment(linker,"/STACK:1024000000,1024000000&qu ...
- CodeVs.1036 商务旅行 ( LCA 最近公共祖先 )
CodeVs.1036 商务旅行 ( LCA 最近公共祖先 ) 题意分析 某首都城市的商人要经常到各城镇去做生意,他们按自己的路线去做,目的是为了更好的节约时间. 假设有N个城镇,首都编号为1,商人从 ...
随机推荐
- Python中的可视化神器!你知道是啥吗?没错就是pyecharts!
pyecharts是一款将python与echarts结合的强大的数据可视化工具,本文将为你阐述pyecharts的使用细则 前言 我们都知道python上的一款可视化工具matplotlib,而前些 ...
- Spring Security 是如何在 Servlet 应用中执行的?
Spring Security 是一个强大的认证和授权框架,它的使用方式也非常简单,但是要想真正理解它就需要花一时间来学习了,最近在学习 Spring Security 时有一些新的理解,特意记录下来 ...
- Hbase的安装与基本操作
简介: 1安装 HBase 本节介绍HBase的安装方法,包括下载安装文件.配置环境变量.添加用户权限等. 1.1 下载安装文件 HBase是Hadoop生态系统中的一个组件,但是,Hado ...
- thinkphp5.0 url跳转
<a href="{:url('member/index/index',['id'=>5])}">跳转</a> define()自定义常量在thiin ...
- Floyd-Warshall算法正确性证明
以下所有讨论,都是基于有向无负权回路的图上的.因为这一性质,任何最短路径都不会含有环,所以也不讨论路径中包含环的情形!并且为避免混淆,将"最短路径"称为权值最小的路径,将路径经过的 ...
- java在指定区间内生成随机数
Random对象生成随机数 首先需要导入包含Random的包 import java.util.Random; nextInt(int)方法将生成0~参数之间的随机整数但不包括参数. 例如生成0~99 ...
- Java ArrayList工作原理及实现
http://yikun.github.io/2015/04/04/Java-ArrayList%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE% ...
- 墨仓式进入2.0时代?爱普生商用墨仓式L4158试用
提起"墨仓式"打印机,相信现在已经没有人需要过多的解释,墨仓式打印机在打印市场占有率不断提高就是最佳佐证.为什么用户对于墨仓式这么认可,想必是墨仓式真正洞悉了他们的需求,解决了打印 ...
- Linked List-1
链表一直是面试的重点问题,恰好最近看到了Stanford的一篇材料,涵盖了链表的基础知识以及派生的各种问题. 第一篇主要是关于链表的基础知识. 一.基本结构 1.数组回顾 链表和数组都是用来存储一堆数 ...
- HTML--HTML入门篇(我想10分钟入门HTML,可以,交给我吧)
我要正经的讲一节课,咳咳! HTML简介(废话) HTML称为超文本标记语言,是一种标识性的语言.它包括一系列标签.通过这些标签可以将网络上的文档格式统一,使分散的Internet资源连接为一个逻辑整 ...