高级堆与树状数组

共 18 题
📑 题目列表 18 题
#
★★★

1. 斐波那契堆的摊还分析中 extract-min 是 O(log n) 摊还、decrease-key 是 O(1) 摊还,势函数 Φ=t+2m 如何支撑这两个界?

说明斐波那契堆的摊还分析,解释为什么 extract-min 是 O(log n) 摊还、decrease-key 是 O(1) 摊还,并说明势函数 Φ=t+2m 如何支撑这两个界?

  • 斐波那契堆的树根链表与懒惰删除
  • 势函数 Φ=t+2m 的定义
  • extract-min 与 decrease-key 的摊还分析

斐波那契堆由若干最小堆序树组成,用势函数 Φ=t+2m 分析,其中 t 是根树数量,m 是标记节点数。extract-min 时需要取出最小根、把它的孩子加入根链表,然后合并相同度数的树(consolidation)。consolidation 使根数 t 从约 D 降到 O(log n),其中 D 为最大度数(由斐波那契数列导出 D=O(log n)),实际代价 O(t) + O(D),而势减小约 t,摊还后为 O(log n)。decrease-key 时若节点键值变小使父节点失去最小堆序,则把该节点从父节点剪断并加入根链表,进行级联剪切;若父节点已标记则递归剪切。每次 cut 只改变一个节点,实际代价 O(1),势增量为 O(1)(t 加 1,m 减 1),故摊还 O(1)。

势函数同时衡量根树数和标记数:extract-min 通过大量合并减小 t,用偿债来支付 O(log n) 的合并代价;decrease-key 通过级联剪切保持 t 与 m 的界,使每次操作摊还 O(1)。标记机制保证每个节点失去一个孩子后必须再失去一个孩子才会被剪断,从而维持 D=O(log n) 的度数上界。

#
★★★

2. 动态中位数的双堆法中左大根堆+右小根堆能在 O(log n) 插入、O(1) 取中位数,两堆大小如何平衡与调整?

说明动态中位数的双堆法,解释为什么用左大根堆加右小根堆能在 O(log n) 插入、O(1) 取中位数,以及两堆大小如何平衡与调整?

  • 大根堆(左半)与小根堆(右半)的划分
  • 插入时先平衡再入堆的调整
  • O(log n) 插入与 O(1) 取中位数

维护左大根堆存较小的一半、右小根堆存较大的一半,并保证两堆大小差不超过 1。插入新元素时,若它小于左堆顶则入左堆,否则入右堆;入堆后若某堆比另一堆多出 2 个以上,就把该堆顶移到另一堆,从而恢复平衡。因此两堆大小相等或差 1,中位数为:总数为偶数时取两堆顶平均值,为奇数时取元素较多那堆的堆顶。插入每次堆操作 O(log n),取中位数只需看堆顶 O(1)。

关键是把中位数压缩到两个堆顶:左堆顶是左半最大值,右堆顶是右半最小值。每次插入调整仅需一次堆顶迁移,保持 O(log n)。这是"流式数据求中位数"问题的标准解法。

class MedianFinder {
    PriorityQueue<Integer> lo = new PriorityQueue<>((a,b)->b-a); // 大根堆
    PriorityQueue<Integer> hi = new PriorityQueue<>();           // 小根堆
    public void addNum(int num) {
        lo.offer(num);
        hi.offer(lo.poll());
        if (lo.size() < hi.size()) lo.offer(hi.poll());
    }
    public double findMedian() {
        if (lo.size() > hi.size()) return lo.peek();
        return (lo.peek() + hi.peek()) / 2.0;
    }
}
#
★★★

3. 并查集的两种优化中为什么按秩合并保证树高 O(log n),路径压缩单独使用均摊 O(log n),两者结合得到反阿克曼 O(α(n))?

说明并查集的两种优化,解释为什么按秩合并保证树高 O(log n)、路径压缩单独使用均摊 O(log n),以及两者结合得到反阿克曼函数 O(α(n))?

  • 按秩合并(union by rank)与树高
  • 路径压缩单独使用的均摊复杂度
  • 两者结合的反阿克曼复杂度

按秩合并:union 时总是把秩较小的树根连到秩较大的根下,可证明任意节点到根的树高至多 O(log n)。路径压缩:find 时把路径上所有节点直接指向根,单独使用它的均摊复杂度为 O(log n)(具体分析每个节点最多被压缩的次数)。两者结合后,通过阿克曼函数的迭代层次分析,均摊复杂度为 O(α(n)),其中 α 是反阿克曼函数,对实际 n 几乎为常数(≤4)。证明用到"秩作为高度上界""压缩使节点上移"以及递归定义嵌套的势能层次。

按秩合并保证树高对数界,路径压缩把深层节点上提;两者结合极大压缩了有效高度,使复杂度降到 O(α(n))。这是均摊分析中经典的"秩+压缩"证明,实际常近似视为常数时间。

class DSU {
    int[] parent, rank;
    DSU(int n) { parent = new int[n]; rank = new int[n]; for (int i=0;i<n;i++) parent[i]=i; }
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    void union(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return;
        if (rank[a] < rank[b]) { int t=a; a=b; b=t; }
        parent[b] = a;
        if (rank[a] == rank[b]) rank[a]++;
    }
}
#
★★★

4. 主席树求静态区间第 k 小的过程中为什么查询要同时在 root[r] 与 root[l-1] 上做差分,每层 O(1) 比较、总复杂度 O(log n)?

说明用主席树(可持久化线段树)求静态区间第 k 小的过程,解释为什么查询要同时在 root[r] 与 root[l-1] 上做差分,以及每层 O(1) 比较、总复杂度 O(log n)?

  • 主席树的值域线段树版本结构
  • 区间 count 的差分求法 root[r]-root[l-1]
  • 每层 O(1) 判断、总 O(log n)

主席树按数组下标从左到右建立 n+1 棵值域线段树,root[i] 对应前缀 [1,i] 中各值出现的次数。查询区间 [l,r] 的第 k 小,需要在 root[r] 与 root[l-1] 上同时下降:对每个节点,若区间 [l,r] 中左子树的值个数 cnt = root[r].left - root[l-1].left 大于等于 k,则答案在左子树,两指针都向左;否则答案在右子树,k 减去 cnt,两指针都向右。因为 root[r] 与 root[l-1] 对应同一值域结构,相减即得任意区间 [l,r] 的 count。每层只做 O(1) 比较,共下降 O(log n) 层,总 O(log n)。

差分是核心:值域线段树中 root[i] 是 root[i-1] 插入 a[i] 后得到的版本,两版本节点结构一致,相减得到区间出现次数。建树时空复杂度 O(n log n),查询 O(log n)。这是静态区间第 k 小问题的经典解法。

#
★★★

5. 可合并堆(左偏堆/斜堆/配对堆)的应用场景中为什么 Dijkstra/Prim 工程上仍常用二叉堆而不用斐波那契堆?

说明可合并堆(左偏堆、斜堆、配对堆)的应用场景,解释为什么 Dijkstra/Prim 工程上仍常用二叉堆而不用斐波那契堆?

  • 可合并堆的 merge 操作
  • 斐波那契堆理论上的优势
  • 工程上选择二叉堆的实践原因

可合并堆支持 merge(合并两堆)操作:左偏堆以"右路径最短"为不变量,merge 时沿右路径递归,O(log n);斜堆是无条件的左偏堆,merge 均摊 O(log n);配对堆 merge O(1)、decrease-key 摊还 O(log n)。这些堆在需要频繁 merge 的场合(如外排序、离散事件模拟)更合适。但 Dijkstra/Prim 工程上仍用二叉堆,因为:斐波那契堆的理论优势(decrease-key O(1))需要大量 decrease-key 才体现,而用二叉堆实现简单、常数小、缓存友好;斐波那契堆实现复杂、常数大、实际内存占用高,O(log n) 的对数在节点数不大时未必比 O(1) 差。

理论复杂度与工程常数存在差距。斐波那契堆虽然理论最优,但实现复杂、常数大、实际性能常不如二叉堆;加上 O(log n) 与 O(1) 的差距在有限规模下不明显,因此工程实现(如 Java 的 PriorityQueue、C++ priority_queue)几乎都用二叉堆。

#
★★

6. 二项堆的结构与合并中二项树 B_k 的节点数恰为 2^k,合并两个二项堆如何用二进制加法类比做到 O(log n)?

说明二项堆的结构,解释为什么二项树 B_k 的节点数恰为 2^k,以及合并两个二项堆如何用二进制加法类比做到 O(log n)?

  • 二项树 B_k 的递归定义与节点数
  • 二项堆的二进制表示
  • 合并操作的二进制加法类比

二项树 B_k 定义为两棵 B_{k-1} 合并而成(一棵的根作为另一棵的根的子节点),因此 B_k 的节点数 = 2·B_{k-1} 的节点数 = 2^k。二项堆由若干不同阶数的二项树组成,每个阶数至多一棵,对应二进制表示:节点数 n 的二进制哪些位为 1,就存在哪些阶数的树。合并两个二项堆时,把相同阶数的树两两合并,如同二进制加法进位,合并后若某阶数出现两棵则合成高一阶,最多 O(log n) 次合并,故 merge 复杂度 O(log n)。

二项堆的二进制本质是其核心:n 的二进制位决定了堆的结构,合并就是"按位相加并进位",进位次数最多等于二进制位数 O(log n)。这使得 merge 高效且结构清晰。

#
★★

7. 树状数组与线段树的功能/复杂度对比中 BIT 代码短、常数小但仅支持前缀操作;线段树功能全但空间 4n、常数大

对比树状数组(BIT)与线段树的功能与复杂度,说明 BIT 代码短、常数小但仅支持前缀操作,线段树功能全但空间 4n、常数大?

  • BIT 的前缀操作与低常数
  • 线段树的区间功能与空间 4n
  • 两者的适用边界

树状数组(Fenwick Tree)用 lowbit 维护前缀和,支持单点更新与前缀查询,均为 O(log n),代码极短、常数小、空间 n。但它只能做可逆运算(如求和、异或)的前缀组合,区间更新需借助差分或多个 BIT,功能受限。线段树支持任意区间操作(区间加、区间最值、区间求和、懒标记),功能全面,但需要 4n 空间、常数更大、实现更复杂。选型原则:能用的场景优先 BIT,无法用 BIT 覆盖的区间操作用线段树。

BIT 的 lowbit 使单点更新只影响 O(log n) 个节点,前缀查询合并 O(log n) 个节点,常数小。线段树用 4n 空间保证任意区间能分解为 O(log n) 个节点(防止越界),功能更全但代价是空间与常数。

#
★★

8. 二叉堆的建堆为什么是 O(n),自底向上 sift-down 的求和分析,与逐个插入 O(n log n) 的差异?

说明二叉堆的建堆为什么是 O(n),用自底向上 sift-down 的求和分析解释,并与逐个插入的 O(n log n) 对比?

  • 自底向上 sift-down 建堆
  • 每层节点数与 sift 深度求和的摊还
  • 与逐个插入 O(n log n) 的差异

建堆从最后一个非叶子节点开始,自底向上对每个节点做 sift-down(下沉)。sift-down 的代价取决于节点距底部的距离:第 i 层(从叶子往上数)的节点下沉深度约为 i,而该层节点数约为 n/2^i。总代价 Σ (n/2^i)·i = O(n)(对 i 求和收敛)。若逐个插入则每个元素 sift-up 需要 O(log n),总 O(n log n)。因此批量建堆用自底向上下沉是 O(n),逐个插入是 O(n log n)。

关键在于大部分节点在堆的底层,下沉深度小;把节点数多但深度小的层与节点少但深度大的层配对,求和收敛为 O(n)。逐个插入则每个新元素都要从底部上浮到合适位置,最坏 O(log n)。

void buildHeap(int[] a) {
    int n = a.length;
    for (int i = n / 2 - 1; i >= 0; i--) siftDown(a, i, n);
}
#
★★

9. Sparse Table 的 O(1) 查询中为什么取 min/max 是幂等(idempotent)操作所以区间可以重叠,而区间和不能这样预处理?

说明 Sparse Table 的 O(1) 查询,解释为什么取 min/max 是幂等操作所以区间可以重叠预处理,而区间和不能这样预处理?

  • Sparse Table 的倍增预处理
  • 幂等操作(min/max)允许区间重叠
  • 区间和因非幂等不能重叠

Sparse Table 预计算每个位置出发长度为 2^k 的区间极值,查询 [l,r] 时取 k=floor(log2(r-l+1)),用两个预计算区间 [l, l+2^k-1] 与 [r-2^k+1, r] 的 min/max 合并得到答案,O(1)。这要求 min/max 是幂等操作:min(a) 与 min(a) 取两次仍为 a,重叠部分不影响结果。而区间和不是幂等,重叠部分会被重复计算,故不能用两个重叠区间求某区间和;区间和需用线段树或前缀和。Sparse Table 因此只能处理幂等(可重叠)的区间查询,不能用于区间和。

幂等性(idempotent)是允许区间重叠的关键:min/max/gcd 等满足 f(a,a)=a,重叠无害。这也是 Sparse Table 预处理 O(n log n)、查询 O(1) 但不可修改的原因;区间和需每个区间精确覆盖,只能靠线段树或前缀和。

#
★★

10. 二叉堆、二项堆、斐波那契堆的复杂度对比中哪些操作在哪种堆上更优,工程上为何多用二叉堆?

对比二叉堆、二项堆、斐波那契堆的复杂度,说明哪些操作在哪种堆上更优,并解释工程上为何多用二叉堆?

  • 三种堆的 insert/merge/extract-min/decrease-key 复杂度
  • 斐波那契堆的理论优势
  • 工程上选二叉堆的实践原因

二叉堆:insert 与 extract-min O(log n),merge O(n)(需合并数组),decrease-key O(log n)。二项堆:insert 均摊 O(1)、merge O(log n)、extract-min O(log n)、decrease-key O(log n)。斐波那契堆:insert 与 decrease-key 均摊 O(1)、merge O(1)、extract-min O(log n)。因此在需要大量 decrease-key 或 merge 的场景(如 Dijkstra、Prim),斐波那契堆理论最优。但工程上多用二叉堆,因为实现简单、常数小、缓存友好、内存占用低,且其 O(log n) 在有限规模下实际很快,斐波那契堆的实现复杂度和常数开销抵消了理论优势。

复杂度对比中斐波那契堆在 decrease-key 和 merge 上占优,但这些都是摊还界且常数大。工程场景(有限 n、常数敏感、要求确定性)下二叉堆更实用,这也是 Java/C++ 标准库选用二叉堆的原因。

#
★★

11. 树状数组如何实现区间加与区间求和,用两个 BIT(差分数组)推导查询公式的过程?

说明树状数组如何实现区间加与区间求和,推导用两个 BIT(差分数组)的组合及查询公式?

  • 差分数组与区间加
  • 两个 BIT 的组合
  • 区间求和的公式推导

区间加 [l,r] 加 x 等价于差分数组 d 上 d[l]+=x、d[r+1]-=x,单点 sum 即前缀和。但区间求和需要二次前缀和:位置 i 的值是 d[1..i] 的前缀和,区间 [1,n] 的和 = Σ_{i=1..n} Σ_{j=1..i} d[j] = Σ_{j=1..n} d[j]·(n-j+1) = (n+1)·Σd[j] - Σ j·d[j]。因此用两个 BIT 分别维护 d 和 j·d 的前缀和,即可在 O(log n) 内完成区间加与区间求和。区间 [l,r] 的和 = prefix(r) - prefix(l-1)。

关键是把"区间加 + 区间求和"转化为维护差分数组 d 及其带权前缀和 j·d。两个 BIT 分别维护 Σd[j] 与 Σ j·d[j],查询时按公式组合,实现 O(log n) 的区间加与区间求和。

// 两棵 BIT 维护 d 与 i*d
void rangeAdd(int l, int r, long x) { add(b1, l, x); add(b1, r+1, -x); add(b2, l, x*(l-1)); add(b2, r+1, -x*r); }
long sum(int x) { return sum(b1, x)*x - sum(b2, x); }
long rangeSum(int l, int r) { return sum(r) - sum(l-1); }
#
★★

12. 线段树与树状数组的互相转化中哪些区间操作(区间加、区间最值)只能用线段树?

说明线段树与树状数组的适用边界,指出哪些区间操作(如区间加、区间最值)只能用线段树实现?

  • BIT 只能处理可逆前缀操作的局限
  • 区间最值、区间加等需要线段树
  • 两者的转化边界

树状数组本质是"单点更新 + 前缀查询",可逆运算(求和、异或)可做;区间加与区间求和可用两个 BIT 实现;但区间最值(求任意区间最大值)不能用 BIT 直接实现,因为 BIT 的前缀节点结构无法覆盖"任意区间"的最值查询。区间加结合区间最值(如"区间加 x 后求区间最大值")也只能用线段树配合懒标记。因此,能覆盖完整区间操作(区间加、区间最值、区间求和、区间更新)的是线段树,BIT 是它的子集。

转化边界在于 BIT 的节点只表达以某点为后缀的区间,无法表达任意区间;线段树将任意区间分解为 O(log n) 个节点,功能最全。规则的"区间加+区间求和"可用 BIT 双维护,但"区间加+区间最值"就必须用线段树懒标记。

#

13. Treap 的期望深度中随机优先级使 Treap 等价于随机 BST,期望高度 O(log n) 如何由随机 BST 分析得到?

说明 Treap 的期望深度,解释为什么随机优先级使 Treap 等价于随机 BST,以及期望高度 O(log n) 如何由随机 BST 分析得到?

  • Treap 的 BST 键序 + 堆优先级结构
  • 随机优先级与随机 BST 的等价性
  • 随机 BST 期望深度 O(log n)

Treap 是一棵同时满足 BST 性质(按键)和堆性质(按优先级)的树,优先级随机分配。固定键的有序集后,随机优先级确定的树结构与随机 BST(把键随机插入)分布相同,因为随机优先级唯一决定了一棵 BST 结构,且各排列等概率。随机 BST 的期望深度为 O(log n):对任意键,它在树中的期望深度等于它前面/后面比它小的键中"是它祖先"的期望个数,该和为调和级数 O(log n)。因此 Treap 期望高度 O(log n),且无需显式平衡旋转,插入/删除通过旋转维护堆性质。

随机 BST 期望深度分析用"每个键 x 的深度等于以 x 为最小(或最大)节点的子序列个数"的计数,期望为 O(log n)。Treap 借助随机优先级自动获得这一性质,实现简单且期望平衡。

#

14. Link-Cut Tree 的 access 操作中虚实链切换需要 splay 与路径聚合,access 的摊还复杂度 O(log n) 如何由 splay 势能证明?

说明 Link-Cut Tree 的 access 操作,解释为什么虚实链切换需要 splay 与路径聚合,以及 access 的摊还复杂度 O(log n) 如何由 splay 势能证明?

  • 虚实链分解与 access 的路径聚合
  • splay 在 access 中的作用
  • 摊还复杂度 O(log n) 的势能证明

Link-Cut Tree 用虚实链分解维护动态树,每棵树被分解为若干条实链(preferred path),每条链用 splay 维护。access(u) 把从根到 u 的路径变成一条实链:从 u 开始,每次把当前节点 splay 到其实链根,然后把它与虚孩子的连接断开,连上上一步的节点,直到根。这样通过 splay 和虚实的切换,把根到 u 的路径聚合为一条实链。access 的摊还复杂度 O(log n) 由 splay 的势能分析证明:splay 单次均摊 O(log n),而 access 中多次 splay 的总摊还代价受势能函数控制,虚实切换的代价也能被势能吸收,故 access 摊还 O(log n)。

access 的核心是"把沿途节点按需 splay 并切换虚实",使路径聚合。splay 的势能(用子树大小对数)保证每次 splay 均摊 O(log n),access 通过有限次 splay 与路径切换,总摊还 O(log n)。这是 LCT 动态树操作的基础。

#

15. 线段树懒标记(Lazy Propagation)的原理中延迟下推区间更新标记,仅在需要访问子节点时才 push_down,保证 O(log n) 单次更新

说明线段树懒标记(Lazy Propagation)的原理,解释延迟下推区间更新标记、仅在需要访问子节点时才 push_down 如何保证 O(log n) 单次更新?

  • 区间更新与懒标记
  • 下推(push_down)的时机
  • O(log n) 单次更新的保证

线段树做区间更新时,若某节点完全覆盖在更新区间内,则只更新该节点并打上懒标记(记录待下推的更新量),不递归到子节点。只有当后续操作需要访问该节点的子节点(如查询或更新区间部分覆盖)时,才 push_down 把懒标记下推到子节点。由于每个区间的更新在 O(log n) 个节点上完全覆盖,而部分覆盖的节点向下递归,递归深度 O(log n),因此单次区间更新 O(log n)。懒标记避免了"每次区间更新都推到底层叶子"的 O(n) 开销。

懒标记的本质是"延迟 + 按需下推":完全覆盖的区间直接记录,需要时再传播。这样区间更新只需触碰 O(log n) 个节点,把暴力 O(n) 的更新降到 O(log n),是线段树强大的关键。

void update(int node, int l, int r, int ql, int qr, int val) {
    if (ql <= l && r <= qr) { tree[node] += val; lazy[node] += val; return; }
    pushDown(node, l, r);
    int mid = (l + r) / 2;
    if (ql <= mid) update(node*2, l, mid, ql, qr, val);
    if (qr > mid) update(node*2+1, mid+1, r, ql, qr, val);
    tree[node] = Math.max(tree[node*2], tree[node*2+1]);
}
#

16. 可持久化线段树的版本共享中为什么修改只新建 O(log n) 个节点、旧版本节点不被改动就能保证历史查询正确?

说明可持久化线段树的版本共享,解释为什么修改只新建 O(log n) 个节点、旧版本节点不被改动就能保证历史查询正确?

  • 可持久化与版本共享
  • 修改时只新建 O(log n) 个节点
  • 旧版本正确性的保证

可持久化线段树每次修改时,从根到叶子的路径上只有 O(log n) 个节点被更新,因此只新建这 O(log n) 个节点,其余节点(包括未修改的子树)直接共享引用。修改路径上的节点是新节点,其子节点要么指向新建的下一层节点,要么指向原版本的共享节点。由于旧版本的节点从不被改动(只读),旧版本根节点对应的整棵树仍然完整,历史查询从旧根出发走共享节点即可得到正确的历史数据。空间 O(n log n),时间 O(log n)。

版本共享的精髓是"写时复制(copy-on-write)":只复制被修改的路径,其余共享。因为旧节点不可变,所以多个版本可安全共享子树,历史查询永远正确。这也是主席树、可持久化数据结构的基础。

#

17. 树状数组(Fenwick Tree)的 lowbit 设计与前缀查询/单点更新,与线段树的适用边界?

说明树状数组的 lowbit 设计及其前缀查询、单点更新原理,并指出与线段树的适用边界?

  • lowbit 的定义与节点覆盖范围
  • 前缀查询与单点更新的路径
  • 与线段树的适用边界

树状数组用 lowbit(x) = x & (-x) 定义每个节点覆盖的区间:节点 i 覆盖 [i-lowbit(i)+1, i]。前缀查询把 i 累加并不断减去 lowbit(i),覆盖 O(log n) 个节点得到前缀和;单点更新把 i 累加并不断加上 lowbit(i),更新 O(log n) 个节点。两者都 O(log n)。适用边界:BIT 只支持可逆运算的前缀组合(单点更新 + 前缀查询;区间加 + 区间求和用两个 BIT),无法直接做区间最值、区间更新等;线段树功能更全但空间 4n、常数大。能用 BIT 的场景优先用 BIT,否则用线段树。

lowbit 使求和与更新都沿固定路径 O(log n)。BIT 的局限在于其节点结构只能表达"前缀 + 单点"的组合,无法表达任意区间最值或区间更新,这是它与线段树的分界。

int lowbit(int x) { return x & -x; }
void add(int i, int v) { for (; i <= n; i += lowbit(i)) tree[i] += v; }
int sum(int i) { int s = 0; for (; i > 0; i -= lowbit(i)) s += tree[i]; return s; }
#

18. Top Tree 与 LCT 的对比中竞赛中的动态树问题多用 LCT 或树链剖分,Top Tree 的 cluster 抽象何时才值得实现?

对比 Top Tree 与 Link-Cut Tree,解释为什么竞赛中的动态树问题多用 LCT 或树链剖分,以及 Top Tree 的 cluster 抽象何时才值得实现?

  • LCT 与树链剖分的适用性
  • Top Tree 的 cluster 抽象
  • Top Tree 的适用场景

Link-Cut Tree 用虚实链分解 + splay 支持动态树操作(link/cut/路径查询),O(log n) 均摊,功能直观、实现较成熟,是竞赛动态树问题的首选。树链剖分用静态的重轻链剖分 + 线段树支持树上的路径/子树查询,适合静态树。Top Tree 用 cluster(簇)抽象,把树分解为可合并的簇,支持更复杂的路径/子树聚合操作,但实现极其复杂、常数大,且大部分动态树问题 LCT 已能解决。因此只有当需要 LCT 难以表达的高级聚合(如动态最小生成树、某些路径合并)时才值得实现 Top Tree,多数竞赛题用 LCT 或树链剖分即可。

Top Tree 的 cluster 抽象提供更统一的"路径+子树"合并框架,但代价是复杂度和常数。LCT 和树链剖分在实现复杂度与功能上对绝大多数问题足够,故竞赛中更常用;Top Tree 是"需要时再上"的重型工具。