开始前,会这些就够了
- 理解一棵有根树中,每个非根节点只有一个父节点。
- 先读 图与遍历,认识节点、边及不含环的依赖。
组织层级几乎不变,却经常有人问“这个人的第五级上级是谁”。每次一步步往上走能得到答案,但长链上反复查询会重复走很多相同的路。
本项目先为每个节点准备向上 1、2、4、8 层的跳跃表。第五级可以拆成先走 1 层、再走 4 层;超过根时则明确返回 -1,不把缺失位置当成数组下标继续查。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。静态单根树、0为根且父编号小于子编号;874棵树/58058次oracle查询,10类拒绝输入,含k=0/越根/巨大k;无动态更新或最大规模性能结论。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
第五级上级:从当前节点先跳 1 层,再跳 4 层
正在准备实验控件…
固定静态树:0→1→2→3→4→5→6,节点 7 也以 2 为父节点;箭头从父到子。输入编号已满足父先子后。只查询第 k 个祖先,不处理树更新、多个父节点或最低公共祖先。
先明确输入:父节点关系固定,根没有父节点
节点编号为 0 到 n-1,parent[v] 表示 v 的直接父节点;根固定为 0,parent[0] 是 -1。本例还要求每个非根节点的父编号小于自己,这使输入能够从小到大建立深度,并排除环和第二个根。
默认 parent 为 [-1, 0, 1, 2, 3, 4, 5, 2]。其中 0 到 6 是一条链,7 则挂在 2 下面。节点 6 深度为 6,节点 7 深度为 3。
这个编号约定是教学接口的限制,不是所有树天然都有这种编号。若业务编号无顺序,需要先验证树并建立自己的处理顺序或编号映射;这里没有替用户自动完成这一步。
把两段同样长的路拼起来,准备更远的跳跃
up[0][v] 是往上 1 层的位置,也就是直接父节点。要得到往上 2 层的位置,可以先走 1 层,再从中间节点走 1 层;往上 4 层则由两次 2 层拼成。
一般地,up[j][v] 记录往上 2 的 j 次方层的位置。构建这一行时,先看上一行给出的中间节点,再用上一行查这个中间节点的同长度跳跃。每次利用已经算好的短跳跃,组成更长跳跃。
中间节点是 -1 时,结果直接保持 -1。Python 的列表允许负下标,若不检查就访问 previous[-1],会读到最后一个真实节点的信息,制造一个看似合法却错误的祖先。
查询第五级:1 加 4,而不是每一级都重走
5 的二进制是 101,表示 1 + 4。本例从低位到高位处理,因此 query(6, 5) 先用 1 层跳跃从 6 到 5,再用 4 层跳跃从 5 到 1,最终答案为 1。
注意第二次查询的起点已经变成节点 5。不能把每一段都从原节点 6 出发,再把节点编号相加;相加的是层数,节点编号不是距离。
轨迹 jumps 中每项是 (本次层数, 起点, 终点),默认输出为 [(1, 6, 5), (4, 5, 1)]。表中一次查询是一次已准备好的跳跃,不代表没有预处理成本。
零层还是自己,越过根就是不存在
k=0 表示不移动,因此 query(6, 0) 返回 6;从节点 6 往上 6 层正好到根 0,往上 7 层就返回 -1。根的第一级祖先也不存在。
程序先比较 k 和节点深度。超过深度时直接返回 -1,因此对 query(7, 10^12) 不需要走一万亿步,也不需要为这个巨大 k 临时加一万亿层表。只要 k 大于 n,就必然超过任何节点的深度;实际上深度检查可以更早拒绝。
本例的 -1 是缺失结果,顶点编号 -1 作为查询输入反而是错误,应抛 ValueError。结果不存在、输入顶点非法和层数为负,是三种不同情况。
运行跳跃表,再用逐级走法检查所有小树
保存完整代码为 demo.py,执行 python demo.py。2026-10-23 在 Windows、CPython 3.14.0 实际运行,使用标准库。输出先列出跳跃表,再列出普通查询、零层、恰好到根、超过根和超大层数。
程序穷举大小 1–7 且每个父编号小于子编号的全部 874 棵树。对每个节点测试 k=0 到 n+2,并与逐级读取 parent 的独立方法比较,共 58,058 次查询。这个范围包含单节点树、链和分叉。
另外拒绝空树、根标记错误、自指、向后编号形成的无效输入、第二根、布尔父编号,以及非法顶点、负层数、布尔层数,共十类具体输入。这里穷举的是接口允许的编号树,不是声称覆盖所有编号的任意图。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Static rooted tree with parent-before-child numbering, zero-based vertices."""
from itertools import product
def require(condition, message):
if not condition:
raise AssertionError(message)
class Ancestors:
def __init__(self, parents):
if not isinstance(parents, (list, tuple)) or not parents:
raise ValueError("a nonempty parent list or tuple is required")
if type(parents[0]) is not int or parents[0] != -1:
raise ValueError("vertex zero is the root, with parent -1")
for vertex, parent in enumerate(parents[1:], 1):
if type(parent) is not int or not 0 <= parent < vertex:
raise ValueError("each parent must have a smaller vertex number")
self.parents = tuple(parents)
self.depth = [0] * len(parents)
for vertex in range(1, len(parents)):
self.depth[vertex] = self.depth[parents[vertex]] + 1
self.up = [list(parents)]
for _ in range(1, len(parents).bit_length()):
previous = self.up[-1]
self.up.append([-1 if middle == -1 else previous[middle]
for middle in previous])
def kth(self, vertex, distance, trace=False):
if type(vertex) is not int or not 0 <= vertex < len(self.parents):
raise ValueError("invalid vertex")
if type(distance) is not int or distance < 0:
raise ValueError("distance must be a nonnegative integer")
if distance > self.depth[vertex]:
return -1, []
events, level = [], 0
while distance:
if distance & 1:
following = self.up[level][vertex]
if trace:
events.append((1 << level, vertex, following))
vertex = following
distance >>= 1
level += 1
return vertex, events
def oracle(parents, vertex, distance):
for _ in range(distance):
if vertex == -1:
break
vertex = parents[vertex]
return vertex
def main():
parents = [-1, 0, 1, 2, 3, 4, 5, 2]
tree = Ancestors(parents)
print(f"parents={parents}")
print(f"depth={tree.depth}")
for level, row in enumerate(tree.up):
print(f"jump={1 << level}: {row}")
answer, trace = tree.kth(6, 5, trace=True)
require(answer == 1, "binary jumps")
print(f"query(6,5)={answer}; jumps={trace}")
for vertex, distance in [(6, 0), (6, 6), (6, 7), (0, 1), (7, 2), (7, 10**12)]:
result, _ = tree.kth(vertex, distance)
if distance < 100:
require(result == oracle(parents, vertex, distance), "boundary")
else:
require(result == -1, "huge distance")
print(f"query({vertex},{distance})={result}")
rejected = 0
for invalid in [[], [0], [-1, 1], [-1, 2, 1], [-1, -1], [-1, True]]:
try:
Ancestors(invalid)
except ValueError:
rejected += 1
else:
raise AssertionError("invalid parent input accepted")
for vertex, distance in [(-1, 1), (8, 0), (0, -1), (0, True)]:
try:
tree.kth(vertex, distance)
except ValueError:
rejected += 1
else:
raise AssertionError("invalid query accepted")
trees = queries = 0
for size in range(1, 8):
for choices in product(*(range(vertex) for vertex in range(1, size))):
data = [-1, *choices]
current = Ancestors(data)
for vertex in range(size):
for distance in range(size + 3):
actual, _ = current.kth(vertex, distance)
require(actual == oracle(data, vertex, distance), (data, vertex, distance))
queries += 1
trees += 1
print(f"oracle_trees={trees}; oracle_queries={queries}; rejected_inputs={rejected}")
print("PASS")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。静态单根树、0为根且父编号小于子编号;874棵树/58058次oracle查询,10类拒绝输入,含k=0/越根/巨大k;无动态更新或最大规模性能结论。
运行后对照结果
parents=[-1, 0, 1, 2, 3, 4, 5, 2]
depth=[0, 1, 2, 3, 4, 5, 6, 3]
jump=1: [-1, 0, 1, 2, 3, 4, 5, 2]
jump=2: [-1, -1, 0, 1, 2, 3, 4, 1]
jump=4: [-1, -1, -1, -1, 0, 1, 2, -1]
jump=8: [-1, -1, -1, -1, -1, -1, -1, -1]
query(6,5)=1; jumps=[(1, 6, 5), (4, 5, 1)]
query(6,0)=6
query(6,6)=0
query(6,7)=-1
query(0,1)=-1
query(7,2)=1
query(7,1000000000000)=-1
oracle_trees=874; oracle_queries=58058; rejected_inputs=10
PASS
多准备一张表,换来多次查询时少走路
每个节点保存 O(log n) 个跳跃位置,因此预处理时间和表空间均为 O(n log n)。一次合法查询只扫描 k 的二进制位,在本例深度检查后至多 O(log n) 次量级;若 k=0 或超过深度,可直接返回。
逐级走法不需要跳跃表,单次查询成本与实际走过层数有关。如果只问一次、树很浅,准备整张表未必值得。倍增的收益来自多次查询复用同一份准备工作,而非所有场景都会更快。
程序打印整张表只为小例子观察;没有测最大规模、内存峰值或耗时优势。整数位操作按常见机器字模型讨论,超大整数的读取和表示成本也不应该从真实系统预算中消失。
结构改了,旧跳跃表不会自动更新
如果把一个节点换到另一个父节点下面,它和许多后代的深度、跳跃位置都可能变化。修改原 parent 数据并不能让已经建立的表自动同步;本例在构造时保存独立的父关系副本,只针对静态树。
它回答的是单个节点往上 k 层的位置,不是最低公共祖先、任意两点最短路,也不处理一个节点有多个父节点的关系。那些问题可以借用部分思路,但需要新的定义和算法。
复查自己的实现时,重点看中间缺失的保护、k=0、跳跃起点是否逐次更新,以及输入是否真的满足单父节点和无环约定。先守住这些边界,再讨论减少查询工作。
记住这些要点
- 预处理把两段同长度跳跃拼成更长跳跃。
- 二进制拆分相加的是层数,下一次跳跃必须从更新后的节点出发。
- k=0 返回自己,超过深度返回缺失,不能继续用 -1 索引列表。
- 跳跃表属于构建时的静态父关系,结构变动需要重新处理。
别把例子用过头
- 把 -1 当作 Python 的合法负下标,读取最后一个节点。
- 把所有跳跃都从原节点出发,忘记更新当前位置。
- 把父编号必须更小这个示例约定当成任意树的天然性质。
- 只记录每次查询的成本,忽略预处理、空间和更新代价。
自己试一小步
如果输入节点使用无顺序的业务编号,你怎样在保留编号的同时支持第 k 个祖先查询?
- 自画一棵有根树,为节点分配不按父子顺序排列的编号。
- 写出输入校验要求,覆盖重复节点、缺失父节点、多个根和环。
- 设计业务编号与内部节点位置的映射,说明输出如何还原。
- 以逐级走法作独立核对,加入 k=0、恰好到根和超过深度的用例。
做到这些,就算完成
- 不能用“父编号必须小于子编号”拒绝本来合法的业务树。
- 不存在的结果不能和合法编号混淆。
- 说明父关系更新后旧查询表如何处理。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Competitive Programmer’s Handbook,18.1 Finding ancestors (新标签页)↗
第 k 个祖先、按二的幂预处理,以及把 k 拆成二的幂来查询。
- CSES Problem Set:Company Queries I (新标签页)↗
单父节点层级中的第 k 级上级查询、父编号在子编号之前的输入约束,以及不存在时输出 -1 的要求。