zoukankan      html  css  js  c++  java
  • 最近公共祖先(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开始)

  • 相关阅读:
    1276
    1235
    1170
    Grids
    1137
    1127
    1076
    🍖Mysql三种模式的指定方式介绍
    🍖存储引擎介绍
    🍖MySQL库的详细操作
  • 原文地址:https://www.cnblogs.com/et3-tsy/p/12443196.html
Copyright © 2011-2022 走看看