树上最近公共祖先LCA

首先介绍一下朴素的O(N)算法

比如我们在树上要找x,y两个节点的LCA,首先我们将两个节点移动到同一深度位置(将深度深的结点向上移动,直到两个节点的深度相同),而后两个节点共同向上移动,直到移动到同一个节点,这个节点就是LCA

具体看代码:

int find_root(int u,int v){
    int depth_u=buf[u].depth;
    int depth_v=buf[v].depth;
    if(depth_u<depth_v)swap(u,v);//另u为深度更大的结点
    int x=u;
    while (buf[x].depth!=buf[v].depth) x=buf[x].root;//u结点向上移动,直到与v结点深度相同
    while(x!=v){//两个节点共同向上移动,直到到达同一个节点
        x=buf[x].root;
        v=buf[v].root;
    }
    return x;
}

对于小数据,这个算法已经够用,但如果数据更大,那么就需要寻找更快的算法。

比如P3379 【模板】最近公共祖先(LCA) 

n取到了500000,由于询问占用0(N)的时间,这就要求我们求LCA的复杂度应优化到log(N)。由此,我们引入倍增法LCA

那么,我们观察朴素算法,可以发现时间主要花在两个地方:

  1. 将两个点移动到深度相同的位置
  2. 两个点一起向上移动找LCA

在朴素算法里,我们是一步一步向上移动的,那么,有没有办法把步子跨得大一下呢?

这里我们用二进制的方法想一想,举个例子,如果我们想向上移动11步,11的二进制是(1011B),那么我们是不是可以第一次先向上移动8(1000B)步,第二次移动2(10B)步,第三次移动1(1B)步呢。这样,我们就把原来需要移动11步优化到了3步。

有了上述思想,我们就可以在预处理阶段处理一个数组jump,令jump[x][i]表示节点x向上跳2^i步到达的位置

还以上述11步为例,假如树的最大深度是1000,那么2^10=1024就可以表示所有深度的结点

我们跳11步,相当于跳00000001011步,这样我们从跳2^10开始尝试:

for(int i=10;i>=0;i--){
        if(depth[jump[a][i]]>=depth[b]){
            a=jump[a][i];
        }
    }

由二进制可以看出,显然i=10~4时都不跳(如果跳,则跳的总步数一定大于11),只在i=3,1,0时跳

这样,深度1000的树,最坏情况跳1000步,我们用10次循环就可以完美解决。

有了第一个问题解决的思想,我们同样可以用这个思路解决第二个问题。不同的是,我们这次的目标是优化跳到LCA下面一层的结点,只有两个节点跳到不是同一个位置时才跳。这一点读者可以结合二进制和代码自己理解一下。

int lca(int a,int b){
    if(depth[b]>depth[a])swap(a,b);
    for(int i=20;i>=0;i--){//移动到同一深度位置
        if(depth[jump[a][i]]>=depth[b]){
            a=jump[a][i];
        }
    }
    if(a==b)return a;
    for(int i=20;i>=0;i--){//共同向上移动查找LCA
        if(jump[a][i]!=jump[b][i]){
            a=jump[a][i];
            b=jump[b][i];
        }
    }
    return jump[a][0];//跳到的是LCA的下方结点
}

那么,现在的问题就到了怎么预处理jump这个数组上了,这里就用到了倍增的思想。

倍增思想的核心公式是2^i=2^(i-1)+2^(i-1)  eg.2^10=2^9+2^9 (1024=512+512)

那么,X向上跳2^i步,是不是可以理解为X向上跳2^(i-1)步后再跳2^(i-1)步呢。是不是也可以理解为X向上跳2^(i-1)步的这个节点再跳2^(i-1)步呢。

由此我们就得到了更新公式 jump[x][i+1]=jump[jump[x][i]][i];  这样更新为什么对呢,首先我们知道当更新 jump[x][i+1]时jump[x][i]已经计算过,并且我们是从根节点向下逐层节点初始化,因此jump[x][i]这个节点的jump[jump[x][i]]都已被更新过,所以每一步中jump[jump[x][i]][i]都是已经计算过的值。这样,我们就可以逐层更新,得到所有结点的jump

void preprocessing(int x,int fa){
    depth[x]=depth[fa]+1;
    jump[x][0]=fa;//向上跳一步是x的父亲节点
    for(int i=0;i<20;i++){//更新x结点的jump数组
        jump[x][i+1]=jump[jump[x][i]][i];
    }
    vector<int>::iterator it;
    for(it=buf[x].begin();it!=buf[x].end();it++){//扩展更新x的子节点
        if(*it==fa)continue;
        preprocessing(*it,x);
    }
}

 

以下是本题的AC代码

这道题对数据卡的非常严格,除了算法本身,输入输出时间,存数的数据结构也同样需要注意效率。

本代码使用cin输入会TLE

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <cstdio>
using namespace std;

int n,m,s;
vector<int> buf[500005];
int depth[500005];
int jump[500005][25];

void preprocessing(int x,int fa){
    depth[x]=depth[fa]+1;
    jump[x][0]=fa;
    for(int i=0;i<20;i++){
        jump[x][i+1]=jump[jump[x][i]][i];
    }
    vector<int>::iterator it;
    for(it=buf[x].begin();it!=buf[x].end();it++){
        if(*it==fa)continue;
        preprocessing(*it,x);
    }
}

int lca(int a,int b){
    if(depth[b]>depth[a])swap(a,b);
    for(int i=20;i>=0;i--){
        if(depth[jump[a][i]]>=depth[b]){
            a=jump[a][i];
        }
    }
    if(a==b)return a;
    for(int i=20;i>=0;i--){
        if(jump[a][i]!=jump[b][i]){
            a=jump[a][i];
            b=jump[b][i];
        }
    }
    return jump[a][0];
}

int main(){
    cin>>n>>m>>s;
    int x,y;
    for(int i=0;i<n-1;i++){
        scanf("%d%d",&x,&y);
        buf[x].push_back(y);
        buf[y].push_back(x);
    }
    preprocessing(s,0);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&x,&y);
        printf("%d\n",lca(x,y));
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值