zoukankan      html  css  js  c++  java
  • LCA(学习笔记)

    LCA指的是最近公共祖先,更具体的意义就不讲了.

    求解LCA的方法有很多,这里讲解向上标记法,树上倍增法,tarjan求LCA.

    向上标记法

    1 从x向上走到根节点,并标记所有经过的节点.

    2 从y向上走到根节点,第一次遇到的已标记的节点就是x和y的LCA.

    但不难发现,这个算法只适用于求一个点和一些点之间的LCA,不支持询问任意两个点的LCA.而且对于每个询问时间复杂度最坏为O(N).

    这种不太好的方法,本蒻就不贴代码了(懒)

    树上倍增法

    首先我们考虑对于两个点,求它们的LCA,首先最暴力的方法就是两个点同时一步一步往上跳,直到跳到同一个点(当然,在此之前先要跑一遍DFS预处理出每个点的深度,不然哪里来的"树上")(当然我们也可以BFS求)

    这样的暴力正确性是显然地,但慢就慢在它(像蜗牛一样)一步一步往上爬,而树上倍增法恰好弥补了这个不足,它能够一次向上跳多步,从而极大地提高了时间效率.

    预处理

    (f[x,k])表示(x)(2^k)辈祖先,即从(x)向根节点走(2^k)步到达的节点.显然,(f[x][0])就是(x)的父节点.

    因为(x)向根节点走(2^k)步等价于向根节点先走(2^{k-1})步,再走(2^{k-1})步,所以有(f[x][k]=f[f[x][k-1]][k-1]).(注意,这里是整个算法的核心思想,也是倍增的核心思想,一定要理解)

    我们先对树进行DFS遍历,得到每个节点的深度,即得到(f[x][0]),再计算(f)数组的所有值.

    void deal_first(int u,int father){
        deep[u]=deep[father]+1;
        for(int i=0;i<=19;i++)
    		f[u][i+1]=f[f[u][i]][i];
    //2^20次方已经足够满足很多题目的数据了,int才2^31
    //这个核心思想只稍微变了一下,认出来了吧
        for(int i=first[u];i;i=next[i]){
    		int v=to[i];
    		if(v==father)continue;
    		f[v][0]=u;
    		deal_first(v,u);
        }
    //枚举与当前遍历的点u所有相邻的点
    //因为是DFS遍历,所以u是v的父亲结点
    //因为u的父亲节点也与u相邻,注意忽略掉
    }
    

    查询

    int LCA(int x,int y){
        if(deep[x]<deep[y])swap(x,y);
    //让x点的深度较大,方便后面的操作
    //用数学语言来讲就是不妨设deep[x]>deep[y];
        for(int i=20;i>=0;i--){
    		if(deep[f[x][i]]>=deep[y])
        		x=f[x][i];
    		if(x==y)return x;
        }
    //先将x,y跳到同一个深度
    //注意这里一定要倒着for
    //特判如果此时x=y,LCA就是x节点.
        for(int i=20;i>=0;i--)
    		if(f[x][i]!=f[y][i]){
    	    	x=f[x][i];
    	    	y=f[y][i];
    		}
    //这里也要倒着for,显然是提高时间效率
    //一次跳得越多越好嘛
    //因为可能无法满足跳一次就找到了LCA,所以就不同才跳
    //又因为是不同才跳,所以要倒着for(我的理解)
        return f[x][0];
    //因为我们之前是不同才往上跳
    //所以最后x节点的父亲节点就是LCA
    }
    
    

    tarjan求LCA

    tarjan求LCA本质上就是向上标价法的并查集优化,知道我为什么上面要简述一个没用的方法的良苦用心了吧.

    这个方法最大的特点是,它将询问离线,统一计算.记得我上面评价向上标记法"不难发现,这个算法只适用于求一个点和一些点之间的LCA".tarjan求LCA就是把询问离线后,求出一个点到询问的另一些点的LCA

    在深度优先遍历(tarjan其实就是在DFS的同时记录一些有用的信息)的时候,

    我们把还没有访问的节点标记为0;

    正在访问的节点(访问过但是还没有访问完:假设现在访问x节点,则x以及x的祖先节点都是正在访问的节点)标记为1;

    已经全部访问完的节点标记为2;

    再再再回顾一下向上标记法:

    1 从x向上走到根节点,并标记所有经过的节点.

    2 从y向上走到根节点,第一次遇到的已标记的节点就是x和y的LCA.

    对于正在访问的节点x,它到根节点的路径上的点都是1号点(上面说了正在访问的x和x的祖先都会是1号点啊).如果节点y是已经访问完毕的节点(即标记为2的点),则LCA(x,y)就是从y向上走到根,第一个遇到的标记为1的节点.(我刚开始也不懂这里,直到我回顾了向上标记法...)

    显然算法到这里讲了这么多,但tarjan求LCA相比向上标记法还是没有任何优化.我们需要借助路径压缩的并查集来优化.

    对于一个已经访问完的节点y(即标记为2),我们可以把它(所在集合)合并到它的父节点(所在集合).(合并时,它的父亲结点标记一定是1).

    合并之后,我们只需要get节点y(所在集合)的代表元素,就相当于从节点y开始一直向上走,直到一个标记为1的节点.(就是说按照上述把y(标记为2)合并到它的父节点(此时标记为1)后,如果父节点也访问完毕,则把y的父节点也标记为2,继续向上走,直到一个合并完之后仍标记为1的节点)

    这个节点在y节点get向上合并之后仍被标记为1,说明它的另一颗子树中一定包括了x节点,故这个节点就是LCA(x,y);

    int n,m,s,tot;
    int v[500005],fa[500005],d[500005];
    int ans[500005];
    int to[1000005],nxt[1000005],head[1000005];
    vector<int> query[500005],query_id[500005];
    void add(int x,int y){
        to[++tot]=y;
        nxt[tot]=head[x];
        head[x]=tot;
    }//数组模拟链式前向星存图
    void add_query(int x,int y,int id){
        query[x].push_back(y);
        query_id[x].push_back(id);
        query[y].push_back(x);
        query_id[y].push_back(id);
    }
    //存下每一个询问,两个节点都存一次
    int get(int x){
        if(x==fa[x])return x;
        return fa[x]=get(fa[x]);
    }//并查集的路径压缩操作
    void tarjan(int x){
        v[x]=1;
    //x为正在访问的点,标记为1
        for(int i=head[x];i;i=nxt[i]){
    		int y=to[i];
    		if(v[y])continue;
    		tarjan(y);
    		fa[y]=x;
        }
    //扫描与x点相连的每一条边
        for(int i=0;i<query[x].size();i++){
    		int y=query[x][i],id=query_id[x][i];
    		if(v[y]==2){
    	    	int lca=get(y);
    	    	ans[id]=lca;
    		}	
        }
        v[x]=2;
    //x已经全部处理完了,标记为2
    }
    int main(){
        n=read();m=read();s=read();
    //s表示根节点
        for(int i=1;i<=n;i++)fa[i]=i;
    //一定要记得并查集初始化
        for(int i=1;i<=n-1;i++){
    		int x,y;x=read();y=read();
    		add(x,y);add(y,x);
        }
    //存边
        for(int i=1;i<=m;i++){
    		int x,y;x=read();y=read();
    		if(x==y)ans[i]=x;
    		else add_query(x,y,i);
        }
    //把每个问题都放入vector中,转为离线求解LCA
    //为了最后输出,别忘了把问题编号
        tarjan(s);
    //从根节点开始tarjan,如果没有根节点就任意一个点
        for(int i=1;i<=m;i++)
    		printf("%d
    ",ans[i]);
        return 0;
    }
    
    
  • 相关阅读:
    只有经历过,才知道苦痛的来源,而时间,将是唯一验证坚持是否值得的标准
    java返回值是list的时候获取list的参数类型
    spring4整合xfire1.2.6的问题解决
    基于tomcat插件的maven多模块工程热部署(附插件源码)
    新鲜出炉的jquery fileupload 插件
    WEB应用打成jar包全记录
    activiti5.14版本在线流程设计器的国际化中文支持
    activiti获取可回退的节点
    园子里的生活
    2022届我校高二下半期理科12题
  • 原文地址:https://www.cnblogs.com/PPXppx/p/10161693.html
Copyright © 2011-2022 走看看