倍增/树链剖分解决LCA问题

已知一棵树,给出u,v两点,求它们的最近公共祖先,这就是 LCA问题(Least Common Ancestors)
如上图,LCA(15,7)=6,LCA(11,3)=10,LCA(13,15)=8...

先从简单的方法入手。不妨设v的深度比u的大,也就是v比u离根节点更远一些。首先要将v提到与u同一深度,然后它们再一起上提,直到相遇,即找到了LCA为止。 
代码如下:
int LCA(int u,int v){
    if(depth[u]>depth[v]) return LCA(v,u);
    while(depth[v]>depth[u]) v=fa[v];
    while(u!=v){
        u=fa[u];
        v=fa[v];
    }
    return u;
} 

但这样一次次上提,会不会太慢了?不妨利用倍增的思想,记fa[k][i]为结点i向上的第2^k个结点,这样预处理好所有的fa之后,再用二分法将结点上提即可。

eg.求LCA(15,16)
k=2时,fa[2][15]、fa[2][16]都越界了
    继续往下 
k=1时,fa[1][15](即15向上第2^1=2个点4)= fa[1][16](即16向上第2^1=2个点4)
    继续往下 
k=0时 fa[0][15](即15向上第2^0=1个点6)!= fa[0][16](即16向上第2^0=1个点10)
    则将15提到6上,将16提到10
最后LCA(15,16)=fa[0][6]=fa[0][10]=4

代码如下:

#include<cstdio>
const int MAXN=300000;
const int MAXLOGN=20;
int n,m,cnt=0;
int last[MAXN],depth[MAXN],fa[MAXLOGN][MAXN];
struct edge{
    int to,next;
}e[MAXN*2];
void add(int u,int v){
    cnt++;
    e[cnt]=(edge){v,last[u]};
    last[u]=cnt;
}
void dfs(int x){
    for(int i=last[x];i;i=e[i].next){
        int cur=e[i].to;
        if(cur!=fa[0][x]){  //防止向上回到父结点 
            fa[0][cur]=x;
            depth[cur]=depth[x]+1;
            dfs(cur);
        }
    }
}
void init(){
    scanf("%d%d",&n,&m); 
    int root;bool isNotRoot[MAXN];
    for(int i=1;i<n;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        isNotRoot[v]=1;  //边从u连向v,则v肯定不是根节点 
        add(u,v);
        add(v,u);
    }
    for(root=1;root<=n;root++)
      if(!isNotRoot[root]) break; 
    depth[root]=1;  //找出根结点,并将深度设为1 
    dfs(root);  //从根结点开始dfs,预处理depth和fa[0]
    for(int k=0;k<MAXLOGN;k++)  //预处理fa
      for(int i=1;i<=n;i++)
          if(fa[k][i]) fa[k+1][i]=fa[k][fa[k][i]]; 
    //因为2^(k+1)=2^k+2^k,所以i向上2^(k+1)歩,相当于i向上走2^k歩后再走2^k歩
    //由此得到递推式 fa[k+1][i]=fa[k][fa[k][i]]
}
int LCA(int u,int v){
    if(depth[u]>depth[v]) return LCA(v,u); //保证u不在v下方
    if(depth[u]!=depth[v]){   //先将v上提,使两个结点在同一深度上
        for(int k=MAXLOGN-1;k>=0;k--){  
            if(depth[v]-(1<<k)>=depth[u]) //1<<k即2^k  v上提后不能在u的上方 
                v=fa[k][v];
        }
    }
    if(u==v) return u;  //如果在同一结点上,直接得LCA 
    for(int k=MAXLOGN-1;k>=0;k--){  //二分法一点点缩小范围 
        if(fa[k][u]==0) continue;  //不能越界
        if(fa[k][u]!=fa[k][v]){  //如果fa不同,将两点都上提 
            u=fa[k][u];
            v=fa[k][v];
        }
    }
    return fa[0][u];
}
void solve(){
    for(int i=1;i<=m;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        printf("%d\n",LCA(u,v));
    }
}
int main(){
    init();
    solve(); 
}


但是,对于树的高度很大甚至成为一条链的情况,暴力法是不足取的,可以将树划分为若干条重链。

做法:对于每个结点,选取它的儿子之中以这个儿子为根的子树size最大的那一个,将它们连接。最后可能有多条线可以首尾相连成一条重链。

eg.对于4,它的儿子有6、10,其中6为根的最大子树有6、15、7三个结点,10为根的最大子树有10、11、...3、12等6个结点,当然将4与10连接起来。同理,将8与4连接起来,记top[4]=8。由于8-4-10首尾相连,所以比较完6和10后直接top[10]=top[4]即可。这样10就直接连到了重链的顶部8。对于剩下的6,记top[6]=6,即它本身。
 
寻找时,如果两点在同一条重链上,LCA就是两点中深度更小的那个;反之,把深度大的那个拉到所在重链的顶部,使其尽量往上靠。
eg.求LCA( 15, 3) 其中定义<</>>为比较结点深度的运算符
top[15]=15 << top[3]=3
将深度更大的 3提到top[3]=3的父亲结点 16
top[15]=15 >> top[16]=8
同理将 15提到top[15]的父亲结点 6
top[6]=6 >> top[16]=8
6提到其父亲 4
top[4]=8 top[16]=8
显然,它们所在的重链顶点相同,即它们在同一条重链上。这样一来,LCA就是 416中深度较小的 4
代码如下:
#include<cstdio>
const int MAXN=300000;
int n,m,cnt=0;
int last[MAXN],depth[MAXN],size[MAXN],son[MAXN],top[MAXN],fa[MAXN];
struct edge{
    int to,next;
}e[MAXN*2];
void add(int u,int v){
    cnt++;
    e[cnt]=(edge){v,last[u]};
    last[u]=cnt;
}
void dfs1(int x){
    size[x]=1;
    for(int i=last[x];i;i=e[i].next)
    {
        int cur=e[i].to;
        if(fa[x]!=cur){ 
            fa[cur]=x;
            depth[cur]=depth[x]+1;
            dfs1(cur);
            size[x]+=size[cur];
            if(size[son[x]]<size[cur])
              son[x]=cur;  //son[i]表示结点i的儿子中以其为根的子树size最大的那个 
        }
    }
}
void dfs2(int x){
    if(x==son[fa[x]]) //如果x是fa[x]的儿子中拥有最大size的那个 
        top[x]=top[fa[x]]; //则把top[x]连向top[fa[x]] 形成重链 
    else top[x]=x; //否则指向它自己 
    for(int i=last[x];i;i=e[i].next)
        if(e[i].to!=fa[x]) 
          dfs2(e[i].to);
}
void init(){
    scanf("%d%d",&n,&m); 
    int root;bool isNotRoot[MAXN];
    for(int i=1;i<n;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        isNotRoot[v]=1;  
        add(u,v);
        add(v,u);
    }
    for(root=1;root<=n;root++)
      if(!isNotRoot[root]) break; 
    depth[root]=1; 
    dfs1(root); //预处理depth和fa 
    dfs2(root); //预处理top 
}
int LCA(int u,int v){
    while(top[u]!=top[v]) { //u、v不在同一条重链上时 
        depth[top[u]]>depth[top[v]]?u=fa[top[u]]:v=fa[top[v]]; //将深度大的上提 
    }
    return depth[u]<depth[v]?u:v; //返回u、v中在较上方的那个 
}
void solve(){
    for(int i=1;i<=m;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        printf("%d\n",LCA(u,v));
    }
}
int main(){
    init();
    solve(); 
}

现有一棵有n个结点、n-1条边的树,给出m次询问。

输入样例
16 5
8 1
8 4
8 5
5 9
4 6
4 10
6 15
6 7
10 11
10 16
10 2
16 3
16 12
1 14
1 13
9 14
6 11
14 13
16 7
10 3

 

输出样例 
8
4
1
4
10
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值