您的位置:首页 > 其它

LCA在线算法

2016-08-18 16:35 176 查看
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 <<j) <=n )次所到达的点
for(int j=1;(1<<j)<=n;j++)
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]<depth[y])swap(x,y);
for(int j=20;j>=0;j--)
if(depth[x]>=depth[y]+(1<<j))
x=fat[x][j];
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<<j)<=nn;j++)
for(int i=1;i+(1<<j)-1<=nn;i++)
{
int a = dp[i][j-1],b=dp[i+(1<<(j-1))][j-1];
if(depth[a]<depth[b])dp[i][j]=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<<i)+1][i];
if(depth[a]<depth[b])return a;
return b;
}

int find_lca(<
9c1b
span class="hljs-keyword">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 点我点我:-)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 LCA