一.什么是最近公共祖先
在一棵树上,两个节点之间公共的深度最大的祖先节点 称为最近公共祖先,例如:
(1)4和3的最近公共祖先是1
(2)4和2的最近公共祖先是2
(3)4和5的最近公共祖先是2
二.如何求两点间的最近公共祖先
1.离线Tarjan算法(基于DFS + 并查集)
(1)所谓离线算法就是把所有的询问保存下来,在一次搜索中全部处理完,最后一起输出。
(2)DFS深搜的特点是:先进入父节点在进入其子孙节点,利用这个特点,我们对于两个节点的公共祖先节点有两种情况:
(假设两个节点为v,u)
<1>u节点是v节点的直接父节点或者祖先节点,这时LCA(u,v) = u,因为肯定先搜u节点,所以标记u节点,进入v发现u已经被标记,此时LCA(u,v) = u;
<2>u节点跟v节点不是一颗子树里面,那么v可能是u的父亲的另一颗子树里面的,也可能是u的父亲的父亲的另一颗子树里面的。。以此类推,反之亦然。所以u和v的最近公共祖先一定是目前所合并的u或者v的祖先节点pre[v]或者pre[u]。
假设先搜索到u节点,现在寻找v在u的父亲的哪个子树或者u的祖先的哪个子树里面,现在就要回溯回祖先节点搜索下一个子树了。每从u回溯一层,搜这层的其他子树时就是寻找v时,如果这时在其他子树里找到了v,那公共祖先就是pre[pre[pre[.....u] ] ](看回溯层数),也就是并查集不断合并的过程!
也就是回溯到u的着一层祖先节点时,在子树里找到了v,那么最近公共祖先就是这层的节点!
(3)算法实现
<1> 先搜后判断
标记当前节点
搜索所有子节点,合并子节点到当前节点
循环所有跟当前的查询关系,若v被标记,公共祖先就是pre[v];
注:此方法会在<1>情况时,父节点和子节点都求一遍
<2>先判断后搜索
循环所有跟当前的查询关系,若v被标记,公共祖先就是pre[v];
标记当前节点
搜索所有子节点,合并子节点到当前节点
(4)代码实现:
#include <iostream>
//#include<bits/stdc++.h>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 100000 + 7;
struct Edge{//保存树上边
int to,next;
}edge[maxn];
struct QEdge{//保存查询关系
int from,to,lca,next;
}Qedge[maxn];
int n,m,head[maxn],Qhead[maxn],tot,cnt,ans[maxn],pre[maxn];
bool judge[maxn],notRoot[maxn];
void addEdge(int a,int b){//树上加边
edge[tot].to = b;edge[tot].next = head[a];head[a] = tot++;
}
void addQEdge(int a,int b){//关系增加
Qedge[cnt].from = a;Qedge[cnt].to = b;Qedge[cnt].next = Qhead[a];Qhead[a] = cnt++;
}
void init(){//初始化
tot = cnt = 0;
memset(head,-1,sizeof(head));
memset(Qhead,-1,sizeof(Qhead));
memset(judge,0,sizeof(judge));
memset(notRoot,0,sizeof(notRoot));
memset(ans,0,sizeof(ans));
for(int i = 0;i<=n;i++)pre[i] = i;
}
int finded(int x){
int v = x;
while(v!=pre[v])v = pre[v];
int j = x;
while(j!=v){
int temp = pre[j];
pre[j] = v;
j = temp;
}
return v;
}
void Union(int a,int b){//并查集
int fx = finded(a);
int fy = finded(b);
if(fx!=fy){
pre[fx] = fy;
}
}
void LCA(int root){
judge[root] = 1;//标记当前节点
for(int i = head[root];~i;i = edge[i].next){//搜索子树
if(!judge[edge[i].to]){
LCA(edge[i].to);
Union(edge[i].to,root);//合并节点
}
}
for(int i = Qhead[root];~i;i = Qedge[i].next){//判断所有关系
if(judge[Qedge[i].to]){//已经被标记
int fa = finded(Qedge[i].to);//最近公共祖先
Qhead[i].lca = fa;
if(i%2==0){//更新另一个相对关系
Qhead[i+1].lca = fa;
}
else Qhead[i-1].lca = fa;
}
}
}
int main()
{
while(scanf("%d",&n)!=EOF){
init();
int root;
for(int i = 1;i<=n;i++){
int p,len;
scanf("%d%d",&p,&len);//父节点,子节点数目
for(int i = 0;i<len;i++){
int child;
scanf("%d",&child);//子节点
notRoot[child] = true;
addEdge(p,child);
}
}
for(int i = 1;i<=n;i++){
if(!notRoot[i]){//寻找根节点
root = i;
break;
}
}
scanf("%d",&m);//查询数目
while(m--){
int x,y;
scanf("%d%d",&x,&y);
addQEdge(x,y);//增加关系
addQEdge(y,x);
}
LCA(root);//LCA
for(int i = 0;i<cnt;i+=2){
printf("(%d %d) LCA is %d\n",Qedge[i].from,Qedge[i].to,Qedge[i].lca);
}
}
return 0;
}
#include <iostream>
#include<bits/stdc++.h>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 100000 + 7;
struct Edge{
int to,next;
}edge[maxn];
struct QEdge{
int from,to,lca,next;
}Qedge[maxn];
int n,m,head[maxn],Qhead[maxn],tot,cnt,ans[maxn],pre[maxn];
bool judge[maxn],notRoot[maxn];
void addEdge(int a,int b){
edge[tot].to = b;edge[tot].next = head[a];head[a] = tot++;
}
void addQEdge(int a,int b){
Qedge[cnt].from = a;Qedge[cnt].to = b;Qedge[cnt].next = Qhead[a];Qhead[a] = cnt++;
}
void init(){
tot = cnt = 0;
memset(head,-1,sizeof(head));
memset(Qhead,-1,sizeof(Qhead));
memset(judge,0,sizeof(judge));
memset(notRoot,0,sizeof(notRoot));
memset(ans,0,sizeof(ans));
for(int i = 0;i<=n;i++)pre[i] = i;
}
int finded(int x){
int v = x;
while(v!=pre[v])v = pre[v];
int j = x;
while(j!=v){
int temp = pre[j];
pre[j] = v;
j = temp;
}
return v;
}
void Union(int a,int b){
int fx = finded(a);
int fy = finded(b);
if(fx!=fy){
pre[fx] = fy;
}
}
void LCA(int root){
for(int i = Qhead[root];~i;i = Qedge[i].next){
if(judge[Qedge[i].to]){
int fa = finded(Qedge[i].to);
Qedge[i].lca = fa;
if(i%2==0)Qedge[i+1].lca = fa;
else Qedge[i-1].lca = fa;
}
}
judge[root] = 1;
for(int i = head[root];~i;i = edge[i].next){
if(!judge[edge[i].to]){
LCA(edge[i].to);
Union(edge[i].to,root);
}
}
}
int main()
{
while(scanf("%d",&n)!=EOF){
init();
int root;
for(int i = 1;i<=n;i++){
int p,len;
scanf("%d%d",&p,&len);
for(int i = 0;i<len;i++){
int child;
scanf("%d",&child);
notRoot[child] = true;
addEdge(p,child);
}
}
for(int i = 1;i<=n;i++){
if(!notRoot[i]){
root = i;
break;
}
}
scanf("%d",&m);
while(m--){
int x,y;
scanf("%d%d",&x,&y);
addQEdge(x,y);
addQEdge(y,x);
}
LCA(root);
for(int i = 0;i<cnt;i+=2){
printf("(%d %d) LCA is %d\n",Qedge[i].from,Qedge[i].to,Qedge[i].lca);
}
}
return 0;
}
2.在线算法
待补....
3.LCA应用:
树上两节点最短距离dis(u,v) = dis(u,root) + dis(v,root) - 2*dis(LCA(u,v), root);
树上两节点最短距离 = u到根节点的距离 + v到根节点的距离 - 两倍的公共祖先到根结点的距离