祖孙询问
题目描述
前置知识
最近公共祖先(LCA)是指有根树中距离两个节点最近的公共祖先。祖先是指从当前节点到树根路径上的所有节点。
如下图所示:
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。
如下图所示:
我们可以使用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]。
如下图所示:
从图中可以看出 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;
如下图所示:
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} 2j−1步得到 F [ i , j − 1 ] F[i,j-1] F[i,j−1]
- 然后再从
F
[
i
,
j
−
1
]
F[i,j-1]
F[i,j−1]这个节点出发向根节点走
2
j
−
1
2^{j-1}
2j−1步,得到
F[F[i,j-1],j-1]
,那么此时走到的节点即为 F [ i , j − 1 ] F[i,j-1] F[i,j−1]
如下图所示:
递推公式: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
2k≤n,
k
=
l
o
g
n
k=logn
k=logn
树上倍增法的两个关键步骤:
- 先将两个点跳到同一层
- 让两个点同时向上跳,一直跳到它们的最近公共祖先的下一层为止
也就是说先让深度大的 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处于同一深度。
如下图所示:
总结:
按照增量递减的方式,如果到达节点的深度比 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]
- 减少增量, 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]就是答案
完整的求解过程如下图所示:
总结:按照增量递减的方式,如果到达的节点相同时,说明是公共祖先,但是不能保证就是最近公共祖先节点,所以什么也不做;如果到达的节点不相同,那么 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]时什么都不做,而且为什么此时就是公共祖先而不能是最近公共祖先呢?
如下图所示:
增量从大到小逐渐递减,设 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
2k≤40000,所以
k
≤
16
k\leq16
k≤16。也就是说我们要给这个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。
如何理解处理超过根节点呢?
举个栗子:
因为我们没有对根节点 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;
}