标签:操作 eal 路径压缩 dfs lin i+1 节点 核心 枚举
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本质上就是向上标价法的并查集优化,知道我为什么上面要简述一个没用的方法的良苦用心了吧.
这个方法最大的特点是,它将询问离线,统一计算.记得我上面评价向上标记法"不难发现,这个算法只适用于求一个点和一些点之间的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\n",ans[i]);
return 0;
}
标签:操作 eal 路径压缩 dfs lin i+1 节点 核心 枚举
原文地址:https://www.cnblogs.com/PPXppx/p/10161693.html