最近公共祖先(LCA)
最近公共祖先(LCA)#
部分资料来自:
引入##
【例】给定一棵n(n <= 100000)个结点的树,以及m(m <= 100000)条询问(u, v),对于每条询问,求u和v的最短距离
常规而言,一无向图求两点间最短距离可以采用单源最短路,将时间复杂度大致控制在O(nlogn),但是当询问足够多的时候,这并不是一个高效的算法。
从树的性质可知,树上任意两点间有且仅有一条简单路径。要求u到v的距离,无非就是找到最近公共祖先(Lowest Common Ancestors),简称LCA。我们需要知道u->根节点的路径单链(u->r),v->根节点的路径单链(v->r),以及两点最近公共祖先路径a->根节点的路径单链(a->r),那么u->v的路径显然就可以通过以上三者的长度计算出来,令dist[x]表示从x到树根r的简单路径的长度,则u到v的距离可以表示成如下:dist[u] + dist[v] - dist[a]。
如何求LCA
1.朴素算法
最容易想到的办法是将u->r和v->r的两条路径通过递归生成出来,并且逆序保存,然后比较两条路径的公共前缀路径,显然公共前缀路径的尾结点就是u和v的最近公共祖先,但是这个算法在树退化成链的时候达到最坏复杂度O(n),并不可行。
而对于二叉查找树,他可以直接从根节点往下遍历,如果两个点一个在左边一个在右边,那么这个的就是最近公共祖先,如果都在左边,那么继续往左边遍历,如果都在右边,那么就往右边遍历。但前提他得是个二叉查找树呀。
2.步进法###
用depth[i]记录当前节点深度(根节点Depth是0),pre[i]表示节点i的父节点
-
当depth[u] < depth[v]时,lca(u, v) = lca(u, pre[v]);
-
当depth[u] > depth[v]时,lca(u, v) = lca(pre[u], v);
3.记忆化步进法###
【例】给定网络拓扑树,两个客户端(x, y),需要找到一个离他们最近的服务器来处理两个客户端的交互。
客户端和服务端数量小于等于1000,但是询问数Q <= 10^8。
询问很多,树结点较少,在步进法计算LCA的时候,计算lca(u, v)的时候可能在某次查询的时候已经被计算过了,这个时候加上记忆化就好了,但是空间开销很大。
4.使用倍增优化步增法(强制在线)###
我们首先要记录下每个节点的父节点和各个祖节点,可以开一个数组fa[x][i]表示节点x往上跳2^i步的点,也就是说x的父亲节点就是fa[x][0],爷爷就是fa[x][1](亦是父亲的父亲fa[fa[x][0]][0])……
这样在更新的时候我们就可以得到一个递推式:
fa[x][i] = fa[fa[x][i - 1]][i - 1]
那么它优于步增的地方就在于它往上跳跃的数值更大
(1)两者不同层,跳log(Depth[x]-Depth[y])/log2,使得两者同层
(2)同层后先特判x==y,否则不断倍增,一起往上跳,找到LCA的左右子点x,y
for(int i=log(Depth[x])/log2;i>=0;i--)
if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
那么LCA就是fa[x][0]
5.Tarjan(强制离线)###
Tarjan步骤(当dfs到节点u时):
1.初始化每个顶点u的祖先,即fa[u]=u(从属的并查集只有自己)
2.接下来对u的每个孩子v先进行递归处理
(1)Tarjan v
(2)合并v到父节点u的集合,更新v的祖先,即fa[v]=u
3.标记u为已遍历
4.处理关于u的查询,若查询(u,v)中的v已遍历过,则LCA(u,v)=v所在的集合的祖先,调用函数find_fa(v)
那么为什么我们可以这样做?
如图假设我们遍历完了10的孩子,现在要处理关于10的查询了
取根节点到当前正在遍历的节点的路径为关键路径,即1->3->8->10
集合的祖先便是这条关键路径上距离集合最近的点
比如此时:
1,2,5,6为一个集合,祖先为1,集合中任意一点和10的LCA为1
3,7为一个集合,祖先为3,集合中任意一点和10的LCA为3
8,9,11为一个集合,祖先为8,集合中任意一点和10的LCA为8
10,12为一个集合,祖先为10,集合中任意一点和10的LCA为10
抽象一点讲,就是说如果你要查找的两个点在某个根节点(LCA)的左右,那么当你开始访问此根节点的右子树时,左子树的任意一点与右子树要建立路径关系,那么它必须通过根节点,换言之,我们用根节点代表了整个左子树,成为一个所谓的集合,即并查集
也就是说,当前并查集的个数当前dfs的深度+1当前栈中的节点数
有几个注意的点:
1.使用set在储存查询内容的时候不妨同一对关系记录两次,set[a].insert(b),set[b].insert(a)
2.在进行查询使用find_fa()的时候,要注意进行状态压缩
例题##
1.洛谷P3379###
标准邻接表存图,dfs预处理倍增,快读优化,注意高效存储logN
#include<iostream>
#include<cstring>
#include<cstdio>
#define re register
using namespace std;
#define MAXN 500003
typedef long long int LL;
//快读,不然刚刚好T掉一个点~
inline int read()
{
re int t=0;
re char v=getchar();
while(v<'0')v=getchar();
while(v>='0')
{
t=(t<<3)+(t<<1)+v-48;
v=getchar();
}
return t;
}
struct edge//存边
{
int r,to;
};
edge e[MAXN<<1];
int head[MAXN];
int fa[MAXN][22];
int depth[MAXN];
int lg[MAXN];
int total=0;
void add(int x,int y)//存图,邻接表
{
e[++total].r=y;
e[total].to=head[x];
head[x]=total;
}
void dfs(int cur,int fapos)//dfs建立关系
{
fa[cur][0]=fapos;
depth[cur]=depth[fapos]+1;
for(int i=1;i<=lg[depth[cur]];i++)
{
fa[cur][i]=fa[fa[cur][i-1]][i-1];//这句话与ST表中核心关系的建立有异曲同工之妙
}
for(int i=head[cur];i;i=e[i].to)
if(e[i].r!=fapos)dfs(e[i].r,cur);//确立父子关系,注意怎么判断返回
}
int LCA(int a,int b)
{
//第一步,使两个点处到同一层
if(depth[a]<depth[b])swap(a,b);
while(depth[a]>depth[b])
a=fa[a][lg[depth[a]-depth[b]]];
if(a==b)return a;//特判
//第二步,向上寻找LCA直属左右孩子
for(int k=lg[depth[a]];k>=0;k--)
if(fa[b][k]!=fa[a][k])a=fa[a][k],b=fa[b][k];
return fa[b][0];
}
int main()
{
int n,m,root,a,b;
cin.tie(0);
ios::sync_with_stdio(false);
memset(head,0,sizeof(head));
n=read();
m=read();
root=read();
for(int i=0;i<n-1;i++)
{
a=read(),b=read();
add(a,b);
add(b,a);
}
//高效打表,如果每次暴力算log N / log 2,会重复好多计算
for(int i=1;i<=n+3; i++)
lg[i]=lg[i-1]+(1<<lg[i-1]==i),lg[i-1]--;//别忘了减1
dfs(root,0);//初始化fa
for(int i=1;i<=m;i++)
{
a=read(),b=read();
cout<<LCA(a,b)<<endl;
}
return 0;
}
2.洛谷P5836##
它的题意是求路径u-v上是否有颜色H或者G
我们记录从根节点到每个节点的路径上颜色为H/G的个数(树上前缀和即可)(这题有点毛病的就是他默认建树是从1开始)