AcWing 1172 祖孙询问

本文详细介绍了如何利用向上标记法和树上倍增法解决有向树中查询节点祖孙关系的问题,涉及了算法原理、步骤和代码实现,适用于理解LCA(最近公共祖先)算法在实际场景的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述:

给定一棵包含 n 个节点的有根无向树,节点编号互不相同,但不一定是 1∼n。

有 m 个询问,每个询问给出了一对节点的编号 x 和 y,询问 x 与 y 的祖孙关系。

输入格式

输入第一行包括一个整数 表示节点个数;

接下来 n 行每行一对整数 a 和 b,表示 a 和 b 之间有一条无向边。如果 b 是 −1,那么 a 就是树的根;

第 n+2 行是一个整数 m 表示询问个数;

接下来 m 行,每行两个不同的正整数 x 和 y,表示一个询问。

输出格式

对于每一个询问,若 x 是 y 的祖先则输出 1,若 y 是 x 的祖先则输出 2,否则输出 0。

数据范围

1≤n,m≤4×10^4,
1≤每个节点的编号≤4×10^4

输入样例:

10
234 -1
12 234
13 234
14 234
15 234
16 234
17 234
18 234
19 234
233 19
5
234 233
233 12
233 13
233 15
233 19

输出样例:

1
0
0
0
2

分析:

从这题开始就进入到最近公共祖先(Lowest Common Ancestors)的问题了。对于一棵有根树,一个节点到根结点路径上所有的节点都被称为这个节点的祖先节点,祖先节点中除节点自身外的节点也被称为真祖先节点。对于树上的两个不同节点u和v,其祖先节点必然有一些是重合的,其中深度最大的节点被称为这两个节点的最近公共祖先。

比如上图中的D点和G点的最近公共祖先节点就是B点。首先介绍求解LCA问题的两种基本方法:向上标记法和树上倍增法。

向上标记法

我们自己是如何判断LCA的呢?两只眼睛同时从D点和G点往上追溯,很快就可以定位到B点。计算机向上回溯自然不是问题,但是并不能很快的判断出回溯过程中最先相交于哪一点,因此只能异步的执行节点的回溯。首先D向上追溯到B点、A点,做好访问标记;然后G点向上追溯到E点,再往上到B点发现已经做好标记了,于是B点就是LCA节点。这种求解LCA的办法被称为向上标记法,由其中一个节点向上走到根节点并做好标记,另一个节点再往上走的时候第一次遇见的已标记的节点就是最近公共祖先节点。向上标记法过程相当简单,书上说其复杂度是O(n),即正比于树上节点的规模。其实该算法的复杂度用O(h),也就是树高来衡量更加准确。

树上倍增法

初学树上倍增法的时候可能有些不理解,书中就是给出一个复杂的步骤然后告诉你这样可以求出LCA,这些步骤是怎么来的,为什么需要这种算法,都没有告诉我们。向上标记法看似简单高效,因为只需要O(h)的时间就可以求出LCA了,这个复杂度在树的算法中已经是非常高效的了。我们求上图中D和G的LCA固然很快,但是如果要求多对节点的LCA呢?比如求D、E的,求G、H的,...,我们会发现G点向上走的时候标记了其祖先节点,然后E向上走又会标记一遍祖先节点,向上回溯的路径大都是相同的,这就存在很大的冗余了,这种冗余的来源正是向上标记法异步的特性,如果我们同步的向上回溯会怎样呢?预处理下每个节点向上回溯任意步后的位置,记录下来,然后用某种算法快速的求出两个节点的LCA。最先遇见的困难就是节点u和节点v离他们的LCA节点的深度差不同,如果两个节点处于同一深度,比如G和H点,深度都是3,我们完全可以二分答案了,先判断下G和H向上两步是不是到达了同一个节点,如果没到达,就向上三步。如果两个节点是处于同一深度的,我们在预处理节点向上走若干步的信息后,就可以在O(logh)的时间内求出LCA了。当然既可以使用二分求解也可以使用倍增求解,两个节点同时向上走1,2,4,...步判断是否到达了同一点。那么我们在后面为什么要选择倍增而不是二分呢?这又是个问题。到这里我们其实已经不知不觉的理解了树上倍增法的第一个重要的步骤,将两个节点调整到同一深度上。比如求u和v的LCA,u的深度是10,v的深度是6,首先求出u向上回溯到深度为6的祖先节点u1,如果u1就是v点,那么LCA就是v,否则继续求u1和v的LCA。此时就是求处于同一深度的两个节点的LCA了,就十分方便了。所以树上倍增法无非就是先将深度大的节点调整到与深度小的节点同一深度(向上回溯),然后继续求LCA。

再回到开头说的预处理出所有节点向上回溯若干步的节点,但是这是十分没有必要的。我们只需要预处理出节点向上回溯2的倍数步的节点就可以满足我们所有的需要了。比如要回溯11步,完全可以先走8步,再走2步,再走1步,任何十进制整数肯定是可以转化为二进制表达的,11 = (1011),二进制拆分的思想足以让我们走到任意的地方。为什么是先走步数最大的再逐步缩小步数,而不是先1再2再8呢?因为二进制拆分实现过程中并不需要我们真的去拆分11,我们可以先尝试迈出一个很大的步数,比如16,发现比11大,于是迈出8步,再尝试迈出4步,发现又超过11了,于是迈出2步,最后迈出1步,这是个不断尝试便于实现的倍增过程。调整到同一深度的两个节点需要再次使用倍增的方法求LCA,比如先判断下两个节点向上回溯8步的节点是不是同一个,是就说明LCA不会在更高的位置了,于是重新判断向上回溯4步的是不是同一个,如果不是,则说明LCA还在上面,再对这个迈出4步的两个节点继续向上回溯2步,直至两个节点的父节点就是LCA为止。

下面用算法实现这一过程。设f[i][k]表示节点i向上回溯2^k步的节点标号,边界情况f[i][0]表示i的父节点。f数组的求解就是使用倍增法常用的动态规划公式了,或者叫分而治之,要想求出i向上走2^k步的节点,只需要先求出i向上走2^(k-1)步的节点j,然后再求j向上走2^(k-1)步的节点即可。用状态转移方程表示就是f[i][k] = f[f[i][k-1]][k-1],后者更直观点令j = f[i][k-1],f[i][k] = f[j][k-1],预处理f数组的过程可以在bfs遍历树的过程中顺便实现,同时还可以记录下所有节点的深度depth。

void bfs(int t){
    memset(depth,0x3f,sizeof depth);
    depth[0]= 0,depth[t] = 1;
    int hh = 0,tt = 0;
    q[0] = t;
    while(hh <= tt){
        int u = q[hh++];
        for(int i = h[u];~i;i = ne[i]){
            int j = e[i];
            if(depth[j] > depth[u] + 1){
                depth[j] = depth[u] + 1;
                q[++tt] = j;
                f[j][0] = u;
                for(int k = 1;k <= 15;k++){
                    f[j][k] = f[f[j][k - 1]][k - 1];
                }
            }
        }
    }
}

初始情况下将所有节点的深度设置为无穷大,所以一旦从u可以走到j并且j的深度比u的深度+1还要大时,就说明u是j的父节点,同时可以更新j的深度了。我们知道bfs遍历树的过程是层序遍历,遍历到j时j的祖先节点的信息都已经被预处理过了,所以此时f[j][k] = f[f[j][k - 1]][k - 1];中的f[j][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[f[a][k]] >= depth[b])  a = f[a][k];
    }
    if(a == b)  return a;
    for(int k = 15;k >= 0;k--){
        if(f[a][k] != f[b][k]){
            a = f[a][k];
            b = f[b][k];
        }
    }
    return f[a][0];
}

如果a深度大于b,就交换a、b节点 ,从而保持a节点一直在下面。然后就是按照上面所说的倍增的操作二进制拆分调整b到与a同一深度,如果两点重合,LCA就是a点。否则,继续对a、b向上倍增求LCA,最后的结果为什么是a的父节点呢?这是因为倍增的终点就是a和b调整到LCA节点的下一层。举个例子,比如a和b离LCA有6步,首先a和b都向上走4步,然后想向上走2步发现此时两点重合了,于是只走一步,此时倍增终止,a和b离LCA恰好是一步之遥。总的代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 40005,M = 80005;
int n,idx,h[N],e[M],ne[M];
int depth[N],q[N],f[N][16];
void add(int a,int b){
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
void bfs(int t){
    memset(depth,0x3f,sizeof depth);
    depth[0]= 0,depth[t] = 1;
    int hh = 0,tt = 0;
    q[0] = t;
    while(hh <= tt){
        int u = q[hh++];
        for(int i = h[u];~i;i = ne[i]){
            int j = e[i];
            if(depth[j] > depth[u] + 1){
                depth[j] = depth[u] + 1;
                q[++tt] = j;
                f[j][0] = u;
                for(int k = 1;k <= 15;k++){
                    f[j][k] = f[f[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[f[a][k]] >= depth[b])  a = f[a][k];
    }
    if(a == b)  return a;
    for(int k = 15;k >= 0;k--){
        if(f[a][k] != f[b][k]){
            a = f[a][k];
            b = f[b][k];
        }
    }
    return f[a][0];
}
int main(){
    scanf("%d",&n);
    int a,b,m,root;
    memset(h,-1,sizeof h);
    for(int i = 0;i < n;i++){
        scanf("%d%d",&a,&b);
        if(b == -1) root = a;
        else{
            add(a,b),add(b,a);
        }
    }
    bfs(root);
    scanf("%d",&m);
    while(m--){
        scanf("%d%d",&a,&b);
        int p = lca(a,b);
        if(p == a)  puts("1");
        else if(p == b) puts("2");
        else    puts("0");
    }
    return 0;
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值