最近公共祖先(LCA)

最近公共祖先(LCA)#

部分资料来自:

1.https://www.cnblogs.com/ECJTUACM-873284962/p/6613379.html

2.https://blog.csdn.net/WhereIsHeroFrom/article/details/78934384?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

3.http://scturtle.is-programmer.com/posts/30055.html


引入##

【例】给定一棵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的父节点

  1. 当depth[u] < depth[v]时,lca(u, v) = lca(u, pre[v]);

  2. 当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##

AC记录:https://www.luogu.com.cn/record/31552012

它的题意是求路径u-v上是否有颜色H或者G

我们记录从根节点到每个节点的路径上颜色为H/G的个数(树上前缀和即可)(这题有点毛病的就是他默认建树是从1开始)

原文地址:https://www.cnblogs.com/et3-tsy/p/12443196.html