一次迭代 DFS 标出进入/离开时刻,把每个子树压成连续区间,祖孙判定化为 O(1) 的区间包含比较,总复杂度 O(n+m)。
OJ: roj
题目 ID: 1557
难度:普及+/提高-
标签:树形结构DFS序树的遍历python
创建: 2026-09-30 17:56
更新: 2026-10-07 11:01
形式化题目
给定一棵
是 的祖先,输出 ; 是 的祖先,输出 ; - 互不为祖先,输出
。
以样例的树为例:根是
graph TD
r234["234(根)"] --- n12["12"]
r234 --- n13["13"]
r234 --- n14["14"]
r234 --- n15["15"]
r234 --- n16["16"]
r234 --- n17["17"]
r234 --- n18["18"]
r234 --- n19["19"]
n19 --- n233["233"]
五个询问依次是
正解
思路
朴素做法先暴露瓶颈。 建树求出每个点的父亲后,对每个询问分别从
DFS 括号序把祖先集合压成一个区间。 对树做一次 DFS,进入节点
反方向也成立:不在
每个询问问两遍方向。
样例走一遍。 按代码的栈序(孩子后压先走,
| 询问 |
包含关系 | 输出 | ||
|---|---|---|---|---|
| 互不包含 | ||||
| 互不包含 | ||||
| 互不包含 | ||||
根的区间完整盖住整棵树
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 18:01
# update_at: 2026-09-30 18:01
import sys
from collections import defaultdict
def assign_times(root: int, graph: dict[int, list[int]]) -> tuple[dict[int, int], dict[int, int]]:
"""迭代 DFS 给有根树标括号序:tin[u] 是 u 的进入时刻,tout[u] 是 u 子树中最后的进入时刻。
栈元素是 (节点, 父节点, 阶段):父节点 None 表示没有(根);阶段 0 进入,记时刻后把
同一节点的离开阶段压回,再压孩子;阶段 1 离开,只收尾。时刻只在进入时递增,且离开时
孩子必然已全部进入,所以 tout[u] 恰等于 u 子树中的最大 tin——子树被压进一个连续区间。
"""
tin: dict[int, int] = {}
tout: dict[int, int] = {}
timer = 0
stack: list[tuple[int, int | None, int]] = [(root, None, 0)]
while stack:
u, parent, stage = stack.pop()
if stage == 0:
timer += 1
tin[u] = timer
stack.append((u, parent, 1)) # 排队等待本子树走完后收尾
stack += [(v, u, 0) for v in graph[u] if v != parent] # 父亲不回头
else:
tout[u] = timer
return tin, tout
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
graph: dict[int, list[int]] = defaultdict(list)
root = -1 # 输入保证会遇到 b = -1 的那一行,把根记在这里
for _ in range(n):
a, b = next(data), next(data)
if b == -1:
root = a
else:
graph[a].append(b)
graph[b].append(a)
tin, tout = assign_times(root, graph)
m = next(data)
out: list[str] = []
for _ in range(m):
x, y = next(data), next(data)
# 祖先的 [tin, tout] 区间完整盖住后代的进入时刻;x ≠ y 时两者不会同时成立
x_is_ancestor = tin[x] <= tin[y] <= tout[x]
y_is_ancestor = tin[y] <= tin[x] <= tout[y]
out.append('1' if x_is_ancestor else '2' if y_is_ancestor else '0')
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 08:09
* update_at: 2026-10-05 08:09
*/
#include <cstdio>
#include <vector>
using namespace std;
typedef long long ll;
// 祖孙询问:DFS 括号序把每棵子树压成一段连续区间,
// "u 是 v 的祖先" 等价于 tin[u] <= tin[v] <= tout[u]。
const int MAXX = 40005; // 节点编号不超过 4×10^4,留几个冗余位置
int n; // 节点个数
int m; // 询问个数
int root; // 根节点编号(输入中 b = -1 的那条边的 a)
vector<int> g[MAXX]; // g[u] 保存与 u 相邻的节点编号
int tin[MAXX]; // tin[u] = DFS 进入 u 的时刻
int tout[MAXX]; // tout[u] = u 子树中最大的进入时刻
int timer; // 全局时刻,只在进入节点时递增
// 显式栈模拟 DFS,每个节点入栈两次(进入阶段、离开阶段),最多 2n 个栈元素
int stack_u[2 * MAXX]; // 栈中节点编号
int stack_par[2 * MAXX]; // 栈中节点的父亲,用来避免走回父亲
int stack_stage[2 * MAXX]; // 0 表示进入该节点,1 表示离开该节点
int top;
// 迭代 DFS 标出括号序,避免链状树递归过深
void build_order() {
top = 0;
stack_u[top] = root;
stack_par[top] = -1;
stack_stage[top] = 0;
top++;
while (top > 0) {
top--;
int u = stack_u[top];
int parent = stack_par[top];
int stage = stack_stage[top];
if (stage == 0) {
timer++;
tin[u] = timer;
// 先把 u 的离开阶段压回栈底,再压孩子,
// 这样孩子全部走完后才轮到 u 收尾,tout[u] 就是子树里最大的 tin
stack_u[top] = u;
stack_par[top] = parent;
stack_stage[top] = 1;
top++;
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (v == parent) {
continue; // 父亲不回头
}
stack_u[top] = v;
stack_par[top] = u;
stack_stage[top] = 0;
top++;
}
} else {
tout[u] = timer;
}
}
}
int main() {
scanf("%d", &n);
root = 0;
for (int i = 1; i <= n; i++) {
int a, b;
scanf("%d%d", &a, &b);
if (b == -1) {
root = a; // 题目保证会出现这样一行
} else {
g[a].push_back(b);
g[b].push_back(a);
}
}
build_order();
scanf("%d", &m);
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d%d", &x, &y);
// x ≠ y 时两个方向的包含关系互斥,先判 x 是否为祖先
if (tin[x] <= tin[y] && tin[y] <= tout[x]) {
printf("1\n");
} else if (tin[y] <= tin[x] && tin[x] <= tout[y]) {
printf("2\n");
} else {
printf("0\n");
}
}
return 0;
}复杂度
- 时间复杂度:
。建邻接表 ,两阶段迭代 DFS 标号每条边检查两次、 , 个询问每个两次整数比较。 - 空间复杂度:
。邻接表、 tin/tout两张时刻表与显式栈各,输出列表 ;实测峰值约 41.1 MB,在 128 MB 限制内。
总结
- 核心观察:DFS 的进入/离开时刻把每个子树压成连续区间,“
是 的祖先” 等价于 “ ”,一次比较替代一次走链。 - 朴素解法
的瓶颈是"每个询问重算固定的祖先集合",正解用 预处理把它压成区间,每询问降到 ,整体 与 C++ 正解同阶。 - 实现要点:DFS 用显式栈的进入/离开两阶段(压回自身的离开阶段后再压孩子,保证配对顺序),
tin/tout用 dict 按原编号存取,兼容编号稀疏;真实数据 10 组全部一致,最大一组 0.059 s、峰值 41.1 MB。