LCA(Least Common Ancestors),即最近公共祖先。对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。
求LCA的算法有很多种,分为在线和离线。离线算法一般有tarjan,在线算法则是树上倍增与rmq。
这里主要讲下在线算法吧:-)
经过“肉眼扫描算法”,我们可以很快的得出4和6的最近公共祖先是1。
倍增
对于两个同一层(也就是深度一样)的结点,向上寻找他们的最近公共祖先。对于上图的4和6来说,我们首先判断 4 的父亲 6 的父亲是不是同一个结点,如果不是,则继续向上重复 “判断父亲是不是相同的” 这个操作。直到找到为止。
显然,这么一层层的找是很愚蠢的。:-(
看起来比较聪明的办法是,首先预处理出,每个点向上跳2^j次,会跳在哪里。根据我们的生活常识,我们可以通过这些2^(i、j、k…)组合出任意一个正整数,也就是说我们可以通过每次跳2^j次跳出我们想要的。
//首先dfs预处理出点的深度,以及向上跳2^0步所到达的点(就是这个点的父亲)。
void dfs(int node)
{
for(int i=head[node];i;i=nxt[i])
{
if(depth[to[i]])continue;
fat2[to[i]][0]=node;
depth[to[i]]=depth[node]+1;
dfs(to[i]);
}
}
然后我们还要求出跳2^j次所到达的点——
//倍增求出跳2^j(j>=1 && ( 1 <
for(int j=1;(1<for (int i=1;i<=n;i++)
if(fat[i][j-1]!=-1)
fat[i][j]=fat[fat2[i][j-1]][j-1];
//fat[i][j]表示从i结点开始,向上跳2^j步,所到达的结点编号
预处理就这样完了:-)
再回到这张图。首先我们讨论的同一层的结点,比如说4和6,我们将他们一起向上跳2^20(这个数字可以按照题目数据规模更改)次,结果发现跳出去了…于是我们跳2^19次,2^18次……直到我们第一次跳到了这样的一层——对于所查询的x,y,在这一层的祖宗x1,y1不是同一个。
可以用脚趾头想到,x1与y1的最近公共祖先,就是x,y的最近公共祖先。然后我们就从x1,y1继续向上跳, 可以用脚趾头想到,由于我们是第一次跳到这样的一层,x与x1的深度差异,一定比x1与x1,y1的最近公共祖先的深度差异小。
假设我们是跳了2^j次跳到了x1,我们只要以x1为起点,从2^(j-1)次开始跳。可以用手趾头想到,这个时候我们跳到的两个点,xn,yn的父亲,就是x,y的最近公共祖先。
是的就这样。^_^
由此我们很容易的就解决了,当x,y深度一样时,查询x与y的最近公共祖先问题。但是更多的时候,x,y是不在同一层的。此时,我们可以运用上面的思想,将深度较大的点向上跳,使得两个点处于同一深度,转换成上面的问题。
查询代码如下:
int find_lca(int x,int y)
{
if(depth[x]for(int j=20;j>=0;j--)
if(depth[x]>=depth[y]+(1<if(x==y)return x;
for(int j=20;j>=0;j--)
if(fat[x][j]!=-1 && fat[x][j]!=fat[y][j])
{
x=fat[x][j];
y=fat[y][j];
}
return fat[x][0];
}
如果有错误,错误是我的:-(
RMQ
由于我不知道怎么长篇大论描述这个算法的神奇,所以我们从左至右来dfs这棵树吧。
经过dfs,我们可以得到这样的一些数组:
first[i]表示第一次访问i结点的次序
ver[i]表示第i次访问了哪个结点
depth[i]表示ver[i]所表示点的深度
对于上面这棵树,这些数组是这样的(都忽略[0]):
first[]=1,2,14,3,5,15,6,10,7
ver[]=1,2,4,2,5,7,9,7,5,8,5,2,1,3,6,3,1
depth[]=1,2,3,2,3,4,5,4,3,4,3,2,1,2,3,2,1
对于任意两个点x,y中,假设first[x] < first[y],则必有first[x] <= ( ver[ ? ]= LCA(x,y) ) <= first[y]。为了更好地理解可以用纸自己画一画。
知道这一点后,求最近公共祖先的问题转化为,求?点满足ver[z]=? , first[x]<= ( ver[z] ) <=first[y],且depth[z]最小。
显然,我们可以用RMQ解决这个问题。:-)
代码如下:
void dfs(int node,int dep)
{
ver[++tot]=node;
first[node]=tot;
depth[tot]=dep;
vis[node]=1;
for(int i=head[node];i!=-1;i=nxt[i])
{
if(vis[to[i]])continue;
dfs(to[i],dep+1);
ver[++tot]=node;
depth[tot]=dep;
}
}
void init(int nn)//nn为访问次数
{
for(int i=1;i<=nn;i++)dp[i][0]=i;
memset(vis,0,sizeof(vis));
dfs(1,1);
for(int j=1;(1<for(int i=1;i+(1<1<=nn;i++)
{
int a = dp[i][j-1],b=dp[i+(1<<(j-1))][j-1];
if(depth[a]else dp[i][j]=b;
}
}
事实上,在实际的操作中,我们并不需要ver数组,first数组就已经能够很好地满足我们的要求了。
查询代码如下:
int askrmq(int left,int right)
{
int i=0;
while((1<<(i+1))<=right-left+1)i++;
int a=dp[left][i],b=dp[right-(1<1][i];
if(depth[a]return a;
return b;
}
int find_lca(int x,int y)
{
x=first[x];y=first[y];
if(x>y)swap(x,y);
return ver[askrmq(x,y)];
}
如果有错误,请让我吃掉它:-(
最后附上几道水题:
求树上两点之间距离 poj 1986 Distance Queries 点我点我:-)
求树上两点之间距离加强版 hdu 2874 Connections between cities 点我点我:-)