AcWing 1172 祖孙询问
一、题目描述
给定一棵包含 n 个节点的有根无向树,节点编号互不相同,但不一定是 1∼n。
有 m 个询问,每个询问给出了一对节点的编号 x 和 y,询问 x 与 y 的祖孙关系。
输入格式
第一行一个整数 n 表示节点个数;
接下来 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⁴, 1 ≤ 每个节点编号 ≤ 4×10⁴
输入样例:
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
二、最近公共祖先(LCA)算法
基本概念
在一棵有根树中,两个节点的最近公共祖先(LCA)是它们所有公共祖先中离它们最近的节点。节点本身也可以作为其祖先节点。
两种求解方法
1. 向上标记法
- 从其中一个节点向上走到根节点并做标记
- 另一个节点向上走时遇到的第一个已标记节点就是LCA
- 时间复杂度:O(h),h为树高
2. 树上倍增法
更高效的预处理方法,适合多次查询。
核心思想:
- 预处理每个节点向上2^k步的祖先节点
- 将两个节点调整到同一深度
- 同步向上倍增查找LCA
实现步骤:
- 预处理f数组:f[i][k]表示节点i向上2^k步的节点
- 边界条件:f[i][0]是i的父节点
- 状态转移:f[i][k] = f[f[i][k-1]][k-1]
三、算法实现
#include <bits/stdc++.h>
using namespace std;
// #define int long long
const int N = 4e4 + 10, M = N << 1;
int h[N], e[M], ne[M], idx;
int n, m;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int q[N];
// LCA
int f[N][16];
int depth[N];
// 预处理倍增数组和深度数组
void bfs(int x)
{
int tt = -1, hh = 0;
q[++tt] = x;
depth[0] = 0;
depth[x] = 1;
while (hh <= tt)
{
auto u = q[hh++];
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
if (!depth[v])
{
depth[v] = depth[u] + 1;
f[v][0] = u;
q[++tt] = v;
}
for (int k = 1; k <= 15; k++)
f[v][k] = f[f[v][k - 1]][k - 1];
}
}
}
int LCA(int a, int b)
{
if (depth[a] < depth[b])
swap(a, b); // 确保a在b的下面(深处)
// 将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];
}
void solve()
{
// 初始化,很重要
memset(h, -1, sizeof h);
cin >> n;
int root = 0;
for (int i = 1; i <= n; i++)
{
int a, b;
cin >> a >> b;
if (b == -1)
root = a;
else
add(a, b), add(b, a);
}
bfs(root);
cin >> m;
for (int i = 1; i <= m; i++)
{
int x, y;
cin >> x >> y;
int op = LCA(x, y);
if (op == x)
cout << 1 << "\n";
else if (op == y)
cout << 2 << "\n";
else
cout << 0 << "\n";
}
}
signed main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
solve();
return 0;
}
四、关键点说明
- 倍增数组大小选择:
- 对于4×10⁴的节点数,最大深度不超过2¹⁶
- 因此f数组第二维设为16足够
- 算法优化:
- 预处理阶段使用BFS保证按层次处理节点
- 查询阶段使用二进制拆分思想快速定位LCA
- 边界处理:
- 根节点的父节点设为0
- 确保深度差调整时不会越界
五、总结
LCA算法本质是利用二进制拆分思想:
- 深度对齐
- 倍增查找
- 动态规划预处理
- 尝试从大到小的步数调整
这种方法将每次查询的时间复杂度优化到O(log h),非常适合处理大规模树的祖孙关系查询问题。