首先是最近公共祖先的概念(什么是最近公共祖先?)(摘自https://www.cnblogs.com/JVxie/p/4854719.html):
在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大的公共的祖先节点。
换句话说,就是两个点在这棵树上距离最近的公共祖先节点。
所以LCA主要是用来处理当两个点仅有唯一一条确定的最短路径时的路径。
有人可能会问:那他本身或者其父亲节点是否可以作为祖先节点呢?
答案是肯定的,很简单,按照人的亲戚观念来说,你的父亲也是你的祖先,而LCA还可以将自己视为祖先节点。
那么求LCA有两种方法,一种是倍增,另一种是Tarjan(好吧,还有RMQ,只不过我不会)
这里以此题为例子
P3379 【模板】最近公共祖先(LCA)
简略的讲一下题目大意,在一个树中间,求出两个点之间的最小公共祖先
首先倍增
先讲一下暴力做法:1.求出两个点的深度,假设两个点分别是a,b ,深度为x,y,且x>y
2.将点a向上跳,知道跳到与b一个深度
3.做完第二步之后,将两个点一起往上跳,知道重合为止
4.那么这个点就是这两个点的LCA
这样做肯定是可以的,但是时间复杂度太高了,可以卡到n^2
那么我们可以把第二步优化,显然就用倍增(显然大法好)
先讲一下倍增的意思
有一个数字n,n=123
那么n=64+32+16+8+2+1=2^6+2^5+2^4+2^3+2^1+2^0
可以得出对于任何自然数n,都可以进行这样的拆分,因为可以看出就是二进制
回归正题
设一个数组f[ ][ ],f[i][j]表示i这个节点的2^j祖先
那么可以得出f[i][j]=f[f[i][j-1]][j-1]
因为f[i][j-1]表示i的2^j-1祖先,那么这个点的2^j-1祖先就是i的2^j祖先
运用BFS就可以求出深度(这个不用说吧……),再求出f的值(t为log2(n),因为这个点的最大祖先不会超过2^t)
void Bfs(){ q.push(root);deep[root]=1; while(!q.empty()){ int x=q.front();q.pop(); for(int i=head[x];i;i=next[i]){ int y=ver[i]; if(deep[y])continue; deep[y]=deep[x]+1; f[y][0]=x; for(int j=1;j<=t;j++)f[y][j]=f[f[y][j-1]][j-1]; q.push(y); } } }
既然已经得出f值,那么在运用常规方法
int Get_LCA(int x,int y){ if(deep[x]<deep[y])swap(x,y);//假设x深度大于y for(int i=t;i>=0;i--) if(deep[f[x][i]]>=deep[y])x=f[x][i];//深的节点向上跳 if(x==y)return x; for(int i=t;i>=0;i--) if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];//一起往上跳 return f[x][0]; }
接下来是完整代码:
#include<bits/stdc++.h> using namespace std; const int N=500002; int number,m,root,f[N][20],next[N*2],head[N*2],ver[N*2],tot=0,res[N],deep[N],t; queue<int> q; int read(){ int s=0,w=1;char ch=getchar(); while(ch<'0'||ch>'9')w=(ch=='-')?-1:1,ch=getchar(); while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar(); return s*w; } void add(int x,int y){ ver[++tot]=y;next[tot]=head[x];head[x]=tot; } void Bfs(){ q.push(root);deep[root]=1; while(!q.empty()){ int x=q.front();q.pop(); for(int i=head[x];i;i=next[i]){ int y=ver[i]; if(deep[y])continue; deep[y]=deep[x]+1; f[y][0]=x; for(int j=1;j<=t;j++)f[y][j]=f[f[y][j-1]][j-1]; q.push(y); } } } int Get_LCA(int x,int y){ if(deep[x]<deep[y])swap(x,y); for(int i=t;i>=0;i--) if(deep[f[x][i]]>=deep[y])x=f[x][i]; if(x==y)return x; for(int i=t;i>=0;i--) if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i]; return f[x][0]; } int main(){ number=read();m=read();root=read(); for(int i=1;i<number;i++){ int x=read(),y=read(); add(x,y);add(y,x); } t=(int)((log(number)/log(2))+1); Bfs(); for(int i=1;i<=m;i++){ int x=read(),y=read(); cout<<Get_LCA(x,y)<<endl; } return 0; }
好的,再是Tarjan
不得不说,Tarjan挺厉害的,可以去度娘上搜一下,他发明了很多算法,所以很多时候,算法名都叫Tarjan,但是内容完全不一样
主要思想为:1.任选一个点为根节点,从根节点开始。
2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。
3.若是v还有子节点,返回2,否则下一步。
4.合并v到u上。
5.寻找与当前点u有询问关系的点v。
6.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。
以上这个还是摘自https://www.cnblogs.com/JVxie/p/4854719.html,大家可以去看看
不过看的时候我也没看懂,学这种东西最好的方法就是拿纸和笔模拟:
以此图为例:
假设求(3,6)(4,5)(4,7)的LCA,有关系表示要求的点,就像3与6有关系,4和5有关系
res[i]=1表示当前的点已经被寻找过,,=0表示还没有遍历到这个点
以1为根,找子节点,先找2,2再找子节点,先找3,那么res[1]=1,res[2]=1;
此时3没有了子节点,那么寻找与其有关系点的点,有一个6
但是res[6]=0,跳过,将这个点与2合并(这里就用的并查集,没学过可以看其他人的博客),father[3]=2
回溯到2,res[3]=1,在寻找4,4有一个儿子5,寻找五,标记res[4]=1;
合并father[5]=4,那么4和5的LCA=find(5)(并查集的查找操作)
继续向上回溯,合并,一直到6
合并1,6,发现3和6有关系,res[3]=1,3和6的LCA为find(3);res[6]=1;
寻找到7,合并7和6,发现4和7有关系,4和7的LCA为find(7);
最后就得出来所有答案,时间复杂度为O(n+m);
但是这个是离线算法,就是不能一个一个求,要一起求完
核心代码:
int find(int a){ if(father[a]!=a)father[a]=find(father[a]); return father[a]; } void Tanjan(int x){ res[x]=1; for(int i=head[x];i;i=next[i]){ int y=ver[i]; if(res[y])continue; Tanjan(y);father[find(y)]=find(x); } for(int i=0;i<son[x].size();i++){ int y=son[x][i]; if(res[y]==2)ans[xb[x][i]]=find(y); } res[x]=2; }
再是完整代码
#include<bits/stdc++.h> using namespace std; const int N=500000+2; int number,m,root,next[N*2],head[N*2],ver[N*2],tot=0; int res[N],father[N],ans[N]; vector<int> son[N],xb[N]; int read(){ int s=0,w=1;char ch=getchar(); while(ch<'0'||ch>'9')w=(ch=='-')?-1:1,ch=getchar(); while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar(); return s*w; } void add(int x,int y){ ver[++tot]=y;next[tot]=head[x];head[x]=tot; } int find(int a){ if(father[a]!=a)father[a]=find(father[a]); return father[a]; } void Tanjan(int x){ res[x]=1; for(int i=head[x];i;i=next[i]){ int y=ver[i]; if(res[y])continue; Tanjan(y);father[find(y)]=find(x); } for(int i=0;i<son[x].size();i++){ int y=son[x][i]; if(res[y]==2)ans[xb[x][i]]=find(y); } res[x]=2; } int main(){ number=read();m=read();root=read(); for(int i=1;i<number;i++){ int x=read(),y=read(); add(x,y);add(y,x); } for(int i=1;i<=m;i++){ int x=read(),y=read();ans[i]=1e9; son[x].push_back(y);son[y].push_back(x); xb[x].push_back(i);xb[y].push_back(i); } for(int i=1;i<=number;i++)father[i]=i; Tanjan(root); for(int i=1;i<=m;i++)cout<<ans[i]<<endl; return 0; }
能力有限,如果没有讲清楚,可以看之前的网址,那篇博客写的很好