qinghuan
← 回到知识探索

计算机趣闻 / 可下载小项目

往上找第五级上级,为什么可以只跳两次?

预先记录向上 1、2、4、8 层的位置,把查询拆成几次跳跃,同时守住根节点和缺失下标的边界。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先明确输入:父节点关系固定,根没有父节点
  4. 把两段同样长的路拼起来,准备更远的跳跃
  5. 查询第五级:1 加 4,而不是每一级都重走
  6. 零层还是自己,越过根就是不存在
  7. 运行跳跃表,再用逐级走法检查所有小树
  8. 多准备一张表,换来多次查询时少走路
  9. 结构改了,旧跳跃表不会自动更新
  10. 记住这些要点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
  • 理解一棵有根树中,每个非根节点只有一个父节点。
  • 先读 图与遍历,认识节点、边及不含环的依赖。
遇到不熟悉的词,先看这 2 个解释
倍增
先保存1步、2步、4步等距离的结果,再组合这些现成跳跃完成查询。
缺失哨兵
专门约定一个不代表真实节点的值,表示这个祖先不存在。
打开完整术语小抄 ↗

组织层级几乎不变,却经常有人问“这个人的第五级上级是谁”。每次一步步往上走能得到答案,但长链上反复查询会重复走很多相同的路。

本项目先为每个节点准备向上 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 层

正在准备实验控件…

树的父关系为 [-1, 0, 1, 2, 3, 4, 5, 2]。缺失结果用 −1 表示,构表时绝不把它继续当下标。

起点深度
6
目标层数的二进制
101
当前位置
6

还有 2 次跳跃。每次都从当前节点出发。

    打开已经准备好的跳跃表
    先花时间和空间构表,再复用这些跳跃
    层数节点0节点1节点2节点3节点4节点5节点6节点7
    1缺失0123452
    2缺失缺失012341
    4缺失缺失缺失缺失012缺失
    8缺失缺失缺失缺失缺失缺失缺失缺失

    固定静态树: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 次查询。这个范围包含单节点树、链和分叉。

    另外拒绝空树、根标记错误、自指、向后编号形成的无效输入、第二根、布尔父编号,以及非法顶点、负层数、布尔层数,共十类具体输入。这里穷举的是接口允许的编号树,不是声称覆盖所有编号的任意图。

    展开收起代码 · 小实验python · 104 行

    长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

    小实验 · python
    """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 个祖先查询?

    1. 自画一棵有根树,为节点分配不按父子顺序排列的编号。
    2. 写出输入校验要求,覆盖重复节点、缺失父节点、多个根和环。
    3. 设计业务编号与内部节点位置的映射,说明输出如何还原。
    4. 以逐级走法作独立核对,加入 k=0、恰好到根和超过深度的用例。

    做到这些,就算完成

    • 不能用“父编号必须小于子编号”拒绝本来合法的业务树。
    • 不存在的结果不能和合法编号混淆。
    • 说明父关系更新后旧查询表如何处理。

    查原始资料

    想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

    • Competitive Programmer’s Handbook,18.1 Finding ancestors (新标签页)↗Antti Laaksonen / CSES · 英文

      第 k 个祖先、按二的幂预处理,以及把 k 拆成二的幂来查询。

      2026-10-23 实际打开并阅读:印刷页 163–164(文件页 173–174)的 Finding ancestors 整节,到 Subtrees and paths 标题前;教材使用的缺失编号与本文 -1 约定分开说明。仅声明该范围,不表示通读整部资料。

    • CSES Problem Set:Company Queries I (新标签页)↗CSES / University of Helsinki · 英文

      单父节点层级中的第 k 级上级查询、父编号在子编号之前的输入约束,以及不存在时输出 -1 的要求。

      2026-10-23 实际打开并阅读:题面、Input、Output、Constraints 与 Example;本文增加 k=0 和超大 k 的边界,没有声称提交评测或覆盖题目最大规模。仅声明该范围,不表示通读整部资料。