• LCA问题


    前言

    我们的LCA是谁呢...


    正文

    LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。 ———摘自百度百科

    什么意思呢?上图

    如你所见,这就是一棵超级难看的树了!

    在这棵树中,(3,4)的LCA就是(2)(8,7)的LCA就是(7)

    了解了LCA的概念之后,就要来思考怎么来求了。

    在初学OI之时,我就已经见过LCA的板子题了,但是它的点数只有不到1k,所以我们考虑使用暴力解法,思路如下:

    对于第一个点,可以一直递归的访问父亲结点直到根结点,用一个(set)来记录第一个点到根节点路径上的点。然后对于第二个点,也递归的访问父亲结点,遇到的第一个在(set)中出现过的点就是他们的公共祖先。


    怎么样,简单吗?

    如果点数与边数的范围都到了50W,还能用暴力解法进行解决吗?显然不行。暴力解法的时间复杂度过高,达到了(O(n^2)),每次进行的查询太花费时间,怎么办?

    回想一下暴力算法的步骤,跳跃深度较低的点到根节点并记录,跳跃深度较高的点并查询。这其中跳跃时的步长都是1,所以花费的时间过多。我们可以从此处着手进行优化。

    倍增算法应运而生。

    这又是一个跟(2)脱不了干系的算法,所谓倍增,就是要以2的倍数进行操作。即每次跳动的步长都以2的倍数来增大。即每次的步长为:(1,2,4,8,16,32,...)

    这样做又会出现一个问题。假如我需要跳动的步长为5,我会选择先跳1,再跳2,但是4又高了,需要退回2步。这样出现的“反悔”的跳法是极其浪费时间和难以实现的。所以我们再对倍增进行一些优化吧。

    这次的优化,让跳跃的步长从大往下排列,即:(...,32,16,8,4,2,1)。这样的话,选择了一个数,剩下的部分也可以用同样的原理进行拆分,不会出现选多了的情况(只要选择的这个数不大于剩下的步数)

    以二进制再形象地说一次吧。

    ((5)_2=101)

    所以从低位开始的话,到第二位的时候是难以判断是否需要取的,因为1+2<5,看上去是可行的。但如果从高位开始,第二位的取舍就很清晰了,因为4+2>5,所以不选。


    经过这样的优化,跳跃的次数被大大的减少了,时间复杂度可以降为(O(nlogn))

    接下来就是代码实现的问题。

    想要实现这个算法,首先我们要记录各个点的深度和他们(2^i)级的的祖先,用数组(dep)表示每个节点的深度,(fa[i][j])表示节点(i)(2^j)级祖先。

    void dfs(int now,int last)
    {
    	dep[now]=dep[last]+1;
    	fa[now][0]=last;
    	for(int i=1;(1<<i)<=dep[now];i++)
    		fa[now][i]=fa[fa[now][i-1]][i-1];
    	for(int i=head[now];i;i=e[i].next)
    		if(e[i].t!=last)dfs(e[i].t,now);
    }
    

    然后就是找LCA。

    每次跳跃步长的起始指数用(log2(n)),可以提前处理一下。

    for(int i=1;i<=n;i++)
        p[i]=p[i-1]+(1<<p[i-1]==i);
    

    接下来就是倍增LCA了,我们先把两个点提到同一高度,再统一开始跳。

    但我们在跳的时候不能直接跳到它们的LCA,因为这可能会误判。找到的虽然是公共祖先,但不一定最近。

    所以我们只跳到LCA的下面一层,再输出它们的父亲就可以了。

    int lca(int x,int y)
    {
    	if(dep[x]<dep[y])swap(x,y);
    	while(dep[x]>dep[y]){x=fa[x][p[dep[x]-dep[y]]-1];}
    	if(x==y)return x;
    	for(int k=p[dep[x]];k>=0;k--)
    		if(fa[x][k]!=fa[y][k])
    			x=fa[x][k],y=fa[y][k];
    	return fa[x][0];
    }
    

    ov.


    完整代码

    #include<bits/stdc++.h>
    using namespace std;
    int read()
    {
       int x=0,f=1;
       char ch=getchar();
       while(ch<'0'||ch>'9'){
           if(ch=='-')
               f=-1;
           ch=getchar();
       }
       while(ch>='0'&&ch<='9'){
           x=(x<<1)+(x<<3)+(ch^48);
           ch=getchar();
       }
       return x*f;
    }
    vector<int> g[500010];
    int dep[500010],fa[500010][25],head[500010],tot,p[500010];
    struct node{int t,next;}e[500010<<1];
    void add(int u,int v){e[++tot]=(node){v,head[u]},head[u]=tot;}
    void dfs(int now,int last)
    {
    	dep[now]=dep[last]+1;
    	fa[now][0]=last;
    	for(int i=1;(1<<i)<=dep[now];i++)
    		fa[now][i]=fa[fa[now][i-1]][i-1];
    	for(int i=head[now];i;i=e[i].next)
    		if(e[i].t!=last)dfs(e[i].t,now);
    }
    int lca(int x,int y)
    {
    	if(dep[x]<dep[y])swap(x,y);
    	while(dep[x]>dep[y]){x=fa[x][p[dep[x]-dep[y]]-1];}
    	if(x==y)return x;
    	for(int k=p[dep[x]];k>=0;k--)
    		if(fa[x][k]!=fa[y][k])
    			x=fa[x][k],y=fa[y][k];
    	return fa[x][0];
    }
    int main()
    {
    	int n,m,s=1/*根节点*/,x,y;
    	cin>>n>>m;
    	for(int i=1;i<n;i++)
    	{
    		x=read(),y=read();
    		add(x,y);
    		add(y,x);
    	}
    	dfs(s,0);
    	for(int i=1;i<=n;i++)
    		p[i]=p[i-1]+(1<<p[i-1]==i);
    	for(int i=1;i<=m;i++)
    	{
    		x=read(),y=read();
    		printf("%d
    ",lca(x,y));
    	}
    	return 0;
    }
    
  • 相关阅读:
    与eolinker api集成
    为什么要用eolinker帮助API开发
    什么是Eolinekr
    使用Eolinker加快您的API开发
    java反射
    mybatis-查询过程
    mybatis初始化过程
    mybatis的插件分析
    web.xml中的ContextLoaderListener和DispatcherServlet区别
    css+js杂记
  • 原文地址:https://www.cnblogs.com/moyujiang/p/12078989.html
Copyright © 2020-2023  润新知