SWE / NG · Phone Algorithm

Databricks Fibonacci Tree 两点路径:不建树的 Preorder 区间定位

用子树大小定位 preorder 编号,分别找根到两点路径并在 LCA 拼接,时间空间都随阶数增长。

题目重点:用子树大小定位 preorder 编号,分别找根到两点路径并在 LCA 拼接,时间空间都随阶数增长。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • F(0) 与 F(1) 各一结点;k≥2 时根的左子树 F(k−2)、右子树 F(k−1),前序编号从 0 连续递增;a,b 为有效节点 ID。
  • 输出从 a 到 b 的节点编号序列,包含端点和最近公共祖先一次。题目说‘输出节点序列/思路即可’,具体函数签名为训练实现。

解题路线

  1. 预计算 size[0]=size[1]=1,size[k]=1+size[k−2]+size[k−1];不能仅说‘任意二叉树都有 size(left)+size(right)+1’,关键是阶数决定子树大小,因此不用建节点。
  2. 当前 F(k) 根编号 r 时,左根 r+1,其区间长 size[k−2];右根 r+1+size[k−2],区间长 size[k−1]。比较 target 与区间边界,迭代进入左/右,同时保存根 ID。
  3. 分别求 root→a、root→b 路径,找到最长公共前缀;其最后节点就是 LCA。将 a 路径的非公共后缀逆序、LCA、b 路径的非公共后缀拼接。
  4. k 很大时 size 可能超固定宽整数;Python 用大整数,C++ 需检查溢出或在仅定位给定 target 时做有上界的饱和比较。不能把 pre-order 标签误作节点值相加。

手动走一遍

F(3) 的左是 F(1) 单点、右是 F(2) 三点;前序编号根 0、左 1、右子树根 2,其左 3、右 4。a=1,b=4 时路径为 1→0→2→4,公共前缀仅根 0。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

"""F(k): F(0), F(1) each one node; F(k) has left F(k-2), right F(k-1).
Nodes are labeled from zero in preorder. Find the sequence from a to b
without materializing the tree, using the stated child orientation.
"""


def sizes_up_to(order: int) -> list[int]:
    if order < 0:
        raise ValueError('order must be nonnegative')
    sizes = [1] * (order + 1)
    for level in range(2, order + 1):
        sizes[level] = 1 + sizes[level - 2] + sizes[level - 1]
    return sizes


def path_from_root(order: int, target: int, sizes: list[int]) -> list[int]:
    if not 0 <= target < sizes[order]:
        raise ValueError('target outside tree')
    root, level = 0, order
    path = []
    while True:
        path.append(root)
        if root == target:
            return path
        if level < 2:
            raise AssertionError('unreachable target')
        left_root = root + 1
        left_end = left_root + sizes[level - 2]
        if target < left_end:
            root, level = left_root, level - 2
        else:
            root, level = left_end, level - 1


def path_between(order: int, a: int, b: int) -> list[int]:
    sizes = sizes_up_to(order)
    left = path_from_root(order, a, sizes)
    right = path_from_root(order, b, sizes)
    common = 0
    while common < min(len(left), len(right)) and left[common] == right[common]:
        common += 1
    return list(reversed(left[common:])) + [left[common - 1]] + right[common:]

复杂度

size 表 O(k) 时间/O(k) 空间;每个 root→target 路径长度至多 O(k),拼接 O(k),总 O(k) 时间/O(k) 空间。显式建树需要与节点总数同阶的时间和内存,随 k 近指数增长。

易错点与追问

容易失分的边界

  • Fibonacci 指子树阶数/大小,不是节点值。
  • 左右 F(k−2)/F(k−1) 互换会改变 preorder 标签。
  • 右子树根忘加整个左子树大小。
  • 拼接时 LCA 重复或端点遗漏。
  • 固定宽整数 size 溢出。

继续深挖

  • 只需 a 与 b 距离而不输出路径,能否降低空间?
  • 编号从 1 开始怎么改边界?
  • k 过大时如何避免溢出?
  • 若左右阶数互换,为什么节点总数一样但答案不同?
✓已复制微信号!