祖孙询问-

祖孙询问


题目描述

image-20210721211507323


前置知识

最近公共祖先(LCA)是指有根树中距离两个节点最近的公共祖先。祖先是指从当前节点到树根路径上的所有节点。

如下图所示:

image-20210721211850774

u u u v v v的公共祖先是指一个节点它既是 u u u的祖先,又是 v v v的祖先。 u u u v v v的最近公共祖先是指距离 u u u v v v最近的祖先。如果 v v v u u u的祖先,那么 u u u v v v的最近公共祖先就是 v v v

如下图所示:

image-20210721212059201

我们可以使用LCA来求解树上任意两点之间的距离。求 u u u v v v这两个之间的距离时,设为 L L L,如果 u u u v v v的最近公共祖先为lca,则 u u u v v v之间的距离为 u u u到树根的距离 d i s t [ u ] dist[u] dist[u]加上 v v v到树根的距离 d i s t [ v ] dist[v] dist[v]再减去2倍的lca到树根的距离 2 × d i s t [ l c a ] 2\times dist[lca] 2×dist[lca]。即有: L = d i s t [ u ] + d i s t [ v ] − 2 × d i s t [ l c a ] L=dist[u]+dist[v]-2\times dist[lca] L=dist[u]+dist[v]2×dist[lca]

如下图所示:

image-20210721212511915

从图中可以看出 u u u到lca的距离加上 v v v到lca的距离,其实就是我们想要求的 u u u v v v之间的距离,设 u u u到lca的距离 L 1 L_1 L1,设 v v v到lca的距离为 L 2 L_2 L2。可知: d i s t [ u ] = L 1 + d i s t [ l c a ] dist[u]=L_1+dist[lca] dist[u]=L1+dist[lca],所以 L 1 = d i s t [ u ] − d i s t [ l c a ] L_1=dist[u]-dist[lca] L1=dist[u]dist[lca] d i s t [ v ] = L 2 + d i s t [ l c a ] dist[v]=L_2+dist[lca] dist[v]=L2+dist[lca],所以 L 2 = d i s t [ v ] − d i s t [ l c a ] L_2=dist[v]-dist[lca] L2=dist[v]dist[lca]。所以 L = L 1 + L 2 = d i s t [ u ] − d i s t [ l c a ] + d i s t [ v ] − d i s t [ l c a ] = d i s t [ u ] + d i s t [ v ] − 2 × d i s t [ l c a ] L=L_1+L_2=dist[u]-dist[lca]+dist[v]-dist[lca]=dist[u]+dist[v]-2\times dist[lca] L=L1+L2=dist[u]dist[lca]+dist[v]dist[lca]=dist[u]+dist[v]2×dist[lca]

树上倍增法

F[i,j]表示 i i i 2 j 2^j 2j辈祖先,即 i i i节点向根节点走 2 j 2^j 2j步所到达的那个节点。

u u u节点向上走 2 0 2^0 20步,则为 u u u的父节点 x x x F [ u , 0 ] = x F[u,0]=x F[u,0]=x u u u节点向上走 2 1 2^1 21步,到达节点 y y y F [ u , 1 ] = y F[u,1]=y F[u,1]=y u u u节点向上走 2 2 2^2 22步,到达节点 z z z F [ u , 2 ] = z F[u,2]=z F[u,2]=z u u u节点向上走 2 3 2^3 23步,节点不存在, F [ u , 3 ] = 0 F[u,3]=0 F[u,3]=0

如下图所示:

image-20210721213503867

F[i,j]表示 i i i 2 j 2^j 2j辈祖先,即 i i i节点向根节点走 2 j 2^j 2j步所到达的那个节点。那么该如何走到 F [ i , j ] F[i,j] F[i,j]这个节点呢?可以分为两个步骤:

  • i i i节点先向根节点走 2 j − 1 2^{j-1} 2j1步得到 F [ i , j − 1 ] F[i,j-1] F[i,j1]
  • 然后再从 F [ i , j − 1 ] F[i,j-1] F[i,j1]这个节点出发向根节点走 2 j − 1 2^{j-1} 2j1步,得到F[F[i,j-1],j-1],那么此时走到的节点即为 F [ i , j − 1 ] F[i,j-1] F[i,j1]

如下图所示:

image-20210721213844186

递推公式:F[i,j]=F[F[i][j-1],j-1] i = 1 , 2 , ⋯   , n i=1,2,\cdots,n i=1,2,,n j = 1 , 2 , ⋯   , k j=1,2,\cdots,k j=1,2,,k 2 k ≤ n 2^k\leq n 2kn k = l o g n k=logn k=logn

树上倍增法的两个关键步骤

  1. 先将两个点跳到同一层
  2. 让两个点同时向上跳,一直跳到它们的最近公共祖先的下一层为止

也就是说先让深度大的 y y y向上走到与 x x x处于同一深度,然后 x , y x,y x,y一起向上走。但是这里是按照倍增思想走的,而不是一步一步往上走的,因此速度比较快。

现在来思考两个问题:

  • 怎么让深度大的 y y y向上走与 x x x处于同一深度呢?
  • x x x y y y一起向上走,怎么找到最近的公共祖先呢?

先来解决第一个问题:怎么让深度大的 y y y向上走与 x x x处于同一深度呢?

假设 y y y的深度比 x x x的深度大,需要 y y y向上走到与 x x x处于同一深度, k = 3 k=3 k=3,则求解过程如下:

  • y y y向上走 2 3 2^3 23步,如果此时到达节点的深度比 x x x的深度小(说明跳的太远了,跳到了 x x x的上面了),那么什么也不做
  • 减小增量, y y y向上走 2 2 2^2 22步,如果此时到达节点的深度比 x x x的深度大(说明此时还处于 x x x的下面),则 y y y向上移动,令 y = F [ y ] [ 2 ] y=F[y][2] y=F[y][2]
  • 减小增量, y y y向上走 2 1 2^1 21步,如果此时到达节点的深度比 x x x的深度相等(说明此时它俩处于同一深度),则 y y y向上移动,令 y = F [ y ] [ 1 ] y=F[y][1] y=F[y][1]
  • 减小增量, y y y向上走 2 0 2^0 20步,如果此时到达节点的深度比 x x x的深度小,那么什么也不做。由于上一次 y y y已经与 x x x处于同一深度了,而这次啥也没做,因此 y y y仍然与 x x x处于同一深度。

如下图所示:

image-20210721215345942

总结:

按照增量递减的方式,如果到达节点的深度比 x x x的深度还要小,则什么也不做;如果到达节点的深度 ≥ x \geq x x的深度时,则 y y y向上移动,令 y = F [ y ] [ 2 ] y=F[y][2] y=F[y][2],一直循环到增量为0,那么最后一定会有 x x x y y y处于同一深度。

再来解决第二个问题: x x x y y y一起向上走,怎么找到最近的公共祖先呢?

假设 x , y x,y x,y已经处于同一深度了,现在一起向上走, k = 3 k=3 k=3,则其求解过程如下:

  • x x x y y y同时向上走 2 3 2^3 23步,如果到达的节点相同,说明是公共祖先节点,但是不能保证是最近公共祖先,于是什么也不做
  • 减少增量, x , y x,y x,y同时向上走 2 2 2^2 22步,如果此时到达的节点不相同,则说明肯定不是最近公共祖先节点,那么此时 x , y x,y x,y都要向上移动,令 x = F [ x ] [ 2 ] x=F[x][2] x=F[x][2] y = F [ y ] [ 2 ] y=F[y][2] y=F[y][2]
  • image-20210721220404938
  • 减少增量, x , y x,y x,y同时向上走 2 1 2^1 21步,如果此时到达的节点不同,那么此时 x , y x,y x,y都要向上移动,令 x = F [ x ] [ 1 ] x=F[x][1] x=F[x][1] y = F [ y ] [ 1 ] y=F[y][1] y=F[y][1]
  • 减少增量, x x x y y y同时向上走 2 0 2^0 20步,如果到达的节点相同,什么也不做
  • 因此经过这么做,那么最终我们一定让 x , y x,y x,y都处于它俩的最近公共祖先的下一层了。此时 x , y x,y x,y的父节点就是最近公共祖先节点了,那么最终只需要 x x x或者 y y y向上走一步就可以到达最近公共祖先了。即最终的 F [ x ] [ 0 ] F[x][0] F[x][0]或者 F [ y ] [ 0 ] F[y][0] F[y][0]就是答案
  • image-20210721220600144

完整的求解过程如下图所示:

image-20210721220655472

总结:按照增量递减的方式,如果到达的节点相同时,说明是公共祖先,但是不能保证就是最近公共祖先节点,所以什么也不做;如果到达的节点不相同,那么 x , y x,y x,y同时往上走,一直到增量为0。此时 x , y x,y x,y的父节点就是最近公共祖先节点了。

问题:为什么碰到 F [ x ] [ k ] = = F [ y ] [ k ] F[x][k]==F[y][k] F[x][k]==F[y][k]时什么都不做,而且为什么此时就是公共祖先而不能是最近公共祖先呢?

如下图所示:

image-20210722134122538

增量从大到小逐渐递减,设 k = 2 k=2 k=2,那么 F [ x ] [ 2 ] = F [ y ] [ 2 ] = 0 F[x][2]=F[y][2]=0 F[x][2]=F[y][2]=0,但是很明显,此时的 0 0 0号节点只是它俩的公共祖先而已,此时我们啥也不做,也就是说 x x x y y y仍然原地踏步,并没有发生跳跃;然后 k k k–,此时 k = 1 k=1 k=1,那么 F [ x ] [ 1 ] = F [ y ] [ 1 ] = 2 F[x][1]=F[y][1]=2 F[x][1]=F[y][1]=2,但是很明显,此时的 2 2 2号节点只是它俩的公共祖先而已,此时我们啥也不做,也就是说 x x x y y y仍然原地踏步,并没有发生跳跃;然后 k k k–,此时 k = 0 k=0 k=0,那么 F [ x ] [ 0 ] = F [ y ] [ 0 ] = 4 F[x][0]=F[y][0]=4 F[x][0]=F[y][0]=4,注意此时我们可以直观地看出来 4 4 4是它俩的最近公共祖先,但是由于 F [ x ] [ 0 ] = F [ y ] [ 0 ] F[x][0]=F[y][0] F[x][0]=F[y][0],因此我们也是什么也不做,也就是说 x x x y y y仍然原地踏步,并没有发生跳跃;然后 k k k–,此时 k k k为-1,退出循环了。那么,我们发现 x x x y y y确实就是在最近公共祖先的下一层!所以最后 x x x或者 y y y再向上跳一步就到了最近公共祖先节点了,即 F [ x ] [ 0 ] F[x][0] F[x][0]或者 F [ y ] [ 0 ] F[y][0] F[y][0]就是答案了。


核心思路

主要就是利用树上倍增法来求解 L C A LCA LCA

这里最多有 N = 40000 N=40000 N=40000个节点,那么 2 k ≤ 40000 2^k\leq40000 2k40000,所以 k ≤ 16 k\leq16 k16。也就是说我们要给这个F[N][]数组的第二维开的大小应该是 16 16 16,即有 16 16 16位二进制。

还有就是我们用 d e p t h [ 0 ] = 0 depth[0]=0 depth[0]=0来当作哨兵,它有什么用处呢?假设从 i i i开始跳 2 j 2^j 2j步,那么有可能此时已经跳出了整棵树的根节点,那么fa[fa[j][k-1]][k-1]=0,那么到达的那个虚无节点 f a [ i ] [ j ] = 0 fa[i][j]=0 fa[i][j]=0,于是 d e p t h [ f a [ i ] [ j ] ] = d e p t h [ 0 ] = 0 depth[fa[i][j]]=depth[0]=0 depth[fa[i][j]]=depth[0]=0

如何理解处理超过根节点呢?

举个栗子:

image-20210722140317627

因为我们没有对根节点 f a [ 1 ] [ 0 ] fa[1][0] fa[1][0]赋值,所以它使用的是全局变量,于是 f a [ 1 ] [ 0 ] = 0 fa[1][0]=0 fa[1][0]=0。可以知道, f a [ 2 ] [ 0 ] = 1 fa[2][0]=1 fa[2][0]=1。假设从 2 2 2节点跳了 2 1 2^1 21步,那么就会跳出根节点,此时的处理是这样的: f a [ 2 ] [ 1 ] = f a [ f a [ 2 ] [ 0 ] ] [ 0 ] = f a [ 1 ] [ 0 ] = 0 fa[2][1]=fa[fa[2][0]][0]=fa[1][0]=0 fa[2][1]=fa[fa[2][0]][0]=fa[1][0]=0,那么 d e p t h [ f a [ 1 ] [ 0 ] = d e p t h [ 0 ] depth[fa[1][0]=depth[0] depth[fa[1][0]=depth[0],但是此时 d e p t h [ 0 ] depth[0] depth[0]还没有定义,那么我们可以给它定义为0,此时就可以表示跳出了根节点了。

还有个细节要注意,我们是让深度更大的先往上跳到与深度较小的节点处于同一层。我们不妨假设 a a a是深度较大的, b b b是深度较小的,那么如果 d e p t h ( a ) < d e p t h ( b ) depth(a)<depth(b) depth(a)<depth(b),则交换,也就是说必须保证 a a a的深度是 ≥ b \geq b b的深度,然后对 a a a往上跳。


代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
//对于树来说 有n个顶点 则有n-1条边  
//由于题目说了是无向边,所以最多有2(n-1)条边
const int N=40010,M=N*2;
int n,m;
int h[N],e[M],ne[M],idx;
//记录每个节点的在树中的深度
int depth[N];
//fa[i][j]表示从i节点出发跳2^j步后所到达的节点
int fa[N][16];
//宽搜的队列
int q[N];
void add(int a,int b)
{
    e[idx]=b;
    ne[idx]=h[a];
    h[a]=idx++;
}
//采用bfs,在构建这棵有根树的过程中,预处理出fa[i][j],预处理的时间复杂度是nlogn
void bfs(int root)
{
    //初始化深度为无穷大
    memset(depth,0x3f,sizeof depth);
    //由于已知了q[0]是根节点  所以这里直接让tt=0了
    int hh=0,tt=0;
    q[0]=root;
    //哨兵 如果跳超过了根节点,那么就设那些超过根节点的虚无节点的深度为0
    depth[0]=0;
    //根节点的深度为1
    depth[root]=1;
    //进行宽搜
    while(hh<=tt)
    {
        int t=q[hh++];//取出队头元素
        //遍历t的所有邻接点
        for(int i=h[t];~i;i=ne[i])
        {
            int j=e[i];//取出t的邻接点j
            //说明j还没有倍搜索过
            if(depth[j]>depth[t]+1)
            {
                //更新顶点j的深度
                depth[j]=depth[t]+1;
                //将j加入队列中
                q[++tt]=j;
                //由于赋值后depth[j]=depth[t]+1 说明j的深度大于t的深度
                //那么从j出发跳2^0步后,到达的节点就是t
                fa[j][0]=t;
                //上面已经处理了从j出发跳2^0步了
                //这里是处理从j出发跳2^1,2^2,...,2^15步所能到达的节点
                for(int k=1;k<=15;k++)
                    fa[j][k]=fa[fa[j][k-1]][k-1];
            }
        }
    }
}
//树上倍增法求出顶点a和顶点b的最近公共祖先
int LCA(int a,int b)
{
    //约定顶点a的深度必须>=顶点b的深度,否则就要交换
    if(depth[a]<depth[b])
        swap(a,b);
    //让深度较大的顶点a先往上跳到与深度较小的顶点b处于同一层(有可能a跳到了b身上)
    for(int k=15;k>=0;k--)
    {
        int tmp=fa[a][k];
        //此时跳到了tmp这个节点 如果这节点的深度>=顶点b的深度
        //说明tmp这个节点还没有跳超过顶点b这一层   那么则让a跳到tmp这个节点
        if(depth[tmp]>=depth[b])
            a=tmp;
    }
    //这里说明a跳到了b身上 此时两个重合 最近公共祖先就是a或者b
    if(a==b)
        return a;
    //如果能到这里,就说明a跳到了与b处于同一层
    //那么此时让a和b同时往上跳
    for(int k=15;k>=0;k--)
    {
        //说明它俩跳到的顶点不相同,那么不是公共祖先,则必然不可能是LCA
        if(fa[a][k]!=fa[b][k])
        {
            //让a跳到fa[a][k]这个顶点
            a=fa[a][k];
            //让b跳到fa[b][k]这个顶点
            b=fa[b][k];
        }
    } 
    //退出循环后,a和b一定是跳到了它俩的最近公共祖先节点的下一层
    //那么此时它俩再向上跳2^0=1步就可以找到LCA了
    //因此最终返回 fa[a][0] 或者 fa[b][0]  都是可以的
    return fa[a][0];      
}
int main()
{
    memset(h,-1,sizeof h);
    //整棵树的根节点
    int root=0;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        //根据题目意思,如果b为-1,那么a就是整棵树的根节点
        if(b==-1)
            root=a;
        else
        {
            //建立无向边
            add(a,b);
            add(b,a);
        }    
    }
    //在构建这棵有根树的过程中,预处理出fa[i][j]
    bfs(root);
    scanf("%d",&m);
    //处理m个询问
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        //找到顶点a和顶点b的最近公共祖先
        int p=LCA(a,b);
        //如果a是它俩的最近公共祖先
        if(p==a)
            puts("1");
        //如果b是它俩的最近公共祖先    
        else if(p==b)
            puts("2");
        else
            puts("0");        
    }
    return 0;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值