浅谈倍增求树上LCA
1 何为LCA?
LCA就是“最近公共祖先”。
例如下图
2,3的LCA为1
4,5的LCA为2
4,3的LCA为1
可以理解为是两个结点最近的父节点。
2基本思路
2-1倍增思想
倍增是什么?
我们可以理解为有一只小兔子 要从1号点,跳到n号点,它可以怎么跳呢?
1.它可以一步一步的跳
如果这个点是n号点,那么它的任务完成,如果不是,就继续跳到下一个点。
这样的跳法在n很大的时候效率显然是不高的,我们考虑别的跳法。
2.它可以一次跳多步
那么,很多步如何界定呢?
在计算机中,数字是由二进制储存的,所以求2的n次方十分便捷(1<<n)。
我们就看到了一个很方便的跳法,先跳2的k次方,看到了n号点没有,如果跳过了,就跳2的k-1次方试试,这样的复杂度是很低的(
O
(
l
o
g
n
)
O(log_n)
O(logn))。
2-2预处理实现
预处理一个log数组,表示
l
o
g
2
n
log_2n
log2n的值是多少。
代码实现如下:
for(int i=1;i<=n;i++){
log[i]=log[i-1]+(1<<log[i-1]==i);
}
(如果看的不是太懂的请自己粘贴到编译器上输出一下。)
我们要处理每个结点在树里的深度,让两个点能从同一深度出发一起跳,需要一个dfs函数来实现。
还要用一个二维数组fa[i][j]来标记从i号点跳
2
j
2^j
2j个深度可以到达的结点。
细节请看代码:
void dfs(int u,int father){
depth[u]=depth[father]+1;//深度=父亲的深度+1
fa[u][0]=father;//从u跳1个就是它的父亲
for (int i=1; (1<<i)<=depth[u]; i++) {//将u可以向上跳的路程全部标记
fa[u][i]=fa[fa[u][i-1]][i-1];//向上跳2^i相当于跳2^(i-1)再跳2^(i-1)
}
for (int i=head[u]; i; i=edges[i].nxt) {
int v=edges[i].to;//遍历每一个子节点dfs
if (v!=father) {//如果没有反向搜回去
dfs(v, u);
}
}
}
好了,我们与处理了每个结点的深度以及它可以跳到的结点,就可以开始求LCA了。
2-3 LCA求法
我们刚拿到两个节点的时候,它们大概率不在同一个深度,那么没有办法一起向上跳到它们的LCA上去,我们要分两步去求:
1.让两个节点处于统一深度。
2.让它们同时上跳到LCA上。
等等,是不是有什么东西没有考虑到?
加入第一步做完后两个节点在同一个点上,我们就不需要让它们再往上跳了。
例如在这个图中:
要求求出2、4的LCA,我们将4提到2上,发现已经到了同一个点,就可以直接输出当前处于同一位置的节点,return函数就行了。
细节参考代码:
int lca(int x,int y){
if (depth[x]<depth[y]) {//使得x的深度总比y大,方便处理。
swap(x, y);
}
while (depth[x]>depth[y]) {//将x提到与y同一深度
x=fa[x][log[depth[x]-depth[y]]-1];
}
if (x==y) {//上面说到的特别判断
return x;
}
for (int i=log[depth[x]]; i>=0; i--) {//x,y一起向上跳
if (fa[x][i]!=fa[y][i]) {//如果不相同就跳,那么如果已经跳到相同的了,就不会再向上蹦了,等待for循环执行完即可(反正也就二三十次)
x=fa[x][i];
y=fa[y][i];
}
}
return fa[x][0];//返回fa[x][0]
}
3 完整代码参考
#include <cstdio>
#include <iostream>
#define maxn 500001
#define maxm 500001
#define maxlog 22
using namespace std;
int n,m,s,cnt,head[maxn],log[maxn],fa[maxn][maxlog],depth[maxn];
struct edge{
int to,nxt;
}edges[maxm<<1];
void addedge(int u,int v){
cnt++;
edges[cnt].to=v;
edges[cnt].nxt=head[u];
head[u]=cnt;
}
void initlog(){
for (int i=1; i<=n; i++) {
log[i]=log[i-1]+(1<<log[i-1]==i);
}
}
void dfs(int u,int father){
depth[u]=depth[father]+1;
fa[u][0]=father;
for (int i=1; (1<<i)<=depth[u]; i++) {
fa[u][i]=fa[fa[u][i-1]][i-1];
}
for (int i=head[u]; i; i=edges[i].nxt) {
int v=edges[i].to;
if (v!=father) {
dfs(v, u);
}
}
}
int lca(int x,int y){
if (depth[x]<depth[y]) {
swap(x, y);
}
while (depth[x]>depth[y]) {
x=fa[x][log[depth[x]-depth[y]]-1];
}
if (x==y) {
return x;
}
for (int i=log[depth[x]]; i>=0; i--) {
if (fa[x][i]!=fa[y][i]) {
x=fa[x][i];
y=fa[y][i];
}
}
return fa[x][0];
}
int main(){
scanf("%d%d%d",&n,&m,&s);
initlog();
for (int i=1; i<n; i++) {
int x,y;
scanf("%d%d",&x,&y);
addedge(x, y);
addedge(y, x);
}
dfs(s, 0);
for (int i=1; i<=m; i++) {
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
return 0;
}
4 结语
倍增是一个非常巧妙的思想,它将反复的尝试化成了由小到大的尝试,大大减小了算法枚举的时间复杂度。希望以后不仅能在LCA问题上使用到倍增的思想,也能在一些别的题上让这个思想得到体现。