[定义]
LCA指的是最近公共祖先(Least Common Ancestors)。如图:
举个例子,如果要[yào]求M和H的最近公共祖先。很明显,M的祖先有G,E,F;而H的祖先有E,F。其公共祖先是E,F。但他们的最近公共祖先是E。很简单吧:-)
好的,接下来看一道模板题。
原题链接(洛谷P3379)
[题目描述]
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
[输入格式]
第一行包含三个正整数N、M、S,分别表示树的结点个数、询问的个数和树根结点的序号。
接下来N-1行每行包含两个正整数x、y,表示x结点和y结点之间有一条直接连接的边(数据保证可以构成树)。
接下来M行每行包含两个正整数a、b,表示询问a结点和b结点的最近公共祖先。
[输出格式]
输出包含M行,每行包含一个正整数,依次为每一个询问的结果。
[输入输出样例]
Sample Input
5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5
Sample Output
4
4
1
4
4
[普通暴力]
如果我们要找B,H的最近公共祖先。(还是这张图) 我们应该先找到B和H的深度,定义为dep。
发现dep[H]<dep[B],所以把B向上跳,跳到和H相同的高度,即为D(dep[D]=dep[H])。然后两点再一起向上一次一次地跳,如果他们跳到的点相同,那么这个点就是他们的最近公共祖先。
暴力虽然实现简单,但是复杂度太高,肯定不能AC。所以,我们就应该考虑如何优化。
[优化思路]
首先,我们要知道这种算法会超时的问题所在。通过分析样例,我们发现,我们找了很多与最近公共祖先无关的点(如:D,C,E),导致算法效率低下。知道这一点后,我们优化的思路就很清晰了:就是要跳尽量少的次数(跳得尽量远),选尽量少的中继点(跳得尽量远), (这不是是一个意思吗?) 但是却仍能找到最近公共祖先 (跳得尽量远) 。
所以,这道题便很自然地与某神奇的方法联系上了——倍增!
- [定义]
倍增,意思是成倍的增加增长;成倍地增长。(字典翻译) - [神奇之处]
这段几乎全部是搬运这位大佬的文章的
上面这段必须有,我可不想被封号
好了,现在开始就是倍增时间。
从前,有两只可爱得不得了的小白兔,它想从A地去往遥远的B地。
2B小白兔: 向右边跳一步,左边跳一步,再向右边跳很多步,再……(对不起,这个太脑残了)
普通小白兔: 向右边跳一步,再跳一步,再跳一步……再跳一步,哇,到了!好开心!
显然是个人都会选择普通小白兔的做法。但是,如果这样一次一次走下去,即使这种方法所需的空间极少,但是时间消耗的太多了。这种用时间换空间的做法,在这种题中是绝对不行的。
就像物极必反一样,2B小白兔突然就变得十分聪明,成功把自己的B去掉了。它变成了一只很2的小白兔好吧其实也没什么区别。 就在所有人都再次无情嘲笑它的时候。它突然灵光一闪,他没想到,自己的名字竟有如此神奇的魔力。没错就是2!
这只聪明的兔子它开始跳一步,然后跳上一次跳的步数 * 2,这样他以比普通小白兔快无数倍的速度到了终点,鄙视一下那只一步一步跳的小白兔;
于是,这只自信的2B兔子开始自信地尝试做LCA。我绝对没有说你们
[正解]
我们采用树倍增法。不妨设f[x][k]表示x的2^k倍祖先。这样,我们查找祖先时以2的倍数递减,就能极大地提高效率。
- 主函数
int main(void)//从主函数开始阅读是好习惯;
{
log[0]=-1;
scanf("%d %d %d",&n,&m,&s);
int i,x,y;
for(i=1;i<=n-1;i++)
{
log[i]=log[i>>1]+1;
scanf("%d %d",&x,&y);
add(x,y);//储存路径:邻接表;
add(y,x);
}
dfs(s,0);//初始化;
for(i=1;i<=m;i++)
{
scanf("%d %d",&x,&y);
if(dep[x]<dep[y])//我们为了方便把要[yào]求的节点调整到同一高度,就默认设置dep[x]>dep[y];
{
swap(x,y);
}
printf("%d\n",query(x,y));//query是查找函数;
}
return 0;
}
log的用处(这是我自己加的,其实不要也无所谓,只是我觉得这样会快一些(真的就只能快一些) :
n(dep) | log[n] |
---|---|
0 | -1 |
1 | 0 |
2 | 1 |
3 | 1 |
4 | 2 |
5 | 2 |
6 | 2 |
… | … |
先给出公式:log[0]=-1(只是初始化);log[i]=log[i/2]+1;
很明显,log的作用是:有一点x,其深度为n,log存的是n在二进制中,最左端1的位置;也就是说,x第一次跳只能跳2^(log[n])。
- 储存
void add(int x,int y)//因为这道题的节点数很大,所以建议用邻接表储存。当然,因为是无向图,所以每次输入要存两次;
{
tot++;
next[tot]=first[x];
first[x]=tot;
v[tot]=y;
}
- 初始化
void dfs(int x,int father)//father是x的父节点;
{
dep[x]=dep[father]+1;//这就没必要解释了吧;
int i;
for(i=1;i<=log[dep[x]];i++)//log在现在就有用了,比起蓝皮书直接把i<=log[dep[x]]换成了i<=20,确实快了一丢丢;
{
f[x][i]=f[f[x][i-1]][i-1];//点x直接向上跳2^i步等价于x向上跳了两次2^(i-1)步;
}
for(i=first[x];i;i=next[i])//找和x连通的点,找到即是x的儿子;
{
if(v[i]==father)
{
continue;
}
f[v[i]][0]=x;//把x的子节点的父节点设置为x;
dfs(v[i],x);
}
return;
}
- 查询
int query (int x,int y)
{
int i;
if(dep[x]!=dep[y])//如果初始高度不相等,就把高度高的x与相对低的y齐平;
{
for(i=log[dep[x]];i>=0;i--)
{
if(dep[f[x][i]]>=dep[y])//如果发现dep[f[x][i]]仍然大于dep[y],就把x变为f[x][i];
{
x=f[x][i];
}
if(x==y)//如果齐平后发现x=y,说明他们的最近公共祖先就是y,直接返回;
{
return x;
}
}
}
for(i=log[dep[x]];i>=0;i--)//高度相等后,开始找祖先(不是公共!!);
{
if(f[x][i]!=f[y][i])//因为是f[x][i]!=f[y][i]才跳,所以找的永远不可能是公共祖先,而是最近公共祖先的儿子;
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];//因为找的是最近公共祖先的儿子,所以在跳一次就是最近公共祖先;
}
- 完整代码
#include<cstdio>
#include<iostream>
using namespace std;
int n,m,s;
int f[1000001][40];
int log[500010],dep[1000010];
int tot,first[1000010],next[1000010],v[1000010];
void add(int x,int y)
{
tot++;
next[tot]=first[x];
first[x]=tot;
v[tot]=y;
}
void dfs(int x,int father)
{
dep[x]=dep[father]+1;
int i;
for(i=1;i<=log[dep[x]];i++)
{
f[x][i]=f[f[x][i-1]][i-1];
}
for(i=first[x];i;i=next[i])
{
if(v[i]==father)
{
continue;
}
f[v[i]][0]=x;
dfs(v[i],x);
}
return;
}
int query (int x,int y)
{
int i;
if(dep[x]!=dep[y])
{
for(i=log[dep[x]];i>=0;i--)
{
if(dep[f[x][i]]>=dep[y])
{
x=f[x][i];
}
if(x==y)
{
return x;
}
}
}
for(i=log[dep[x]];i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int main(void)
{
log[0]=-1;
scanf("%d %d %d",&n,&m,&s);
int i,x,y;
for(i=1;i<=n-1;i++)
{
log[i]=log[i>>1]+1;
scanf("%d %d",&x,&y);
add(x,y);
add(y,x);
}
dfs(s,0);
for(i=1;i<=m;i++)
{
scanf("%d %d",&x,&y);
if(dep[x]<dep[y])
{
swap(x,y);
}
printf("%d\n",query(x,y));
}
return 0;
}
附赠另一道模板题Lowest Common Ancestor
<=to be continued