浅谈倍增求树上LCA

浅谈倍增求树上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问题上使用到倍增的思想,也能在一些别的题上让这个思想得到体现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值