题目来源:改编自 LeetCode 235. 二叉搜索树的最近公共祖先。本 Lab 使用课程自定义输入输出协议和独立测试,不复制来源站点的代码或测试。
学习目标
前置知识与环境
先阅读第 8.2 节 二叉排序树的"查找"与"复杂度取决于树高"两节,理解查找路径上比较方向的含义;LCA 恰好是两条查找路径的最后一个公共结点。
在 Lab 目录执行 make doctor 检查环境;未安装 Make 时可在仓库根使用 pnpm 兜底命令(见下文"运行")。
任务
给定一棵以编号 0 为根的二叉搜索树,树中各结点关键字互不相同。对每次查询给出的两个结点编号 u、v,输出它们在树中的最近公共祖先(LCA)的关键字。
LCA 定义:同时以 u、v 为后代(或相等)的深度最大的结点。一个结点可以是它自己的祖先,因此当 u 是 v 的祖先(或两者相同)时,答案就是 u(或该结点本身)。
输入格式
- 第一行:两个整数
n和q,分别为结点数与查询次数; - 接下来
n行:每行四个整数i val l r,表示编号为i的结点关键字为val,左孩子编号为l、右孩子编号为r,-1表示空;输入保证这些结点构成一棵合法 BST,且编号 0 是根; - 接下来
q行:每行两个结点编号u v,表示一次查询。
输出格式
共 q 行,每行一个整数:该查询对应的最近公共祖先的关键字。
数据范围与限制
| 项目 | 范围 |
|---|---|
结点数 n | 1 ≤ n ≤ 5×10⁴ |
查询次数 q | 1 ≤ q ≤ 5×10⁴ |
关键字 val | −10⁹ < val < 10⁹,全树互不相同 |
| 孩子编号 | −1 或 [0, n) 内的合法编号 |
| 查询编号 | u、v 均在树中,可以相等 |
| 时间复杂度要求 | 每次查询沿树走一趟 |
正常、边界与错误情况
- 正常:
u、v分居某结点两侧(或一等于分叉点)时,沿查找路径走到分叉处即答案; - 祖先-后代:
u是v的祖先(或反之)时,答案是较深的那个结点本身,样例 1 的第 3 组查询覆盖该情况; - 退化查询:
u = v,答案是该结点自身的关键字; - 极端形态:树可能退化成一条链(见压力与边界用例),算法不能假设树是平衡的。
样例
样例 1
7 5
0 50 1 2
1 30 3 4
2 70 5 6
3 20 -1 -1
4 40 -1 -1
5 60 -1 -1
6 80 -1 -1
1 6
3 4
1 3
0 0
5 650
30
30
50
70树形为:根 50,左子树 30(20,40),右子树 70(60,80)。逐条解释:
1 6(关键字 20 与 80):一个在根左侧、一个在根右侧,分叉在根,答案 50;3 4(20 与 40):都小于 50 进入左子树,但 40 大于 30,分叉在 30,答案 30;1 3(30 与 20):30 是 20 的祖先,答案是较深的 20 的祖先——即 30 本身,答案 30;0 0:u = v,答案为该结点自身 50;5 6(60 与 80):都大于 50 进入右子树,60 与 80 分居 70 两侧,答案 70。
样例 2
2 3
0 10 1 -1
1 5 -1 -1
0 1
1 1
0 010
5
10两结点构成左链:结点 1(关键字 5)是结点 0(关键字 10)的左孩子。0 1 的公共祖先只有 0,答案 10;1 1 退化为单点查询,答案 5。
实现提示
设本轮查询两关键字的小者为 a、大者为 b,从根出发:若 b < 当前关键字,两目标都在左子树,走进左孩子;若 a > 当前关键字,都走进右孩子;否则当前结点就是分叉点,或恰好等于某个目标(祖先-后代情形),即答案。
不需要真的把 u、v 对应的结点走一遍——只需它们的关键字参与比较,因为 BST 中关键字唯一的结点与查找路径一一对应。若写成"先找 u 的路径、再找 v 的路径、最后比对最后一个公共结点"也能得到正确答案,但要想清楚它为什么等价。
运行
make run
# 免 Make 兜底:在仓库根执行
pnpm lab:run -- labs/chapter-08/exercise/E-08-07-bst-lca需要单用例或严格评分时使用 make run CASE=006-stress-random 与 make score;也可用 pnpm lab:score -- labs/chapter-08/exercise/E-08-07-bst-lca 兜底。
完成清单
思考与复盘
- 如果把"两个结点都在同一侧"的比较写成只看
u的关键字,会在什么输入上出错? - 普通(非搜索)二叉树求 LCA 常用"父指针 + 访问标记"或递归后序。它们为什么不能直接利用 BST 性质?各需多少额外空间?
- 当 q 次查询全部落在同一对结点上时(用例 004),你的实现重复做了哪些工作?若允许离线预处理,可以怎样把总复杂度降到
附近?