1. 问题描述:
给定一棵包含 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
来源:https://www.acwing.com/problem/content/description/1174/
2. 思路分析:
分析题目可以知道我们需要求解节点x和节点y的最近公共祖先,求解最近公共祖先其实有很多种求解方法,其中有一种比较常用的方法是基于倍增的思想,这个方法需要先预处理fa和depth数组,其中fa[i][j]表示从节点i往上走2 ^ j步到达的节点,0 <= j <= ⌊logn⌋,因为n最大为4 × 10 ^ 4,所以从某个节点出发最多能够走2 ^ 16步,也即第二维最大为16,depth[i]表示节点i的深度,求解树中节点的深度可以使用bfs或者dfs,因为节点数目比较多,所以为了避免爆栈的问题我们可以使用bfs来求解每一个节点的深度,使用bfs求解节点的深度的时候有一个好处是不需要使用标记数组来标记哪些节点已经访问过了,每一个节点只会被访问一次,在bfs遍历邻接点的时候我们可以顺带求解fa数组,对于fa数组我们可以使用迭代的方法进行求解,主要分为两种情况:
- j = 0,fa[i][j] = t
- j > 0,fa[i][j] = fa[fa[i][j - 1]][j - 1],其实是分为两步来走,第一步从i走2 ^ (j - 1)步到达的节点,第二步从这个节点再走2 ^ (j - 1)步那么就是i走2 ^ j步对应的节点。
预处理好了fa数组和depth数组之后那么接下来就是求解两个节点的最近公共祖先了,也是分为两个步骤进行求解:
① 先将两个点往上跳到同一层,其实是深度较大的节点往上跳到与深度较小的节点的同一层,跳的时候其实是基于二进制拼凑的思想,假设x的深度大于等于y的深度,当前需要跳的深度为t = depth(x) - depth(y),2 ^ 0,2 ^ 1... 2 ^ k,我们从高位的二进制权重开始枚举,只要t >= 2 ^ k说明二进制位就包含当前的第k位那么就可以跳(也即判断x往上跳2 ^ k步到达的深度是否大于等于y的深度,大于等于就跳),具体在实现的时候其实并不需要将值计算出来,使用depth(fa(x, k)) >= depth(y),当两个节点在同一层的时候那么就不满足条件了,此时节点x和节点y在同一层。
② 让两个节点继续往上跳,一直跳到fa(a,k) = fa(b,k),他们的下一层节点就是答案,为什么当fa(a,k) = fa(b,k)不是他们的最近公共祖先呢?其实当fa(a,k) = fa(b,k)相等的时候只能够说明当前的节点是他们的祖先,但是并不能够说明当前的节点是他们的最近公共祖先,因为有可能在往上跳的时候跳过了最近公共祖先,也即跳到了最近公共祖先的上面一个节点,所以当两者相等的时候当前节点往上跳一步就是他们的最近公共祖先,例如对于下图中的节点x和节点y,当两个节点同时往上跳一步的时候到达节点2和节点3,再往上跳两步的时候跳出了树的范围,此时下一层节点1才是我们的答案(所以0这个编号的节点相当于是一个哨兵可以帮助我们避免边界上的问题)。这两个节点往上跳的时候其实也是基于二进制的思想,从大到小枚举k,只要是fa(a,k) != fa(b,k)那么就继续往上跳,直到两者相等,此时fa(a,k) = fa(b,k),说明a往上跳k步的节点等于b往上跳k步的节点,此时节点a或者节点b往上跳一步就是x和y的最近公共祖先,也即fa[a][0]或者fa[b][0]就是答案,这里编号为0的节点作为哨兵的第二个好处是当我们跳出根节点之后if判断是不成立的。
3. 代码如下:
倍增思想:
import collections
from typing import List
class Solution:
# 树中的节点都是int类型的
# bfs求解深度的一个好处是不容易爆栈而且是无向图也不用标记是否已经被访问了, 因为第一次访问到的距离肯定是最短的
def bfs(self, root: int, fa: List[List[int]], depth: List[int], g: List[List[int]]):
q = collections.deque()
q.append(root)
# 这里0号点充当哨兵的作用
depth[0] = 0
depth[root] = 1
while q:
p = q.popleft()
for next in g[p]:
j = next
if depth[j] > depth[p] + 1:
depth[j] = depth[p] + 1
# 加入队列
q.append(j)
# 预处理fa列表, k = 0是边界为跳2 ^ 0 = 1步
fa[j][0] = p
# 因为节点个数最多为4 * 10 ** 4, 所以最多跳2 ^ 17, 这里0号点作为哨兵的好处是可以当跳出根节点之后那么节点的祖先是0, 可以避免边界问题
for k in range(1, 17):
# 从当前节点跳2 ^ (k - 1)步到某一个节点然后从那个节点再往上跳2 ^ (k - 1)步那么相当于是从j这个节点往上跳2 ^ j步
fa[j][k] = fa[fa[j][k - 1]][k - 1]
def lca(self, a: int, b: int, fa: List[List[int]], depth: List[int]):
# 确保a的深度比b的深度要大
if depth[a] < depth[b]:
a, b = b, a
# 1. 首先是从将深度较大的点跳到与深度较低的高度
for k in range(16, -1, -1):
if depth[fa[a][k]] >= depth[b]:
# a往上跳直到两者的深度相等
a = fa[a][k]
# 说明b是a的祖先直接返回即可
if a == b: return a
# 2. 两个节点同时往上跳直到fa[a][k] = fa[b][k], 说明a再往上跳一步就是当前的最近公共最先
for k in range(16, -1, -1):
if fa[a][k] != fa[b][k]:
a = fa[a][k]
b = fa[b][k]
# 节点a往上跳1步就是两个节点的最近公共祖先
return fa[a][0]
# 需要理解其中的跳的过程
def process(self):
# n个节点
n = int(input())
root = 0
# 注意节点编号不一定是1~n
g = [list() for i in range(5 * 10 ** 4)]
for i in range(n):
a, b = map(int, input().split())
if b == -1:
# 当前的a是根节点
root = a
else:
# 注意是无向边
g[a].append(b)
g[b].append(a)
INF = 10 ** 10
fa, depth = [[0] * 17 for i in range(5 * 10 ** 4)], [INF] * (5 * 10 ** 4)
# bfs预处理fa和depth列表
self.bfs(root, fa, depth, g)
# m个询问
m = int(input())
for i in range(m):
x, y = map(int, input().split())
t = self.lca(x, y, fa, depth)
if t == x: print(1)
elif t == y: print(2)
else: print(0)
if __name__ == "__main__":
Solution().process()