LCA(学习笔记)

摘要:
LCA指的是最近公共祖先,更具体的意义就不讲了.求解LCA的方法有很多,这里讲解向上标记法,树上倍增法,tarjan求LCA.向上标记法1从x向上走到根节点,并标记所有经过的节点.2从y向上走到根节点,第一次遇到的已标记的节点就是x和y的LCA.但不难发现,这个算法只适用于求一个点和一些点之间的LCA,不支持询问任意两个点的LCA.而且对于每个询问时间复杂度最坏为O(N).这种不太好的方法,本蒻就

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;
}

免责声明:文章转载自《LCA(学习笔记)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇win 10 pip Could not find a version that satisfies the requirement selenium (from versions: ) No matching distribution found for seleniumRuby语法基础(二)下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

LCA(最近公共祖先)离线算法Tarjan+并查集

本文来自:http://www.cnblogs.com/Findxiaoxun/p/3428516.html 写得很好,一看就懂了。 在这里就复制了一份。 LCA问题: 给出一棵有根树T,对于任意两个结点u,v求出LCA(T, u, v),即离根最远的结点x,使得x同时是u和v的祖先。      把LCA问题看成询问式的:给出一系列询问,程序应当对每一个询...

LCA问题【RMQ+Tarjan】

LCA-求树上两点最近公共祖先问题 lrj的紫书上提供了一种将LCA问题转化为RMQ问题的方法,即dfs一次处理出一个序列,first(u)代表u第一次出现的下标,则对于u,v的最近公共祖先的下标即为RMQ(first(u), first(v))。 LCA->RMQ(在线处理): 1 #include<bits/stdc++.h> 2...

可持久化 trie 的简单入门

可持久化 $trie$ ....又是一个表里不一的东西..... 可持久化 $trie$ 的介绍: 和主席树类似的,其实可持久化就是体现在前缀信息的维护上(搞不懂这怎么就叫做可持久化了...) $trie$ (字典树)大家应该都知道,就是一棵用来做字符串匹配的树, 但是!在这里,可持久化 $trie$ 就是完全不一样的东西了... 基本上(我做过的题),可...

[2020牛客暑期多校训练营(第一场)虚树 Infinite Tree]

2020牛客暑期多校训练营(第一场)虚树 Infinite Tree 题解参考博客:https://blog.nowcoder.net/n/df889adfaf824d50ad2291f4d2eb04a2?&toCommentId=6480068 题目大意: 定义 (mindiv(n)) 是 (n) 最小的大于1的约数,对于每一个 (i),(1&l...

关于树上差分

关于树上差分,可参见网友一博客 https://www.luogu.com.cn/blog/sincereactor/shu-shang-ci-fen-di-liang-zhong-sai-lu 这里我也来说两句: 树上差分分为两个(就像树上dp一样) 1.点差分 2.边差分 1.点差分: 二.关于点的差分(如将路径上的所有点权值加一,求最后点的权值) 此...