用寻找公共祖先,祖先即为父节点的父节点的父节点......一路到根节点都算祖先
本文记录了两种方法,分别为倍增求和tarjan离线求
1倍增法
1利用了倍增思想,存储i点往上2^j的祖先
2求x,y两点的最近公共祖先,若x和y之间没有祖先关系(比如x是y的祖先),先让xy到深度相同的地方,然后一并向上移动,最终会到达公共祖先的下方。
代码实现
1数据存储
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
vector<int>vv[maxn];//记录图
inline void add(int u,int v)
{
vv[u].push_back(v);
vv[v].push_back(u);
}
int n;
int dep[maxn];//点的深度
int fa[maxn][22];//第i个点向上2^j个的祖先
2初始化
void init()
{
for(int i=1;i<=n;i++)
{
vv[i].clear();
dep[i]=0;
fa[i][0];
}
}
3dfs预处理,获取每个点的父节点和深度
void dfs(int u,int fat)
{
fa[u][0]=fat;
dep[u]=dep[fat]+1;
for(int i=0;i<vv[u].size();i++)
{
int to=vv[u][i];
if(to==fat)
continue;
dfs(to,u);
}
}
4倍增的预处理
void pre_work()
{
for(int j=1;j<=20;j++)
for(int i=1;i<=n;i++)
fa[i][j]=fa[fa[i][j-1]][j-1];//第i个点向上2^j的祖先是“第i个点向上2^(j-1)的祖先”的向上
//2^j的祖先
}
5lca求最近公共祖先
int lca(int x,int y)
{
if(x==y)//自己是自己的祖先
return x;
if(dep[y]>dep[x])//保证x是最深的那个
swap(x,y);
for(int i=20;i>=0;i--)
if(fa[x][i]&&dep[fa[x][i]]>=dep[y])//如果x能向上爬并且不会爬到y的上方,那么就跳跃
x=fa[x][i];
//到x与y深度相等
if(x==y)//如果x等于y说明y是x的祖先
return x;
//如果不是,两个点一起向上爬,但是保证不能相遇
//因为不能确定是否是最近的公共祖先
for(int i=20;i>=0;i--)
{
if(fa[x][i]&&fa[y][i]&&fa[x][i]!=fa[y][i])
{
x=fa[x][i];
y=fa[y][i];
}
}
//最终x会到达最近公共祖先的下方
return fa[x][0];
}
2tarjan离线求法
1需要用到并查集
2将询问记录成图的样子,用边代表询问,记录起点终点以及询问的编号,主要方便存储
3在dfs遍历中,某个节点遍历了他的所有自己子节点之后,开始查找询问,假设存在与该点相关的询问,存在几种情况:
(1)询问的另外一个点是他的子树中的一点(肯定被访问过了),此时询问的答案就是当前点u。
(2)询问点还没有被访问过,说明当前点和另外一点处于所查询的最近公共祖先的两个不同的子树,那么就放弃查询,等遍历到另外一点时再查询
(3)如果询问的另外点被访问了,那么就寻找另外一点现在正处于并查集中哪个集合,答案就是这个集合的源头
实际上(1)(3)说的应该是同一种情况
代码实现
1数据存储
const int maxn=5e5+10;
struct node//存储询问
{
int to,id;//to代表询问to和该点的公共祖先,id代表询问编号
};
vector<int>vv[maxn];//存储图
vector<node>q[maxn];//将询问存储成图
int vis[maxn];//标记是否被访问过
inline void add(int u,int v)//在图中添加无向边
{
vv[u].push_back(v);
vv[v].push_back(u);
}
inline void add_query(int u,int v,int id)//添加询问
{
q[u].push_back({v,id});//建立双向关系,保证后遍历的点能得到当前询问的答案
q[v].push_back({u,id});
}
int fa[maxn];//并查集
int ans[maxn];//记录答案
2并查集
int find(int x)//寻找属于哪个祖先
{
return x==fa[x]?x:fa[x]=find(fa[x]);
}
inline void merge(int x,int y)//并查集合并
{
x=find(x);
y=find(y);
if(x!=y)
fa[x]=y;
}
3tarjan遍历
void tarjan(int u,int fat)
{
for(int to:vv[u])
{
if(vis[to]||fat==to)
continue;
tarjan(to,u);//先遍历子节点再合并,因为如果某个查询中的两点都处于该子树中,先合并的话会影响答案
merge(to,u);
vis[to]=1;
}
for(int i=0;i<q[u].size();i++)
{
int to=q[u][i].to;
int id=q[u][i].id;
if(vis[to]==1)//如果与之相连的点访问过了,可以求得答案
ans[id]=find(to);
if(to==u)
ans[id]=u;
//如果to没有访问过,那么到访问到to时再查找答案
}
}
4主函数应用
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n,m,root;
cin>>n>>m>>root;//n个节点,m次查询,以root为根
for(int i=1;i<=n;i++)//并查集初始化
fa[i]=i;
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add(u,v);
}
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
add_query(u,v,i);
}
tarjan(root,0);
for(int i=1;i<=m;i++)
cout<<ans[i]<<'\n';
return 0;
}