最近公共祖先问题(LCA)

一、最近公共祖先定义

在一棵树上,对于节点 x , y x,y x,y而言,如果说 z z z节点既是 x x x的祖先节点,也是 y y y的祖先节点,那么我们认为 z z z节点是 ( x , y ) (x,y) (x,y)的公共祖先节点。

公共祖先节点有很多,那么深度最大的节点,被称之为最近公共祖先,记为 l c a ( x , y ) lca(x,y) lca(x,y)

二、倍增法(在线求lca)

1.朴素法

将其中一个节点 x x x向根节点移动,中途经过的点做标记。再将另一个节点 y y y向上移动,移动到第一个标记过的点,即为最近公共祖先节点。

2.倍增法

可以考虑一次性移动不止 1 1 1个距离。我们知道,任何一个整数都可以分解为若干个 2 i 2^i 2i求和,所以我们就采用每次移动 2 i 2^i 2i距离的方法。

我们需要预处理( b f s bfs bfs)两个数组depth[]fa[][]。其中depth数组,记录节点的深度,根节点深度为 1 1 1 f a [ j ] [ k ] fa[j][k] fa[j][k]记录第 j j j个节点向上移动 2 k 2^k 2k步到达的节点。对于fa数组的初始化,存在一个递推关系,就是先向上跳 2 k − 1 2^{k-1} 2k1步,然后再在那个点的基础上,再跳 2 k − 1 2^{k-1} 2k1步。即, f a [ j ] [ k ] = f a [ f a [ j ] [ k − 1 ] ] [ k − 1 ] fa[j][k] = fa[fa[j][k-1]][k-1] fa[j][k]=fa[fa[j][k1]][k1]

算法步骤如下:

  • 将深度更大的节点移动到与另一个节点同等深度的地方
  • 两个节点一起向上移动,一直移动到最近公共祖先的儿子节点
  • 返回当前点的父亲节点,即为两点的lca

代码模板:

场景1

给定一棵包含 n n n个节点的有根无向树,节点编号互不相同,但不一定是 1 ∼ n 1∼n 1n。有 m m m个询问,每个询问给出了一对节点的编号 x x x y y y,询问 x x x y y y的祖孙关系。对于每一个询问,若 x x x y y y的祖先则输出 1 1 1,若 y y y x x x的祖先则输出 2 2 2,否则输出 0 0 0

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 40010, M = 2 * N;
int n,m;
int h[N],e[M],ne[M],idx;
int depth[N],fa[N][16];
queue<int> que;

void add(int a,int b)
{
    
    
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void bfs(int root)
{
    
    
    memset(depth,0x3f,sizeof(depth));
    depth[0] = 0;
    depth[root] = 1;
    que.push(root);
    while(que.size()){
    
    
        int t = que.front();
        que.pop();
        for(int i=h[t];~i;i=ne[i]){
    
    
            int j = e[i];
            if(depth[j]>depth[t]+1){
    
    
                depth[j] = depth[t] + 1;
                fa[j][0] = t;
                que.push(j);
                for(int k=1;k<=15;k++){
    
    
                    fa[j][k] = fa[fa[j][k-1]][k-1];
                }
            }
        }
    }
}

int lca(int a,int b)
{
    
    
    if(depth[a]<depth[b]) swap(a,b);
    for(int k=15;k>=0;k--){
    
    
        if(depth[fa[a][k]]>=depth[b]){
    
    
            a = fa[a][k];
        }
    }
    if(a==b) return a;
    for(int k=15;k>=0;k--){
    
    
        if(fa[a][k]!=fa[b][k]){
    
    
            a = fa[a][k];
            b = fa[b][k];
        }
    }
    return fa[a][0];
}

int main()
{
    
    
    cin >> n;
    memset(h,-1,sizeof(h));
    int root = 0;
    for(int i=1;i<=n;i++){
    
    
        int a,b;
        cin >> a >> b;
        if(b==-1) root = a; //题目特定输入规则
        add(a,b);
        add(b,a);
    }
    cin >> m;
    bfs(root);
    while(m--){
    
    
        int a,b;
        cin >> a >> b;
        int p = lca(a,b);
        if(p==a) puts("1");
        else if(p==b) puts("2");
        else puts("0");
    }
    return 0;
}

场景2

利用lca可以很容易求出树上任意两点的距离, d i s t ( a , b ) = d i s t ( a , r o o t ) + d i s t ( b , r o o t ) − 2 ∗ d i s t ( l c a ( a , b ) , r o o t ) dist(a,b) = dist(a,root) + dist(b,root) - 2*dist(lca(a,b),root) dist(a,b)=dist(a,root)+dist(b,root)2dist(lca(a,b),root)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 10010 , M = 2*N;
int n,m;
int h[N] , e[M] , ne[M] , w[M] , idx;
int depth[N],fa[N][16];
int dist[N];
queue<int> que;

void add(int a,int b,int c)
{
    
    
    e[idx] = b , w[idx] = c , ne[idx] = h[a] , h[a] = idx ++;
}

void bfs(int root)
{
    
    
    memset(depth,0x3f,sizeof(depth));
    memset(dist,0x3f,sizeof(dist));
    depth[0] = 0;
    depth[root] = 1;
    dist[root] = 0;
    que.push(root);
    while(que.size()){
    
    
        int t = que.front();
        que.pop();
        for(int i=h[t];~i;i=ne[i]){
    
    
            int j = e[i];
            if(depth[j]>depth[t]+1){
    
    
                depth[j] = depth[t] + 1;
                dist[j] = dist[t] + w[i];
                fa[j][0] = t;
                que.push(j);
                for(int k=1;k<=15;k++){
    
    
                    fa[j][k] = fa[fa[j][k-1]][k-1];
                }
            }
        }
    }
}

int lca(int a,int b)
{
    
    
    if(depth[a]<depth[b]) swap(a,b);
    for(int k=15;k>=0;k--){
    
    
        if(depth[fa[a][k]]>=depth[b]){
    
    
            a = fa[a][k];
        }
    }
    if(a==b) return a;
    for(int k=15;k>=0;k--){
    
    
        if(fa[a][k]!=fa[b][k]){
    
    
            a = fa[a][k];
            b = fa[b][k];
        }
    }
    return fa[a][0];
}

int main()
{
    
    
    cin >> n >> m;
    memset(h,-1,sizeof(h));
    for(int i=1;i<n;i++){
    
    
        int a,b,c;
        cin >> a >> b >> c;
        add(a,b,c);
        add(b,a,c);
    }
    int root = 1;
    bfs(root);
    while(m--){
    
    
        int a,b;
        cin >> a >> b;
        int p = lca(a,b);
        cout << dist[a] + dist[b] - 2*dist[p] << endl;
    }
    return 0;
}

场景3

利用lca也可以判断一个节点是否在另一个节点的子树里,即判断lca(a,b)==a||lca(a,b)==b

场景4

如果三个点需要汇聚到某个点,问最短距离和(树的边长都为 1 1 1)。汇聚到的这个点不一定是这三个点的lca,但是可以这样做:设三点分别为 a , b , c a,b,c a,b,c,令 p 1 = l c a ( a , b ) , d 1 = l c a ( p 1 , c ) p_1 = lca(a,b),d_1 = lca(p_1,c) p1=lca(a,b),d1=lca(p1,c)。同理可以求出 p 2 , d 2 , p 3 , d 3 p_2,d_2,p_3,d_3 p2,d2,p3,d3。最后比较一下到 d 1 , d 2 , d 3 d_1,d_2,d_3 d1,d2,d3的距离即可。

猜你喜欢

转载自blog.csdn.net/weixin_43634220/article/details/108545297