Skip to content

算法Algorithm

虚树构造与终点路径压缩

Virtual tree construction · Compressed terminal tree · 虚树

将查询终点按DFS序排序,补相邻LCA并以栈连接最近保留祖先,在至多2k−1点的压缩树上计算连通子树与两两距离总和。

原树有一百万个顶点,一次查询却只关心四个终点。把整棵树复制一份再删除无关叶子,虽然最后也能留下所需道路,却已经支付了全树遍历。虚树先找到不能省略的分叉点,再直接连接它们;一个查询要处理多少点,取决于终点数量,而非原树大小。

形式陈述 ​

压缩什么,保留什么 ​

输入是一棵固定根 r 的有限非空树 T,边长为非负整数,顶点编号 0,…,n−1。一次查询给出终点列表,去重后的集合为 S,大小为 k。重复出现同一编号不增加权重;若要按出现次数计对数,是另一个接口。

利用最近公共祖先,定义保留点集

U=S∪{LCA(x,y):x,y∈S}.

若 S 为空,输出空树;若只有一个终点,输出该点及零条边。否则,以所有终点的共同LCA为虚树根,每个其他保留点连接到它最近的真保留祖先。原树根不自动加入:它只有在属于 U 时才出现。

虚边 (p,v) 代表原树中从祖先 p 到后代 v 的整段路径。令 D(v) 为原根到 v 的加权距离,则边长为

ℓ(p,v)=D(v)−D(p).

边数深度仍用于祖先/LCA,不用加权距离代替深度排序。零边可能让祖先和后代有相同的 D,但祖先关系并未消失。

只补相邻终点的LCA ​

预处理DFS前序与子树区间,得到 tin,tout。祖先判定采用半开区间:p 是 v 的祖先当且仅当 tin(p)≤tin(v)<tout(p)。另用二进制倍增保存父亲跳表,以 O(log⁡(n+1)) 时间求LCA。本参考器选择这份易检验的静态接口,不借用别的常数查询实现的时间界。

text
ordered = 将去重终点按 tin 排序
U = ordered 中所有点
对 ordered 中每一对相邻点 (x,y): 将 LCA(x,y) 加入 U
nodes = 将 U 去重并按 tin 排序
stack = 空
依次处理 nodes 中的 v:
  当栈非空且栈顶不是 v 的祖先:弹栈
  若栈非空:输出 (栈顶,v,D(v)-D(栈顶))
  将 v 入栈

循环只对原来的相邻终点求LCA,不一边追加一边继续扩大终点循环。至多新增 k−1 个不同点,因此 k≥1 时 |U|≤2k−1。相邻LCA足够的证明在后文给出,不能只凭这个数量界断言没有漏分叉。[1]

直觉

DFS进入一个子树后,会先走完它再离开。如果两个终点属于某个分叉点的不同孩子子树,按DFS序排列的终点在跨过这条分界时,必有一对相邻终点把这个分叉点作为LCA。于是,无须枚举 k2 对终点,就能找齐全部必要的会合处。

保留这些会合处以后,其余内部点只是道路上的中途站,可以把一段连续道路折成一条带总长度的边。折叠不能忘记边长,也不能顺手丢掉某个本来就是终点的中途站。

压缩保留分叉和长度,按切开的终点对计费
例子与边界

四个终点压成七个点 ​

原树根为0,边及长度为

text
0—1(2)  0—2(3)  1—3(1)  1—4(4)  3—5(2)
3—6(1)  2—7(2)  7—8(3)  7—9(1)  9—10(2)

按这个邻接次序遍历,前序为 [0,1,3,5,6,4,2,7,8,9,10]。给定 S={4,5,8,10},排序后是 [5,4,8,10],不是编号顺序。相邻LCA依次为1、0、7;第二次排序得到

nodes=[0,1,5,4,7,8,10].

栈先压入0、1、5。处理4时,5不是4的祖先,弹出5后栈顶为1,连 1→4。处理7时,4和1都不再是祖先,弹到0,连 0→7。之后同样处理8、10。最终边为

(0,1,2), (1,5,3), (1,4,4), (0,7,5), (7,8,3), (7,10,3).

例如 1→5 代表 1→3→5,长度 1+2=3;0→7 代表 0→2→7,长度 3+2=5。终点之间的最小连通子树总长为 2+3+4+5+3+3=20。原树总长21,多出的边 3−6 不连接任何所需终点。

不枚举六条路径也能得到67 ​

对每个虚点 v,自底向上算其虚子树内的终点数 s(v)。一个点即便不是虚树叶子,只要属于 S,也应贡献1。对虚边 (p,v),删除它把 k 个终点分成 s(v) 与 k−s(v) 两侧,恰有 s(v)(k−s(v)) 个无序终点对的路径经过它。

虚边 长度 下侧终点数 s 跨边无序对数 对距离总和的贡献
0→1 2 2 4 8
1→5 3 1 3 9
1→4 4 1 3 12
0→7 5 2 4 20
7→8 3 1 3 9
7→10 3 1 3 9

所以总和为67。直接核验六个距离得到 7,14,14,13,13,6,和仍为67。每对路径上的每条边贡献一次,这就是公式

∑{x,y}⊆Sd(x,y)=∑(p,v)∈EUℓ(p,v)s(v)(k−s(v))

的双计数证明,不要求所有终点都在叶子上。

三种看似方便的错误 ​

只保留四个终点会漏掉1、0、7这三个会合处,无法把真实路径组织成正确的祖先树。按编号排序而非DFS序,虽偶尔碰巧补齐,也不满足“子树形成连续段”的证明前提。把所有虚边都当长1,则连通子树长度错成6,终点距离也随之改变。

编号排序确实会失败:另取根0、单位边 0−1,1−2,0−3,1−4,终点为2、3、4。编号相邻对的LCA都是0,漏掉1;正确的DFS终点顺序是2、4、3,能补出1、0。若把漏掉1的三个终点直接连到0,两条长2的虚边会重复覆盖原边 0−1,使子树长度从4错成5,成对距离和从8错成10。

只有 S={4} 时,正确输出是单点4、总长0、成对距离和0。强行加入原根0会多保留从0到4长6的道路,不再是本题的最小连通子树。若任务另要求连接指定基地,应把基地明确加入终点集合后重算。

本算法不能保留被压掉点上的任意附加费用。例如原路径 1→3→5 上的点3若另有需要计费的标签,单个边长3没有记录它。必须先定义足够的路径摘要,或者把该点也加入保留集。树外额外连边同样会破坏唯一路径与本算法的证明。

推论与应用

为什么相邻LCA已经覆盖所有分叉 ​

取任意两个终点 x,y,令 z=LCA(x,y)。若 z 本身是终点,已在 S 中。否则 x,y 位于 z 的不同孩子子树中。在包含终点的这些孩子子树里,取DFS次序相邻的两支;令 a 为前一支中最后一个终点,b 为后一支中第一个终点。

子树前序区间连续,两支之间没有其他含终点的孩子子树,z 自己又不在 S,所以 a,b 在全体终点排序中也相邻,并且 LCA(a,b)=z。每个可能的成对LCA因而都会被添加。反过来,所添点本来就是终点对的LCA,没有引入无依据的点。

这些保留点对LCA也封闭:两个保留点若互为祖先,LCA就是其中一个;否则可在它们各自下面选择一个原终点,两终点的LCA与两保留点的LCA相同,也已经加入。因此所有保留点有一个共同的最深祖先,它正是排序后的第一个点。

栈给出最近保留祖先 ​

按前序处理时,栈中的点始终是当前尚未结束的保留祖先链。遇见下一点 v,不再包含 v 的子树已经结束,弹出它们不会丢掉 v 的祖先;剩下的栈顶就是此前访问过的最深保留祖先。除第一个点外,栈不会弹空,因为共同根始终是祖先。

每点入栈、出栈各至多一次,边恰连到最近的保留祖先。若一条被压缩的路径内部还存在通向其他终点的真实分叉,这个分叉会是两终点的LCA,本应被保留,与其位于虚边内部矛盾。因此不同虚边在原树上的内部不重叠,它们展开后恰好是连接全部终点的最小连通子树。这里“最小”先指包含意义上的唯一最小子树;零长边允许多个同权扩展,但不改变这个子树本身。

压缩边保留原路径总长,所以所有终点对距离保持;展开虚树得到的子树总长也就是虚边长之和。两个统计量不依赖原根的选择,虽然保留点集和方向可以改变。把主例根改为7,保留点只需 [7,1,5,4,8,10],7→1 长7替代经过0的两条边;两项结果仍为20和67。

每次查询的真实费用 ​

本参考器预先用迭代DFS和倍增表,时间与空间为 O(nlog⁡(n+1))。若原始列表有 m 项、去重后有 k 项,哈希集合去重的期望工作为 O(m);两次排序为 O(klog⁡(k+1)),相邻的至多 k−1 次倍增LCA为 O(klog⁡(n+1))。栈连接和两项统计都是 O(k)。因此一次查询总期望时间为

O(m+klog⁡(k+1)+klog⁡(n+1)+1),

临时及输出空间为 O(k+1),另加调用者持有的输入列表。代码不为每次查询清空一个长 n 的数组,也不复制全树。若改用常数LCA查询,则可以省去查询界中那一项对数,但必须另付并验证那套预处理,不能直接写成本参考器已经达到 O(klog⁡k)。

上述界按一字可容纳距离、终点计数和总和计。任意精度整数的乘法与加法需另计位成本;打印全部原路径也需按展开边数付费,O(k) 大小的虚树输出不包含免费展开。

共同终点会把终点改成 {5,6,10},得到五点虚树、总长14与两两距离和28,并要求解释哪些分叉留下、哪些道路消失。完整参考器打印逐边终点数与贡献,可与直接枚举路径对照。

参考资料

[1] Simon Lindholm,KACTL: CompressTree.h,文件日期2016-01-14,2026-10-10核查:终点按DFS序排序、添加相邻LCA、至多 |S|−1 个新增点的可执行参考。其配套LCA接口使用RMQ;本页另写迭代倍增与祖先栈,并明确计入每次LCA的对数代价,扩展空集合、重复终点、带权边和两项统计。

关系图谱5 个相邻概念 · 2 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系