Codeforces 树上查询三题 从祖先区间到 LCA 与等距点计数
树上的查询不一定需要复杂数据结构。更重要的是先看清:询问的是一条根路径、两点之间的行走,还是一群满足距离关系的点。本文用三题逐层增加工具,最后把“枚举所有点”变成“找中点、减去两边”。
学习顺序是 DFS 子树区间 → 倍增祖先与 LCA → 路径距离 → 奇偶性与分量计数。三份代码独立完整,均使用迭代遍历,避免长链触发递归栈风险。
学习路线与题目边界
先读 树上 DFS 三题 和 子树区间,再进入本文;若目标是每个根的整体答案,可对照 换根 DP 三题。LCA 不是换根 DP 的替代品,它主要回答固定树上两点的关系。
以下题名、难度、原始标签与约束于 2026-09-30 从 Codeforces 官方题目页核对。难度是平台标记,不代表三题对所有人都严格递增。
| 官方题目 | 难度 | 官方标签 | 本文抓手 |
|---|---|---|---|
| 1328E Tree Queries | 1900 | dfs and similar, graphs, trees | 全部上移一层后是否共链 |
| 1304E 1-Trees and Queries | 2000 | data structures, dfs and similar, shortest paths, trees | 三种路线长度与奇偶性 |
| 519E A and B and Lecture Rooms | 2100 | binary search, data structures, dfs and similar, dp, trees | 中点两侧分量的补集 |
| 题目 | 核心规模 | 不能忽略的条件 |
|---|---|---|
| 1328E | 2 ≤ n ≤ 200000;1 ≤ m ≤ 200000;所有询问点数之和 ≤ 200000 | 根为 1;同一询问内顶点互异 |
| 1304E | 3 ≤ n ≤ 100000;1 ≤ q ≤ 100000;1 ≤ k ≤ 1000000000 | 新边连接不同且原本不相邻的点;可重复走边;询问独立 |
| 519E | 1 ≤ n ≤ 100000;1 ≤ m ≤ 100000 | 两个查询点允许相同;距离按边数计 |
flowchart TD A["固定树与大量查询"] --> B["DFS 区间判祖先"] B --> C["1328E 上移一层后共链"] A --> D["倍增祖先与 LCA"] D --> E["两点距离"] E --> F["1304E 比较长度和奇偶性"] E --> G["519E 找中点并减去分量"]
第一题 1328E 把靠近路径改写为祖先关系
为什么只看父节点就够了
问题要求存在一条从根出发的路径,使每个给定点到这条路径的距离不超过 1。直接枚举路径终点,再逐个算距离,最坏会重复处理整棵树。
令根的父节点仍为根,将每个查询点 v 换成 parent[v]。原条件等价于:这些父节点都在某条根路径上。
必要性:若 v 在合法路径上,它的父节点也在路径上;若 v 不在路径上但与路径相邻,邻接的只能是 v 的父节点。假如邻接的是 v 的孩子,根到孩子的路径本来就经过 v,与“不在路径上”矛盾。
充分性:若所有 parent[v] 都在路径上,每个原点 v 就在路径上或与路径相邻。上移不是要求真实移动顶点,而是把存在性条件换成更易检查的关系。
最深父节点决定唯一的候选链
在上移后的点中选深度最大的 w。它们共处一条根路径,当且仅当每个点都是 w 的祖先。相同深度的两个不同点不能互为祖先,因此选谁都不会掩盖冲突。
DFS 先序中,v 的子树恰好占据半开区间 [tin[v], tin[v]+size[v])。于是 v 是 w 的祖先,当且仅当 w 的进入编号落在该区间内。这里包含“自己是自己的祖先”。
flowchart TD
A["读取查询点"] --> B["各自替换为父节点"]
B --> C["选择最深的父节点 w"]
C --> D{"所有父节点都是 w 的祖先"}
D -->|是| Y["YES 原点距离根到 w 的路径至多一条边"]
D -->|否| N["NO 出现无法放进同一根路径的分支"]
手算一次分支冲突
自建树的边为 1—2、1—3、2—4、2—5、3—6,根为 1。
| 原查询点 | 上移后的点 | 最深点 | 结果 |
|---|---|---|---|
| 4、5 | 2、2 | 2 | YES,可取根到 2 的路径 |
| 4、6 | 2、3 | 2 或 3 | NO,两条分支无法共链 |
| 1、4、3 | 1、2、1 | 2 | YES,根留在根 |
| 6 | 3 | 3 | YES,单点总可满足 |
| 实现细节 | 不变量 |
|---|---|
| 栈模拟 DFS,弹出时记录 tin | 一棵子树被完整访问后才轮到兄弟子树 |
| 逆先序累计 size | 孩子的计数先加入父节点 |
| 根的 parent 设为 1 | 查询包含根时无需访问虚构的 0 号节点 |
| 比较区间右端使用严格小于 | 半开区间不包含下一个子树 |
完整 C++17 实现
1 |
|
预处理时间与空间均为 O(n),所有查询时间为 O(Σk)。这题无需为“可能用到”而建立 O(n log n) 的 LCA 表。
共用工具 倍增祖先和最近公共祖先
第二、三题需要快速计算任意两点距离。若每次沿父指针逐步向上,长链上一次查询就会花 O(n)。
定义 up[j][v] 为 v 的第 2^j 个祖先,深度 dep[1]=0。根以上仍停在根。转移是 up[j][v]=up[j−1][up[j−1][v]],即跳两段相同长度。
| 要做的操作 | 方法 | 单次复杂度 |
|---|---|---|
| 向上跳 s 条边 | 将 s 按二进制拆开 | O(log n) |
| 求 LCA(a,b) | 先抬平深度,再从大到小同时跳 | O(log n) |
| 求距离 | dep[a]+dep[b]−2dep[LCA(a,b)] | O(log n) |
| 求子树大小 | 按父先子后的遍历顺序逆序汇总 | 全树 O(n) |
LCA 同跳阶段只在两个 2^j 祖先不同时跳跃。此时双方仍严格位于最近公共祖先下方;大步都不能再跳后,两者的父节点就是 LCA。先处理抬平后两点重合的情况,否则祖先与后代的查询会出错。
例如上面的六点树:LCA(4,5)=2,距离为 2+2−2×1=2;LCA(4,6)=1,距离为 2+2−0=4。向上跳 13 层则可拆成 8+4+1,并不需要循环走 13 次。
代码中的共用 Tree 结构使用父先子后的迭代顺序构建深度和子树大小。这个顺序不用于 DFS 区间判定;第一题的 tin 必须来自真正的 DFS,不能用广度优先顺序冒充。
第二题 1304E 恰好走 k 步不等于最短路
每个询问临时增加无向边 x—y,问能否从 a 走到 b,恰好使用 k 条边。这里允许重复经过点和边,所以研究的是行走,不是简单路径。新增边在下一次询问中不保留。
三个长度不能只留下最小值
不必重新建图跑 BFS。只计算:
| 候选 | 长度 | 含义 |
|---|---|---|
| d0 | dist(a,b) | 不使用新边 |
| d1 | dist(a,x)+1+dist(y,b) | 经 x 到 y 使用新边 |
| d2 | dist(a,y)+1+dist(x,b) | 经 y 到 x 使用新边 |
这三种路线都是真实可走的,即使两段树路径有重叠也没有关系。对于某个长度 d,只要 d≤k 且 k−d 为偶数,就可以沿一条边来回走若干次补足长度。n≥3 保证不会遇到无边可绕的单节点树。
为什么这样也没有漏掉其他可能?加一条边后图中只有一个环。对任意合法行走,先去掉沿边往返的片段,每次删去 2 步;在唯一环上多绕两整圈也可删去,减少的长度仍为偶数。保留至多一次绕环后,新边至多使用一次,余下树上的绕行也都能按偶数步消去。对应的基础路线就是上述三种之一。因此至少存在一个候选,不长于原行走且同奇偶。
也可以从奇偶理解:偶数长度的环不能改变树路径的奇偶;奇数长度的环能够改变奇偶,但绕到环上同样需要成本,不能只看 k 大不大。
flowchart TD
A["临时增加 x 与 y 之间的一条边"] --> B["用原树 LCA 计算 d0 d1 d2"]
B --> C["逐个检查 d 不大于 k"]
C --> D["再检查 k 与 d 奇偶相同"]
D --> E{"至少一个候选满足"}
E -->|是| Y["YES 用两步往返补足"]
E -->|否| N["NO 长度或奇偶性不允许"]
一条短链说明为什么要检查三个候选
原树为 1—2—3—4—5,临时加入 1—3,查询 a=1、b=2。三个长度分别是 d0=1、d1=2、d2=4。
| k | 可用候选 | 答案 | 理由 |
|---|---|---|---|
| 1 | d0=1 | YES | 直接到达 |
| 2 | d1=2 | YES | 1→3→2 |
| 3 | d0=1 | YES | 比直接路线多两步 |
| 4 | d1=2 或 d2=4 | YES | 可补两步或直接选四步路线 |
若只取最短值 1,再检查奇偶,会错误拒绝 k=2。另一个对照:加入 1—4、查询 1 到 3,三个长度为 2、2、6;k=3 时全部不合格,虽然最短距离只有 2。
完整 C++17 实现
下面包含完整倍增结构,可独立提交,不依赖上一份代码。
1 |
|
预处理 O(n log n),每个询问只做常数次 LCA,总时间 O((n+q)log n),空间 O(n log n)。k 最大为 10^9,代码用 long long 保存候选与差值,避免后续扩展约束时混用整数类型。
第三题 519E 等距点是中点两侧之外的所有点
给定 a、b,要数出多少顶点 z 满足 dist(z,a)=dist(z,b)。逐点做两次距离查询仍要 O(n log n),无法承受十万次询问。
从路径投影证明中点条件
树上任意 z 通向 a、b 的路径,会在 a—b 路径上的某个点 p 分开。两个距离都包含 dist(z,p),相减后只剩 dist(p,a)−dist(p,b)。所以 z 是否等距,只取决于它接入 a—b 路径的位置是不是正中点。
先分三种情况:
| 情况 | 结论 | 原因 |
|---|---|---|
| a=b | n | 每个点到同一个端点的距离当然相同 |
| dist(a,b) 为奇数 | 0 | 中点落在边内,不是顶点 |
| 正的偶数距离 | 找到顶点中点 mid | 保留接入点为 mid 的所有分支 |
令距离 d=2h。从中点沿路径向 a、b 各走一步,会进入两个不同分量;这些点分别离一个端点更近,都应排除。其余点包括中点自身与旁支,正好等距。
flowchart TD
A["输入 a 和 b"] --> B{"a 与 b 相同"}
B -->|是| N["答案 n"]
B -->|否| C{"路径长度为奇数"}
C -->|是| Z["答案 0"]
C -->|否| D["定位路径中点"]
D --> E{"两端深度相同"}
E -->|是| F["总点数减去中点下方两棵子树"]
E -->|否| G["中点子树减去朝更深端点的子树"]
为什么有两条计数公式
若 dep[a]=dep[b],中点就是它们的 LCA。令 ca=jump(a,h−1)、cb=jump(b,h−1),它们是中点朝两个端点方向的孩子。答案为 n−size[ca]−size[cb]。中点上方的整块区域也等距,因此不能只在中点的子树中数。
若深度不同,交换后让 a 更深。中点必在从 a 向上走 h 步的位置,且位于 LCA 的严格下方。令 mid=jump(a,h)、child=jump(a,h−1)。朝 b 的分量就是 mid 子树之外的区域;朝 a 的分量是 child 子树。剩下 size[mid]−size[child]。
| 六点树上的查询 | 中点或距离 | 手算结果 |
|---|---|---|
| 4、5 | 中点 2,扣掉子树 4 和 5 | 6−1−1=4,点为 1、2、3、6 |
| 4、6 | 中点 1,扣掉子树 2 和 3 | 6−3−2=1,只剩 1 |
| 1、4 | 中点 2,深度不同 | size[2]−size[4]=3−1=2,点为 2、5 |
| 2、6 | 距离 3 | 0 |
| 3、3 | 相同点 | 6 |
完整 C++17 实现
1 |
|
预处理 O(n log n),每次查询 O(log n),空间 O(n log n)。根可以任取,因为无根树上的距离和等距关系不会因预处理根改变;两条公式只是采用根为 1 时的子树表达。
边界清单与独立验证
| 容易出错的地方 | 后果 | 检查方式 |
|---|---|---|
| 把 BFS 顺序当 DFS 先序 | 子树区间混进其他分支 | 让兄弟各自带孩子 |
| 1328E 检查原点必须共链 | 误拒绝合法的相邻旁支 | 查询兄弟叶子 |
| 1304E 只看最短候选 | 丢失另一个奇偶类 | 三角环上的二步查询 |
| 1304E 永久加入新边 | 下一询问不再是题目规定的图 | 同一原树切换不同新边 |
| 519E 漏掉相同端点 | h−1 变成负数 | 查询 a=a,包括 n=1 |
| 519E 相同深度只数中点子树 | 漏掉中点上方的等距点 | 查询同一深层父节点的孩子 |
| 用递归遍历十万级长链 | 可能栈溢出 | 极限长链运行 |
| 把边数与点数混用 | 中点偏移一格 | 距离 2 的最小非平凡查询 |
专项程序从本文抽取三段 C++17,使用 -O2 -Wall -Wextra -pedantic 编译。参考答案不使用文章的 LCA 或计数公式:
本次三份程序均无警告编译,通过 744 次执行和 581,896 个查询结果核验;其中 246 棵小树包含 1 至 5 个点的全部标号树及固定种子的随机树,另有最大规模链、星形树和最大步数测试。
| 题目 | 独立参考方法 |
|---|---|
| 1328E | 枚举每个路径终点,显式构造根路径,再检查每个查询点到路径的最小 BFS 距离 |
| 1304E | 临时建图,逐步扩展恰好第 t 步可到达的顶点集合 |
| 519E | 从每个点 BFS,逐个比较到两个端点的距离 |
| 极限规模 | 链与星形的闭式答案,以及最大 k 的奇偶边界 |
1 | node tests/verify-tree-queries-three-article.cjs |
从这三题带走的不是三条孤立公式:先确定“可替代的条件”,再选择祖先判断或 LCA;最后把树的唯一通路转成奇偶约束或分量计数。回到 算法练习路径,可以继续比较 并查集离线查询 与 线段树区间摘要,分清不同查询模型需要维护的信息。