一棵树上陆续开放一些服务点。每开放一点,之后的查询都要回答:“离我最近的开放点有多远?”每次从查询位置重新遍历整棵树当然能算对,但树很大而查询很多时,这样做会反复走同样的边。重心分解给每个位置准备一条短清单:只检查这些分隔点,就足以找到一条真正达到最短距离的路线。
形式陈述
原树与分解树是两套父子关系
输入是有限非空无向树 T ,顶点编号为 0 , … , n − 1 ,每条边有非负整数长度。距离 d ( u , v ) 是原树唯一简单路径的长度和。零长边允许,不同顶点的距离可以为零;顶点身份仍然不同。树的拓扑和长度在整个查询阶段固定。
对含 m 个顶点的连通分量 C ,若删除顶点 c 后每个分量至多含 m / 2 个顶点,称 c 为 C 的重心。这里平衡的是顶点数 ,不是边长,也不是当前标记数。找到 c ,把它设为这一层的分解根,删除它,再对各剩余分量递归。得到的新树 T C 使用同一批顶点;各子分量的重心以 c 为父亲。[1]
有根树的父子与祖先关系 理路 有根树与祖先关系 Rooted tree · Ancestor relation in a rooted tree · Parent and depth in a tree 在树中选定根后,由唯一根路径定义父子、祖先、深度与子树。 分别适用于原树临时选定的根,以及这棵分解树。两者不能共用一个 parent 数组。分解边不必是原树的边,它也不代表长度为一的真实道路。
为每个顶点 v 保存标签表
是 在 中 的 祖 先 , 含 L ( v ) = { ( c , d ( v , c ) ) : c 是 v 在 T C 中的祖先,含 v } . 标签里的距离始终在原树 上计算。构建时,每选出一个重心 c ,就在当前分量中从 c 遍历一遍,给其中每个顶点追加一个标签,然后才把 c 排除出后续分量。无须对每对 ( v , c ) 再调用一次最短路或LCA查询。
如何找出一个重心
在当前分量任选临时根,计算每点的子树大小 s i z e ( v ) 。删除 v 后,向孩子一侧的分量大小分别为各个 s i z e ( c h i l d ) ,向父亲一侧为 m − s i z e ( v ) 。于是检查
是 的 孩 子 max ( { m − s i z e ( v ) } ∪ { s i z e ( x ) : x 是 v 的孩子 } ) ≤ m / 2. 至少有一个点通过检查。证明可以从任意点出发:若它不满足条件,就沿唯一一个超过 m / 2 的分量走到邻点。跨过这条边后,反方向的分量小于 m / 2 ,不会再走回来;一条有限树中不回头的行走必停止,停止点就是重心。这里“唯一”来自两个互不相交分量不可能都超过一半。
参考器检查全部候选,若有两个则取编号较小者,使输出可重复。这个平局规则只固定一种分解,不参与距离正确性。
只增加标记的完整接口
初始没有标记。对每个重心 c ,维护 B [ c ] :所有已经标记、且标签表含 c 的顶点中,到 c 的最小距离;没有这样的点时为 + ∞ 。因此 B [ c ] 只汇总这一层原分量里的标记点,不能把它解释成全树所有标记点的最小值。
text mark(v):
对 (c,delta) 属于 L(v): B[c] = min(B[c],delta)
nearest(u):
返回 min{delta+B[c] : (c,delta) 属于 L(u)}
1 2 3 4 5
重复标记不改变集合。参考器额外保存达到最小值的标记身份,距离并列时选编号较小者;空集合返回 None,不伪造某个有限的大数。本接口没有取消单个标记的操作。reset 可以清空全部标记及 B ,费用为 O ( n ) ,之后继续使用原标签。
直觉
删除重心会把道路网络分成几支。若查询点和某个服务点在不同支,它们的唯一路径必须经过这个重心;若还在同一支,就继续到下一层。最终总会遇到一个恰好位于两点真实路径上的共同分隔点。
其他祖先可能让路线绕远。例如从同一支的两个点先绕到顶层重心再回来,会重复走一段路。非负边长使这种绕路不会比真实路径更短,所以可以把所有候选取最小:绕远者不会造成低估,而其中至少一个候选不绕远。
图片加载失败 分解边不是原树道路,标签保存的是原树距离
例子与边界
十一个顶点,三次开放
固定以下原树,括号内为边长:
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)
1 2
删除0后两支各有5点,所以先选0。左支重心为3,删除后大小为2、1、1;右支重心为7,删除后大小为1、1、2。两点分量按编号平局选1或9。最终分解父亲为
text 0:无;1:3;2:7;3:0;4:1;5:3;6:3;7:0;8:7;9:7;10:9
1
第一次标记4。它的标签是 ( 0 , 6 ) , ( 3 , 5 ) , ( 1 , 4 ) , ( 4 , 0 ) 。查询6时,自己的标签为 ( 0 , 4 ) , ( 3 , 1 ) , ( 6 , 0 ) :
经哪个重心
查询点到重心
B [ c ]
候选距离
0
4
6
10
3
1
5
6
6
0
+ ∞
无候选
答案为6,真实路径是 6 → 3 → 1 → 4 ,长度 1 + 1 + 4 = 6 。顶层候选10多走了 1 → 0 → 1 ,不能只查顶层摘要。
接着标记8,查询10。经0的候选为 8 + 6 = 14 ,经7为 3 + 3 = 6 ,经9和10没有标记摘要。最近点为8,路径 10 → 9 → 7 → 8 长 2 + 1 + 3 = 6 。最后标记6,再查询5,经0的候选为 5 + 4 = 9 ,经3为 2 + 1 = 3 ;最近点变为6。三次答案依次为6、6、3,不是只对一份静态标记集合求值。
三个不能偷偷扩大的条件
负边会破坏下界。 在路径 0 − 1 − 2 上把两边长度设为 − 2 , 1 ,重心为1,只标记0并查询0。经1的候选是 − 2 + ( − 2 ) = − 4 ,却小于自身的真实简单路径距离0。因此本参考器在入口拒绝负边;“树上没有圈”不能挽救这次绕路比较。
取最小不能撤销。 若只标记4后又把4取消,旧 B 仍保存4提供的距离,查询会回答一个已经不存在的点。支持删除需要保存更多候选,例如具有删除能力的有序多重集合,并重新计费。不能把 marked[4] 改为假就声称实现了删除。
分解静态,标记动态。 改变边长会使距离标签失效;link/cut还会改变分量与分隔关系。本接口必须重建。与重链剖分 理路 重链剖分 heavy-light decomposition · HLD 按子树大小选择重儿子,把树路径拆成对数条连续链区间。 不同,这里没有把任意路径变成若干连续数组区间;与Link–Cut Tree 理路 Link–Cut Tree Link-cut tree 以 preferred-path 分解和辅助伸展树维护动态森林,在对数摊还时间内支持换根、连边、断边与路径聚合。 也不同,它不承诺维护动态森林。
推论与应用
为什么最小候选恰好是答案
设标记集合为 M ≠ ∅ ,真实答案 D = min v ∈ M d ( u , v ) 。先证明算法不会低估。一个有限的 B [ c ] 由某个真实标记点 v 达到,所以相应候选为 d ( u , c ) + d ( c , v ) 。原树上这段拼接行走比唯一路径可能多走往返边,但非负长度保证
d ( u , c ) + d ( c , v ) ≥ d ( u , v ) ≥ D . 再取真正最近的标记点 v ∗ 。让 c 是 u , v ∗ 在分解树 上的最近公共祖先。若 c 等于其中一点,它显然在原路径上;否则两点在删除 c 后进入不同分量,原路径也必须经过 c 。二者标签都含 c ,且 B [ c ] ≤ d ( c , v ∗ ) ,于是
d ( u , c ) + B [ c ] ≤ d ( u , c ) + d ( c , v ∗ ) = d ( u , v ∗ ) = D . 上下两向给出相等。这里的分解树LCA只用于证明,查询程序不必真的求它;逐一检查短标签已经覆盖这个见证。返回的标记身份也是真实最近点:若其经 c 的候选等于 D ,它的真实距离夹在 D 与该候选之间,只能同为 D 。
构建、操作与输出分别花多少钱
每层剩余分量至多减半,任一顶点最多出现在 1 + ⌊ log 2 n ⌋ 层。因此标签总数为 O ( n log ( n + 1 ) ) ,标记与查询都至多检查这一条对数长祖先链。固定字长距离、数组访问及整数运算下,完成标签后每次操作最坏 O ( log ( n + 1 ) ) ,B 和标记数组另占 O ( n ) 。
构建采用分治法 理路 分治法 Divide and conquer 把问题分成较小同类子问题,递归求解后合并结果的算法设计范式。 ,逐分量重新求子树大小并填写距离标签。每层各分量的顶点不相交,总遍历工作为 O ( n ) ,层数对数,故总时间和标签空间为 O ( n log ( n + 1 ) ) 。参考器保留原邻接表并跳过已删除点;即使小分量仍会检查通向旧重心的邻接项,同一层所有被访问顶点的原度数总和仍不超过 2 ( n − 1 ) ,因此这些检查也被上述总界覆盖。输入连通性、边数和权值检查为 O ( n ) 。
参考器用字典记录一个临时分量的父亲和大小,这部分采用期望常数散列表操作;构建的相应界为期望 O ( n log ( n + 1 ) ) 。标签建好后的 mark/nearest仅访问列表,仍有上述最坏界。若整数可任意长,应再乘实际加法与比较的位成本。输出全部标签本身已可能需要 Θ ( n log n ) 项;只构造分解树另有线性算法,[1]并不意味着本页带全距离标签的输出也能无条件线性完成。
程序用显式栈遍历原树,长链不会用掉同样深度的宿主递归栈。全点对距离、全部标记集合和故障输入检查是测试工具,不属于单次在线查询的费用。
共同终点 要求交出每层分量大小、三个查询的所有候选及真实路径。完整参考器 还用同一原树构造虚树 理路 虚树构造与终点路径压缩 Virtual tree construction · Compressed terminal tree · 虚树 将查询终点按DFS序排序,补相邻LCA并以栈连接最近保留祖先,在至多2k−1点的压缩树上计算连通子树与两两距离总和。 :后者按一次查询的少数终点压缩道路,本页则预先给每个顶点保存分隔点标签,两种缩小工作量的方法有不同接口。
参考资料
[1] Davide Della Giustina、Nicola Prezza、Rossano Venturini,A new Linear-time Algorithm for Centroid Decomposition ,SPIRE 2019,§1、PDF pp.1–2:重心定义、递归分解与传统 O ( n log n ) 构建;论文主体另给线性算法。本页实现传统构建及自含证明的只增标记接口,不声称实现该线性改进。