2026.11.5 闲话:浅谈 Top Tree 的应用

关于 AI 仅让蓝色大肥鱼找出了所有错别字,保证人类贡献远远大于 AI。

前言

下了很大决心,决定写一篇 Top Tree 详解。

我们将从基础理论入手,一步步从入门级 Top Tree 题,直到 SATT 等。

作为一篇讲解性文章,仅会讲解 Top Tree 的用途以及注意事项,关于复杂度等证明则不会提及,主要以理解结构为主。

如果想学好 Top Tree 建议自己画一画图,尤其是越往后的用途,毕竟这是一个拥有诸多性质的强大数据结构。

由于笔者的语文年级倒数,所以如果有哪里讲述不清楚的话欢迎提出。

基础 Top Cluster 分解

一切起源当然是从基础的理论开始。

首先我们来思考一件事情,我们能否将一棵树按照有序的方式缩成一条边呢?

这个是可以做到的,如果我们能做到这个,Top Tree 自然就浮出水面。

系统化描述一下:

现在有一棵树 \(G\)。

我们定义一个簇 \((x, y, E)\) 表示树 \(G\) 的一个连通子图,规定 \(x, y\) 为这个簇的界点,\(E\) 为 \(x\to y\) 路径上的边集,其中 \(x\) 称为上界点,\(y\) 称为下界点,\(E\) 称为簇路径。

可以发现,树上的一条边 \((x, y)\) 也是一个簇。

假设 \(a, b\) 为树 \(G\) 上的簇。

\[a = (a_x, a_y, a_E) \]

\[b = (b_x, b_y, b_E) \]

接下来定义两种簇的合并方式。

当 \(a_x = b_x\) 或 \(a_y = b_y\) 时定义:

\[\operatorname{Rake}(a, b)=a \]

当 \(a_y=b_x\) 时定义:

\[\operatorname{Compress}(a, b) = (a_x, b_y, a_E\cup b_E) \]

这太不像人话了,有什么更好的理解方式吗?

其实 \(\operatorname{Rake}\) 就是将一条边丢到另一条边上。

其实 \(\operatorname{Compress}\) 就是将两条边合并起来。

经过这两种合并我们可以将一棵树缩成一个簇。

由于我们特化到了一棵树,于是现在规定 \(\operatorname{Rake}\) 操作只能是上界点相等时才行,并规定一个簇的上界点为下界点的祖先。

注意,这两个操作的两个簇是有序的,不同的人可能会有不同的书写习惯。

那么在树上可以不断将叶子 \(\operatorname{Rake}\) 到另一条边,将二度点的父亲儿子 \(\operatorname{Compress}\) 起来。

A    A
|    |
B -> |
|    |
C    C
  A        A
 / \  ->  /
B   C    B

这样我们就可以将一棵树缩为一个簇了。

将每次操作的两个簇和合并后的簇连边,保持单个操作的顺序(区分左右儿子),会获得一棵完全二叉树,这棵树我们便称为 Top Tree。

由于我们记录的是边信息,所以一些维护点信息的情况我们会加入一个没有贡献的点作为根,让原树的根作为其儿子,这样根的贡献也能拍到边上。

为方便起见,下文仅讨论加入无贡献根的情况,假设加入的根为 \(0\) 号节点,树上节点的编号是 \(1\sim n\)。

我们来看看这棵二叉树都有哪些性质。

  1. 除叶子外,每个节点都有一个节点类型 C/R,表示这个节点是两个儿子 \(\operatorname{Compress}\) 而来还是 \(\operatorname{Rake}\) 而来。
  2. 如果将每个点拍到其到父亲的边,那么这些簇就是 Top Tree 上的叶子节点,恰有 \(n\) 个,于是整棵 Top Tree 恰有 \(2n-1\) 个节点。
  3. 一个簇的上界点是整个簇中深度最小的点,注意,下界点没有类似的性质,故如果处理的是点权信息,那么对于一个簇,我们不会计算上界点的贡献。
  4. 对于一个节点,其两个儿子簇内的节点的交有且仅有一个,如果是 \(\operatorname{Rake}\) 节点则为该节点的上界点/左儿子的上界点/右儿子的上界点,否则为左儿子的上界点/右儿子的下界点。
  5. 对于一个节点,其两个儿子中分别取一个点 \(x, y\),那么 \(x\to y\) 的路径必过这两个儿子的公共节点。
  6. 对于一个节点,其子树中的所有叶子节点构成的集合,便是这个簇所包含的所有边。

再来强调一遍顺序问题,以防后面产生不必要的误会。

\(\operatorname{Compress}(a, b)\) 表示 \(a\) 在上方,\(b\) 在下方,将两个簇合并,合并后 \(a\) 为左儿子,\(b\) 为右儿子。

\(\operatorname{Rake}(a, b)\) 表示将 \(b\) 拍到 \(a\) 上面,合并后 \(a\) 为左儿子,\(b\) 为右儿子。

静态 Top Tree 的构建

如果我们瞎建一棵 Top Tree,其树高是 \(O(n)\) 的,这太可恶了。

树上问题最常用算法之一的便是树链剖分,于是考虑使用树链剖分优化建树。

考虑递归问题,假设现在提取了一条重链,链顶为 \(x\),那么将这条重链上所有点相连的轻儿子全部缩成一条与父亲相连的边,再将这些边全部 \(\operatorname{Rake}\) 到重链上,最后将重链 \(\operatorname{Compress}\) 起来,这样可以方便的构建一棵 Top Tree。

如何将一堆簇 \(\operatorname{Rake}\) 或 \(\operatorname{Compress}\) 起来呢?

如果你学过全局平衡二叉树的话,大概就能想到使用重量平衡了吧?

我们维护每个簇的节点个数作为重量,在做一堆簇合并的时候,进行分治合并,假设现在分治到了 \([l, r]\),我们取一个 \(mid\) 使得左边的节点重量和与右边的节点重量和基本相等,分治处理 \([l, mid]\) 和 \([mid + 1, r]\) 即可。

这样建出来的 Top Tree 树高是 \(O(\log n)\) 的。

此处省略证明过程,感兴趣的读者可以自行证明或搜索。

听起来可能有点迷惑,但是放出代码来写几遍基本就不会再忘了。

code
// 类似线段树的那种 PushUp 吧,op 为 0 表示 Rake 节点,1 表示 Compress 节点。
void PushUp(int x)
{
    int _fa = tr[x].fa;
    if (tr[x].op) tr[x] = Compress(tr[x].ls, tr[x].rs);
    else tr[x] = Rake(tr[x].ls, tr[x].rs);
    tr[tr[x].ls].fa = x, tr[tr[x].rs].fa = x;
    tr[x].siz = tr[tr[x].ls].siz + tr[tr[x].rs].siz;
    tr[x].fa = _fa;
}

// 合并两个簇,这里复用了 PushUp 函数,写起来更方便(?)
int Merge(int x, int y, int op)
{
    ++tot;
    tr[tot].op = op;
    tr[tot].ls = x, tr[tot].rs = y;
    PushUp(tot);
    return tot;
}

// 重量平衡分治合并,这个可以写成二分形式减小常数。
int Solve(vector<int> &p, int l, int r, int op)
{
    if (l == r) return p[l];
    int mid = r - 1; //! 注意,这里的初始值是极其重要的,因为可能会出现左边极小的情况,此时该代码将可能不会给 mid 赋值。
    int s = 0, ns = 0;
    for (int i = l; i <= r; i++) s += tr[p[i]].siz;
    for (int i = l; i < r; i++)
    {
        ns += tr[p[i]].siz;
        if (ns >= (s >> 1)) {mid = i; break;}
    }
    return Merge(Solve(p, l, mid, op), Solve(p, mid + 1, r, op), op);
}

// 很优美的建树,ef[x] 表示 x 到 x 父亲的簇。
int Build(int x)
{
    vector<int> p; // 此处可以用类似启发式合并的 $O(n\log n)$ 数组减小常数。
    if (ef[x]) p.emp(ef[x]); // 这个是上面所说的最后会缩成一条到父亲的边。
    for (; son[x]; x = son[x])
    {
        vector<int> q;
        q.emp(ef[son[x]]); // 保证所有边会 Rake 到重链上。
        for (auto to : G[x])
        {
            if (to == f[x] || to == son[x]) continue;
            q.emp(Build(to));
        }
        p.emp(Solve(q, 0, q.size() - 1, 0)); // 将轻儿子 Rake 到重链上。
    }
    return Solve(p, 0, p.size() - 1, 1); // 将重链 Compress 起来。
}

这样我们成功建出了一棵单老哥树高 Top Tree,树高的常数大概是二倍。

Top Tree 的基本应用

在正式应用之前,我们先来看看 Top Tree 如何进行修改操作。

假设现在要修改一条边(点的话直接找拍到的那条边就行)。

首先找到这条边在 Top Tree 上的位置(一定是一个叶子)。

那么由上面的性质 6 可以得知,Top Tree 上包含这条边的节点其实只有这个叶子的所有父亲。

那太好了,只需要将叶子修改,然后一路 PushUp 上去就好了。

维护树的直径

恭喜,进入了第一个可以爽爽切题的部分咯。

Top Tree 的信息设计就和线段树极其相似,看一下 \(\operatorname{Rake}\) 和 \(\operatorname{Compress}\) 操作是否能转移就能设计出维护的信息。

现在手把手来设计树的直径如何维护。

首先对于一个簇,肯定会有一个值 \(s\) 表示这个簇内的直径,怎么转移呢?

对于 \(\operatorname{Compress}(a, b)\) 会出现两类贡献:\(a, b\) 内的贡献,\(a, b\) 之间的贡献。

\(a, b\) 之间的贡献怎么求,由之前的性质可得,\(a, b\) 的唯一交点是可以知道的,是 \(a\) 的下界点,也是 \(b\) 的上界点。

那么 \(a, b\) 之间的贡献可以与这个公共的交点联系起来,只需要维护出 \(as, bs\) 表示簇内点到上界点的最长距离和到下界点的最长距离即可。

这样 \(a, b\) 之间的贡献便是 a.bs + b.as,怎么样,是不是和线段树很像。

与线段树最不同的地方在于还需要处理 \(\operatorname{Rake}\) 操作,考虑这个怎么转移。

那么到上界点的最长距离只需要 max(a.bs, b.bs) 就行,到下界点的距离我们发现可以经过完整的 \(a\) 的簇路径后从 \(a\) 的上界点走到 \(b\) 中的某个点,于是我们还需要记录 \(len\) 表示簇路径长度,这个也转移一下就行。

具体的移在代码里了。
// f 表示到上界点的最大距离,g 表示到下界点的最大距离,h 表示簇内的直径,sum 表示簇路径长度。
// 不计算上界点到上下界点的最大距离以及直径长度。
Node Compress(int x, int y)
{
    Node tmp;
    tmp.sum = tr[x].sum + tr[y].sum;
    tmp.f = max(tr[x].f, tr[x].sum + tr[y].f);
    tmp.g = max(tr[y].g, tr[x].g + tr[y].sum);
    tmp.h = max(max(tr[x].h, tr[y].h), tr[x].g + tr[y].f);
    tmp.ls = x, tmp.rs = y, tmp.tag = 1;
    tmp.u = tr[x].u, tmp.d = tr[y].d;
    return tmp;
}

Node Rake(int x, int y)
{
    Node tmp;
    tmp.sum = tr[x].sum;
    tmp.f = max(tr[x].f, tr[y].f);
    tmp.g = max(tr[x].g, tr[x].sum + tr[y].f);
    tmp.h = max(max(tr[x].h, tr[y].h), tr[x].f + tr[y].f);
    tmp.ls = x, tmp.rs = y, tmp.tag = 0;
    tmp.u = tr[x].u, tmp.d = tr[x].d;
    return tmp;
}

不过大多数题目会在静态树上动态更改点集,只有某些点之间算直径,这其实也只需要更改一条边的信息,然后一路 PushUp 即可,因为我们没有计算上界点的贡献。

练习题:

P2056 [ZJOI2007] 捉迷藏。

P4115 Qtree4。

动态 DP

以最大独立集为例子。

由于我们的合并其实只和界点有关,于是状态只记录界点信息,并且上界点只负责记录信息而不产生贡献。

可能不好理解,做一下最大独立集大概就能理解了。

对于一个簇,设 \(f_{0/1,0/1}\) 表示上界点选不选,下界点选不选能产生的最大独立集大小。

在合并的时候,我们要保证两个簇的公共的界点状态一致,保证这个后直接合并就行,因为这两个簇的非界点节点之间不会互相影响。

这时不计算上界点贡献的道理就出现了,我们在 \(\operatorname{Compress}\) 的时候,右儿子的上界点没有贡献,但对应到左儿子的下界点便有了贡献,于是这个被忽略的贡献不重不漏的出现了。

给出【模板】动态 DP 的完整代码,有哪里不明白可以看代码再理解理解。

code
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 1e6 + 10;

#define emp emplace_back

const int inf = 0x3f3f3f3f;

int tot;
vector <int> G[N];
int f[N], son[N], siz[N], ef[N], v[N];

struct Node
{
    int ls, rs, fa, tag, siz, t, c;
    int g[2][2];
}tr[N << 1];

Node Compress(int x, int y)
{
    Node tmp;
    tmp.g[0][0] = max(tr[x].g[0][1] + tr[y].g[1][0], tr[x].g[0][0] + tr[y].g[0][0]);
    tmp.g[0][1] = max(tr[x].g[0][1] + tr[y].g[1][1], tr[x].g[0][0] + tr[y].g[0][1]);
    tmp.g[1][1] = max(tr[x].g[1][1] + tr[y].g[1][1], tr[x].g[1][0] + tr[y].g[0][1]);
    tmp.g[1][0] = max(tr[x].g[1][1] + tr[y].g[1][0], tr[x].g[1][0] + tr[y].g[0][0]);
    tmp.ls = x, tmp.rs = y, tmp.tag = 1;
    tmp.t = tr[x].t, tmp.c = tr[y].c;
    return tmp;
}

Node Rake(int x, int y)
{
    Node tmp;
    tmp.g[0][0] = max(tr[x].g[0][0] + tr[y].g[0][0], tr[x].g[0][0] + tr[y].g[0][1]);
    tmp.g[0][1] = max(tr[x].g[0][1] + tr[y].g[0][0], tr[x].g[0][1] + tr[y].g[0][1]);
    tmp.g[1][1] = max(tr[x].g[1][1] + tr[y].g[1][0], tr[x].g[1][1] + tr[y].g[1][1]);
    tmp.g[1][0] = max(tr[x].g[1][0] + tr[y].g[1][0], tr[x].g[1][0] + tr[y].g[1][1]);
    tmp.ls = x, tmp.rs = y, tmp.tag = 0;
    tmp.t = tr[x].t, tmp.c = tr[x].c;
    return tmp;
}

void PushUp(int x)
{
    int _fa = tr[x].fa;
    if (tr[x].tag) tr[x] = Compress(tr[x].ls, tr[x].rs);
    else tr[x] = Rake(tr[x].ls, tr[x].rs);
    tr[tr[x].ls].fa = x, tr[tr[x].rs].fa = x;
    tr[x].siz = tr[tr[x].ls].siz + tr[tr[x].rs].siz;
    tr[x].fa = _fa;
}

int Merge(int x, int y, int tag)
{
    ++tot;
    tr[tot].tag = tag;
    tr[tot].ls = x, tr[tot].rs = y;
    PushUp(tot);
    return tot;
}

Node _New(int x, int y)
{
    Node tmp;
    tmp.g[0][0] = 0, tmp.g[0][1] = v[y];
    tmp.g[1][0] = 0; // 此处便是上界点选了但是不纳入贡献中。
    tmp.g[1][1] = -inf;
    tmp.ls = tmp.rs = tmp.fa = 0;
    tmp.tag = -1;
    tmp.t = x, tmp.c = y;
    return tmp;
}

int New(int x, int y)
{
    ++tot;
    tr[tot] = _New(x, y);
    return tot;
}

int Solve(vector<int> &p, int l, int r, int tag)
{
    if (l == r) return p[l];
    int mid = (l + r) >> 1;
    int s = 0, ns = 0;
    for (int i = l; i <= r; i++) s += tr[p[i]].siz;
    for (int i = l; i < r; i++)
    {
        ns += tr[p[i]].siz;
        if (ns >= (s >> 1)) {mid = i; break;}
    }
    return Merge(Solve(p, l, mid, tag), Solve(p, mid + 1, r, tag), tag);
}

int Build(int x)
{
    vector <int> p;
    if (ef[x]) p.emp(ef[x]);
    for (; son[x]; x = son[x])
    {
        vector <int> q;
        q.emp(ef[son[x]]);
        for (auto to : G[x])
        {
            if (to == f[x] || to == son[x]) continue;
            q.emp(Build(to));
        }
        p.emp(Solve(q, 0, q.size() - 1, 0));
    }
    return Solve(p, 0, p.size() - 1, 1);
}

void dfs(int x, int fa)
{
    f[x] = fa;
    if (x) siz[x] = 1;
    for (auto to : G[x])
    {
        if (to == fa) continue;
        dfs(to, x);
        ef[to] = New(x, to);
        if (x) siz[x] += siz[to];
        if (siz[to] > siz[son[x]]) son[x] = to;
    }
}

void Update(int x)
{
    int _fa = tr[ef[x]].fa;
    tr[ef[x]] = _New(f[x], x);
    tr[ef[x]].fa = _fa;
    x = ef[x];
    while (tr[x].fa)
    {
        x = tr[x].fa;
        PushUp(x);
    }
}

void add(int x, int y) {G[x].emp(y);}

signed main()
{
    // freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);

    int n, m; cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i];
    for (int i = 1, x, y; i < n; i++) cin >> x >> y, add(x, y), add(y, x);
    add(0, 1);
    dfs(0, 0);
    int rt = Build(0);
    for (int i = 1; i <= m; i++)
    {
        int x, y; cin >> x >> y;
        v[x] = y;
        Update(x);
        cout << max(max(tr[rt].g[0][0], tr[rt].g[0][1]), max(tr[rt].g[1][0], tr[rt].g[1][1])) << '\n';
    }

    return 0;
}

例题:

P4719 【模板】动态 DP。

P4751 【模板】动态 DP(加强版)。

AT_abc351_g Hash on Tree。

P4426 [HNOI/AHOI2018] 毒瘤。

P3781 [SDOI2017] 切树游戏。

还有若干道板子不放到里面了,通过这些题你就能感受到 Top Tree 的通用性了。

Top Tree 的进阶应用

接下来的部分就比较偏门了,建议对 Top Tree 结构有过深入了解后再来学习。

特殊结构查询

子树查询

想要做这个,我们就需要更进一步的剖析 Top Tree 的结构了。

首先我们会发现,在 Top Tree 上有一段连通的节点,其均为 \(\operatorname{Rake}\) 节点或均为 \(\operatorname{Compress}\) 节点,我们称这样的结构叫做 \(\operatorname{Rake}\) 树或 \(\operatorname{Compress}\) 树。

由我们重量平衡建树法容易发现,对于重链 \(\operatorname{Compress}\) 的过程会产生一个 \(\operatorname{Compress}\) 树,而这棵 \(\operatorname{Compress}\) 树下挂上了众多的 \(\operatorname{Rake}\) 树,而 \(\operatorname{Rake}\) 树又挂了许多的 \(\operatorname{Compress}\) 树。

这是一个交替的结构,并且每个极大的这样的结构均有其独特的含义。

怎么理解这种独特含义就需要根据需求灵活变换了。

现在我们要做子树查询,那么找到 \(x\) 所在的重链,这条重链一定会延伸到叶子,我们只需要提取出 \(x\) 到重链底以及所有相连的轻边就可以获得这棵子树。

注意到一件事情,\(\operatorname{Compress}\) 树其实完全可以理解为一棵广义线段树,这样只需要做“线段树”后缀查询就可以提取出 \(x\) 到重链底的所有边,然后底下挂了一堆 \(\operatorname{Rake}\) 树恰恰就是所有轻边,这样就做完了。

当然也可以可持久化,也可以写成从根向下递归的形式而不是预处理 \(x\) 所在的 \(\operatorname{Compress}\) 树。

放一下例题的代码,没看懂的可以看看代码。

code
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

using ll = unsigned long long;
using pii = pair <int, int>;

#define int ll
const int N = 2e5 + 200, mod = 998244353;

#define emp emplace_back
#define fi first
#define se second

struct Node
{
    int u, d, l, r, f, g, h;
    int ls, rs, fa, siz, tag;
}tr[N << 1];

int tot, siz[N], son[N], f[N], n, dfn[N], ef[N], timer, dep[N], w[N], out[N], Top[N];
int DEP, p[18][N], q[18][N];
pii e[N], _e[N];
vector <int> G[N];

inline Node Compress(int x, int y)
{
    Node tmp;
    tmp.ls = x, tmp.rs = y, tmp.tag = 1;
    tmp.u = tr[x].u, tmp.d = tr[y].d, tmp.l = tr[x].l, tmp.r = tr[x].r;
    // if (!(tr[x].l <= tr[y].l && tr[y].r <= tr[x].r)) cerr << "JIA!";
    tmp.f = min(tr[x].f + tr[y].f, tr[x].g);
    tmp.f = min(tmp.f, tr[x].h + tr[y].f);
    tmp.g = min(tr[x].g, tr[x].h + tr[y].g);
    tmp.h = tr[x].h + tr[y].h;
    return tmp;
}

inline Node Rake(int x, int y)
{
    Node tmp;
    tmp.ls = x, tmp.rs = y, tmp.tag = 0;
    tmp.u = tr[x].u, tmp.d = tr[x].d, tmp.l = min(tr[x].l, tr[y].l), tmp.r = max(tr[x].r, tr[y].r);
    tmp.f = tr[x].f + tr[y].f;
    tmp.g = tr[x].g + tr[y].f;
    tmp.h = tr[x].h + tr[y].f;
    return tmp;
}

inline Node New(int u, int d)
{
    Node tmp;
    tmp.ls = tmp.rs = tmp.fa = tmp.tag = 0, tmp.f = tmp.g = w[d], tmp.h = 0;
    tmp.siz = 1, tmp.u = u, tmp.d = d, tmp.l = dfn[d], tmp.r = out[d];
    return tmp;
}

int _New(int u, int d)
{
    return tr[++tot] = New(u, d), tot;
}

void PushUp(int x)
{
    int _fa = tr[x].fa;
    if (tr[x].tag == 1) tr[x] = Compress(tr[x].ls, tr[x].rs);
    else tr[x] = Rake(tr[x].ls, tr[x].rs);
    tr[x].fa = _fa, tr[x].siz = tr[tr[x].ls].siz + tr[tr[x].rs].siz;
    tr[tr[x].ls].fa = x, tr[tr[x].rs].fa = x;
}

inline int Merge(int x, int y, int tag)
{
    ++tot;
    tr[tot].ls = x, tr[tot].rs = y;
    tr[tot].tag = tag;
    PushUp(tot);
    return tot;
}

void dfs(int x, int fa)
{
    if (x) siz[x] = 1, f[x] = fa;
    for (auto to : G[x])
    {
        if (to == fa) continue;
        dfs(to, x);
        if (x) siz[x] += siz[to];
        if (siz[to] > siz[son[x]]) son[x] = to;
    }
}

void dfs2(int x)
{
    dep[x] = dep[f[x]] + 1, dfn[x] = out[x] = ++timer;
    if (son[x]) dfs2(son[x]), out[x] = out[son[x]];
    for (auto to : G[x])
    {
        if (to == son[x] || to == f[x]) continue;
        dfs2(to);
        out[x] = out[to];
    }
}

int sum[N];
int _Solve(int *p, int l, int r, int tag)
{
    if (l == r) return p[l];
    int s = 0;
    s = (sum[r] - sum[l - 1]) / 2 + sum[l - 1];
    int mid = lower_bound(sum + l, sum + r + 1, s) - sum;
    if (mid >= r) mid = r - 1;
    return Merge(_Solve(p, l, mid, tag), _Solve(p, mid + 1, r, tag), tag);
}

int Solve(int *p, int l, int r, int tag)
{
    for (int i = l; i <= r; i++)
        sum[i] = sum[i - 1] + tr[p[i]].siz;
    return _Solve(p, l, r, tag);
}

int Build(int x)
{
    ++DEP;
    int P = 0, Q = 0, *pp = p[DEP], *qq = q[DEP];
    if (ef[x]) qq[++Q] = ef[x];
    int _x = x;
    for (; son[x]; x = son[x])
    {
        P = 0;
        pp[++P] = ef[son[x]];
        for (int to : G[x])
        {
            if (to == f[x] || to == son[x]) continue;
            pp[++P] = Build(to);
        }
        qq[++Q] = Solve(pp, 1, P, 0);
    }
    --DEP;
    x = _x;
    Top[_x] = Solve(qq, 1, Q, 1);
    do {Top[x] = Top[_x], x = son[x];}while(x);
    return Top[_x];
}

inline Node Compress(Node x, Node y)
{
    Node tmp;
    tmp.f = min(x.f + y.f, x.g);
    tmp.f = min(tmp.f, x.h + y.f);
    tmp.g = min(x.g, x.h + y.g);
    tmp.h = x.h + y.h;
    return tmp;
}

inline Node Rake(Node x, Node y)
{
    Node tmp;
    tmp.f = x.f + y.f;
    tmp.g = x.g + y.f;
    tmp.h = x.h + y.f;
    return tmp;
}

inline Node Merge(Node x, Node y, bool tag)
{
    if (tag) return Compress(x, y);
    else return Rake(x, y);
}

void Update(int x, int y)
{
    w[x] += y;
    int _fa = tr[ef[x]].fa;
    tr[ef[x]] = New(f[x], x);
    tr[ef[x]].fa = _fa;
    x = ef[x];
    while (tr[x].fa)
    {
        x = tr[x].fa;
        PushUp(x);
    }
}

Node Query(int x, int k)
{
    if (tr[x].u == k) return tr[x];
    if (dep[tr[tr[x].rs].u] > dep[k]) return Compress(Query(tr[x].ls, k), tr[tr[x].rs]);
    else return Query(tr[x].rs, k);
}

signed main()
{
    // freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> w[i];
    for (int i = 1, x, y; i < n; i++) cin >> x >> y, G[x].emp(y), G[y].emp(x);
    G[0].emp(1), dfs(0, 0), dfs2(0);
    for (int i = 1; i <= n; i++) ef[i] = _New(f[i], i);
    int rt = Build(0);
    int q; cin >> q;
    for (int i = 1; i <= q; i++)
    {
        char ch; cin >> ch;
        if (ch == 'Q')
        {
            int x; cin >> x;
            if (!son[x]) cout << tr[ef[x]].f << '\n';
            else cout << Compress(tr[ef[x]], Query(Top[x], x)).f << '\n';
        }
        else
        {
            int x, y; cin >> x >> y;
            Update(x, y);
        }
    }

    return 0;
}

例题:

P6021 洪水。

链查询

假设查询链 \(x, y\),那么可以找到这两个点在 Top Tree 上的 LCA,那么根据之前提及的性质,可以得到 \(x\to y\) 必定经过两簇公共点,记为 \(u\),那么只需要处理 \(x\to u\) 和 \(y\to u\) 即可,跳到儿子保证 \(u\) 是当前簇的界点,这样好做。

现在问题就是如何处理 \(x\to u\),其中 \(u\) 为界点的信息了,这个可以递归处理。

分情况讨论一下:

  1. 当前节点是 \(\operatorname{Rake}\) 节点,那么如果 \(u\) 是下界点则分为一部分是两个都是界点的直接返回当前的簇路径信息,另一部分找到有 \(x\) 的儿子递归下去。
  2. 当前节点是 \(\operatorname{Compress}\) 节点,那么就像线段树查询一样写就行。

容易发现复杂度是 \(O(\log n)\) 的。

需要注意的是,如果现在有修改操作,那么对于 \(\operatorname{Rake}\) 树中的标记,只需要传递给左儿子,不能传给右儿子。

当然可以可持久化,也可以写的更麻烦一些,直接处理 \(x\to y\),其中 \(y\) 为 \(x\) 祖先的情况,这样可以直接从根修改查询。

例题:

P7671 [GDOI2016] 疯狂动物城。

维护距离相关

这个需要涉及更深刻的 Top Tree 结构,不过不会太难。

最经典的题目当然就是 P8498 [NOI2022] 树上邻域数点。

就以这个为例,看看如何维护这类距离相关问题。

考虑一个类似分治的东西,假设现在询问一个点 \(x\) 的 \(d\) 邻域,那么在 Top Tree 上找到包含这个点的簇,满足其是最大的直径 \(\leq d\) 的簇,可以发现一件事情,那就是在该簇之外仅需处理出该簇的父亲直径大小的邻域,因为其父亲的直径已经 \(> d\) 了,这样竟然就是 \(O(n\log n)\) 的了。

那么设 \(g_{i, 0/1, k}\) 表示簇之外到达上/下界点的 \(k\) 邻域。

转移发现需要关注簇内的信息。

观察一下性质,对于一个 \(\operatorname{Compress}\) 节点而言,其下界点之下一定没有连子树,而非上/下界点所连的轻儿子一定已经包含了整个子树,对于 \(\operatorname{Rake}\) 节点而言,非簇路径之外也都是完整的了,这个可以通过建树的代码轻易得到,那么其实一个簇所包含的邻域信息已经很强了,不包含的信息仅有界点附近的不在簇内的信息。

具体的,设 \(f_{i, 0/1, k}\) 表示簇内到达上/下界点的 \(k\) 邻域,这个的转移是简单的,和写求直径差不多,可以在建树的时候顺便求出来。

那 \(g_{i, 0/1, k}\) 如何求出?

先看一下对于 \(\operatorname{Compress}\) 节点,其儿子的某一个 \(g_{i, 0/1, k}\) 可以直接继承父亲的状态,\(\operatorname{Rake}\) 节点则需要从上到下去添加簇内贡献,于是考虑从上到下转移。

对于 \(\operatorname{Compress}\) 节点儿子的另一个状态,假设是左儿子的上界点信息,有刚才观察的性质,由于已经开始进行 \(\operatorname{Compress}\) 了,于是右儿子上界点的信息也是完整的,右儿子簇内没包含的信息只有其右儿子下界点的一些邻域,所以需要依赖右儿子的 \(f_{rs, 0, k}\) 和父亲的 \(g_{fa, 1, k}\),右儿子同理。

对于 \(\operatorname{Rake}\) 节点而言,对于下界点,有之前观察的性质,右儿子下界点底下不会出现其他点了,这个可以直接跳过,而左儿子的下界点则直接继承父亲就好。

对于上界点来说就有点恶心了,首先我们发现一个儿子失去了另一个儿子的簇内信息,所以需要将另一个儿子的 \(f_{ls/rs, 0, k}\) 并到 \(g_{rs/ls, 0, k}\) 上去,但发现对于右儿子来说,左儿子再往下其实还可以继续扩展出来,于是我们还需要 \(g_{ls, 1, k}\),调整一下顺序就行。

查询的话就是我们找到的那个簇并上上界点簇外的邻域和下界点簇外的邻域,要做到 \(1\) 次合并的话就把这个簇预处理时并进某一个 \(g_i\) 里就行。

复杂度是 \(O(n\log n)\) 的,查询是 \(O(1)\) 的。

实现起来有点恶心,很混乱的代码。

例题:

P8498 [NOI2022] 树上邻域数点。

P8532 [Ynoi2003] 雾雨魔理沙(其实是同一个题)。

Top Tree 分治

可以用于写点分树。

以点分树模板题为例。

首先我们来思考一件事情,假设现在查询点 \(x\),那么找到其在 Top Tree 上的位置,然后会发现,枚举其他的点,则这些点和 \(x\) 在 Top Tree 上有唯一的一个 LCA,根据之前的性质,也就是会有一个界点在路径上,那如果我们预处理出来到达界点的信息后就可以查询了。

由于重量平衡,Top Tree 的子树大小和是 \(O(n\log n)\) 的,直接维护一个节点的所有叶子到这个节点的界点的距离,用一个树状数组即可。

查询的时候,跳父亲,找非 \(x\) 方向的那个儿子,去查询到达该节点一定距离的信息即可。

给个模板题的代码,不懂的可以看代码。

code
#include <iostream>
#include <vector>

using namespace std;

const int N = 1e5 + 10;

#define emp emplace_back

using pii = pair<int, int>;

struct ArrayTree
{
    int siz, *c;

    ArrayTree(int _siz)
    {
        siz = _siz;
        c = new int[siz + 3];
        for (int i = 0; i <= siz; i++) c[i] = 0;
    }
    
    void Update(int pos, int v)
    {
        ++pos;
        for (int i = pos; i <= siz; i += i & -i) c[i] += v;
    }

    int Query(int pos)
    {
        ++pos;
        if (pos < 0) return 0;
        if (pos > siz) pos = siz;
        int ans = 0;
        for (int i = pos; i; i ^= i & -i) ans += c[i];
        return ans;
    }
};

int tot, f[N], son[N], siz[N], dep[N], top[N], v[N], ef[N];
vector <int> G[N];
struct Node
{
    int ls, rs, fa, tag, siz, u, d, m;
    int *sons;
    ArrayTree *vs;
}tr[N << 1];

Node Rake(int x, int y)
{
    Node tmp;
    tmp.ls = x, tmp.rs = y;
    tmp.u = tr[x].u, tmp.d = tr[x].d, tmp.m = tr[x].u;
    tmp.tag = 0;
    return tmp;
}

Node Compress(int x, int y)
{
    Node tmp;
    tmp.ls = x, tmp.rs = y;
    tmp.u = tr[x].u, tmp.d = tr[y].d, tmp.m = tr[x].d;
    tmp.tag = 1;
    return tmp;
}

void PushUp(int x)
{
    int _fa = tr[x].fa;
    if (tr[x].tag == 0) tr[x] = Rake(tr[x].ls, tr[x].rs);
    else tr[x] = Compress(tr[x].ls, tr[x].rs);
    tr[tr[x].ls].fa = x, tr[tr[x].rs].fa = x;
    tr[x].siz = tr[tr[x].ls].siz + tr[tr[x].rs].siz;
    tr[x].fa = _fa;
}

int Merge(int x, int y, int tag)
{
    tr[++tot].ls = x, tr[tot].rs = y, tr[tot].tag = tag;
    PushUp(tot);
    return tot;
}

int Solve(vector<int> &p, int l, int r, int tag)
{
    if (l == r) return p[l];
    int mid = r - 1, s = 0, ns = 0;
    for (int i = l; i <= r; i++) s += tr[p[i]].siz;
    for (int i = l; i < r; i++)
    {
        ns += tr[p[i]].siz;
        if ((ns << 1) >= s) {mid = i; break;}
    }
    return Merge(Solve(p, l, mid, tag), Solve(p, mid + 1, r, tag), tag);
}

int Build(int x)
{
    vector<int> p;
    vector<int> q;
    if (ef[x]) p.emp(ef[x]);
    for (; son[x]; x = son[x])
    {
        q.clear();
        q.emp(ef[son[x]]);
        for (auto to : G[x])
        {
            if (to == f[x] || to == son[x]) continue;
            q.emp(Build(to));
        }
        p.emp(Solve(q, 0, q.size() - 1, 0)); // Rake
    }
    return Solve(p, 0, p.size() - 1, 1); // Compress
}

void dfs(int x, int fa)
{
    if (x) f[x] = fa, siz[x] = 1, dep[x] = dep[fa] + 1;
    for (auto to : G[x])
    {
        if (to == fa) continue;
        dfs(to, x);
        ef[to] = ++tot;
        tr[tot].u = x, tr[tot].d = to;
        tr[tot].siz = 1;
        if (x) siz[x] += siz[to];
        if (siz[to] > siz[son[x]]) son[x] = to;
    }
}

void dfs2(int x, int fa)
{
    top[x] = fa;
    if (son[x]) dfs2(son[x], fa);
    for (auto to : G[x])
    {
        if (to == son[x] || to == f[x]) continue;
        dfs2(to, to);
    }
}

int Lca(int x, int y)
{
    while (top[x] != top[y])
    {
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
        x = f[top[x]];
    }
    return dep[x] < dep[y] ? x : y;
}

int GetDis(int x, int y)
{
    return dep[x] + dep[y] - 2 * dep[Lca(x, y)];
}

vector <int> E[N];

void Update(int x, int val)
{
    for (auto y : E[x])
    {
        tr[y].vs->Update(GetDis(x, tr[tr[y].fa].m), -v[x]);
    }
    v[x] = val;
    for (auto y : E[x])
    {
        tr[y].vs->Update(GetDis(x, tr[tr[y].fa].m), v[x]);
    }
    // v[x] = val;
    // y = ef[x];
    // while (tr[y].fa)
    // {
    //     if (x != tr[tr[y].fa].m) tr[y].vs->Update(GetDis(x, tr[tr[y].fa].m), v[x]);
    //     y = tr[y].fa;
    // }
}

void PreWork(int x)
{
    if (!x) return ;
    PreWork(tr[x].ls), PreWork(tr[x].rs);
    if (!tr[x].fa) return ;
    tr[x].sons = new int[tr[x].siz + 2];
    if (!tr[x].ls)
    {
        tr[x].sons[1] = tr[x].d;
        tr[x].sons[2] = tr[x].u;
        tr[x].siz = 2;
    }
    else
    {
        int p = 0;
        for (int i = 1; i <= tr[tr[x].ls].siz; i++)
        {
            if (tr[tr[x].ls].sons[i] != tr[x].m) tr[x].sons[++p] = tr[tr[x].ls].sons[i];
        }
        for (int i = 1; i <= tr[tr[x].rs].siz; i++)
        {
            if (tr[tr[x].rs].sons[i] != tr[x].m) tr[x].sons[++p] = tr[tr[x].rs].sons[i];
        }
        tr[x].sons[++p] = tr[x].m;
        tr[x].siz = p;
    }
    tr[x].vs = new ArrayTree(tr[x].siz);
    for (int i = 1; i <= tr[x].siz; i++)
    {
        if (tr[x].sons[i] != tr[tr[x].fa].m) tr[x].vs->Update(GetDis(tr[tr[x].fa].m, tr[x].sons[i]), v[tr[x].sons[i]]), E[tr[x].sons[i]].emp(x);
    }
}

int Query(int x, int k)
{
    int y = ef[x], ans = 0;
    while (tr[y].fa)
    {
        if (y == tr[tr[y].fa].ls)
        {
            int dis = GetDis(tr[tr[y].fa].m, x);
            ans += tr[tr[tr[y].fa].rs].vs->Query(k - dis);
        }
        else
        {
            int dis = GetDis(tr[tr[y].fa].m, x);
            ans += tr[tr[tr[y].fa].ls].vs->Query(k - dis);
        }
        y = tr[y].fa;
    }
    return ans + (k != 0) * v[f[x]] + v[x];
}

signed main()
{
    // freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);

    int n, m; cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i];
    for (int i = 1, x, y; i < n; i++) cin >> x >> y, G[x].emp(y), G[y].emp(x);
    G[0].emp(1);
    dfs(0, 0), dfs2(0, 0);
    int rt = Build(0), last = 0;
    PreWork(rt);
    for (int i = 1; i <= m; i++)
    {
        int op, x, y; cin >> op >> x >> y;
        x ^= last, y ^= last;
        // if (i == 418) PreWork(rt);
        if (op == 0) cout << (last = Query(x, y)) << '\n';
        else Update(x, y);
    }

    return 0;
}

例题:

P6329 【模板】点分树 / 震波。

Top Tree 分块

这个东西基本就是树分块,不过拥有簇的极强性质。

直接从根 dfs,找到所有不交的极大的大小小于等于 \(B\) 的簇,由于重量平衡可以发现簇数量级为 \(O(\frac{n}{B})\),成功实现树分块。

对于与路径信息联系紧密的问题可以维护每个点到达最近的簇路径上点,上簇路径之前直接暴力,上了簇路径会有一个前缀散块,也直接暴力,剩下的都是整的簇路径了,打标记就行。

剩下的基本就是普通的树分块了。

例题:

P6177 【模板】树分块 / Count on a tree II。

P6778 [Ynoi2009] rpdq。

Top Tree 扩展用法

接下来的应用将不再局限于树,也就是需要处理环了。

引入新的操作,\(\operatorname{Twist}\)。

当 \(a, b\) 的两个界点都相等时定义 \(\operatorname{Twist}(a, b) = (x, y, E_a\cup E_b)\),翻译一下就是将两条重边合并。

有了这个操作,就可以处理环的问题了。

仙人掌图上的应用

参见本文。

广义串并联图上的应用

基本广义串并联图应用

什么是广义串并联图呢,其实就是可以使用 \(\operatorname{Rake},\operatorname{Compress},\operatorname{Twist}\) 三种操作缩为一条边的图。

这样的图不存在与 \(K_4\) 同胚的导出子图,这里做题的时候只需要看看 \(K_4\) 有没有特殊的性质就行,比如有 \(K_4\) 就不合法之类的。

那么将上面的操作描述的更简单一些就是每次可以将二度点缩起来,将一度点删除,将重边合并。

这个直接写一个队列就行,是 \(O(n\log n)\) 的。

在维护的时候需要额外考虑 \(\operatorname{Twist}\) 的转移。

代码已经比较普及了,不放了。

广义串并联上动态 DP

纯口胡,谨慎观看。

需要注意的是,无论如何,我们是不可能在广义串并联图上建出树高为 \(O(\log n)\) 的 Top Tree,因为考虑一张两个点宽网格图类似状物其树高只能是 \(O(n)\) 的。

那么我们如何做 DDP 呢?

假设维护信息的状态数为 \(k\)。

那么现在考虑对于 Top Tree 构建 Top Tree,维护界点所在簇的界点信息,状态数会翻倍,从之前的 \(k^2\) 变成了 \(k^4\)。

普通的 Top Tree 维护 DDP 需要界点信息一致,映射在现在这个问题上便是 Top Tree 的父亲兄弟之间的界点信息一致,父亲的限制好刻画,而兄弟的限制则考虑维护上界点在簇路径内的儿子的界点信息,这么做的话状态数会变成 \(k^5\) 的,其他做法的话不太清楚。

广义串并联上分治

参考 1:《浅谈静态 Top Tree 在树和广义串并联图上的应用》,2023 国家集训队论文集。

参考 2:ExplodingKonjac - Top Tree 相关理论扯淡。

纯口胡,谨慎观看。

首先引入一种 Top Tree 上的边分治方式,对于 Top Tree \(T\),我们定义 \(T'\) 的收缩树为:

  1. 将原树上 \(T'\) 中深度最小的点的子树外的边全都删除。
  2. 对于 \(T - T'\) 所有不与根连通的点进行对应的收缩操作。
  3. 所得到的收缩树便是 \(T'\) 的收缩树。

更加直观的理解方式就是将 \(T'\) 子树外的边删掉,然后将 \(T'\) 叶子对应的簇变成一条边。

分治方式就是假设现在分治到了 Top Tree 上的 \(T\),找到一条边 \((x, fa_x)\) 使得两边的点数尽可能相等,然后将 \(T\) 分为 \(subtree_x\) 和 \(T - subtree_x + {x}\) 两个连通子图,统计 \(T\) 的收缩树中所有端点为界点或端点不在同一个连通子图中的路径信息,分别递归下去。

理解一下收缩树其实就是将已经考虑过的那些簇收缩成一条边。

于是这样的话,特殊处理一下界点处的算重就好了,分析复杂度的话,由于递归 \(k\) 层最多增加 \(k\) 条不存在的边,而一条边不会同时分治到两边,于是收缩树总边数是 \(n\log n\) 的。

现在将目光放回广义串并联图上。

由于一开始定义收缩树的时候删除了所有不在子树内的边,但是有一些路径是可能从子树内走到界点,再从子树外走到另一个界点回到子树内。

假设现在处理 \(T'\) 连通子图,假设深度最小的点为 \(r\),那么在 \(r\) 之外的图也是一张广义串并联图,如果能将其缩起来,就可以用这个簇代替 \(r\) 子树外的部分。

考虑现在分治过程,分治到 \(T\),那么 \(T\) 的子树外的等价簇已经求出,假设 \(T' = subtree(x)\),那么如果维护信息允许,可以在 \(O(|T - T'|)\) 时间内求出 \(T'\) 的子树外等价簇。

这样子复杂度还是 \(O(V\log V)\) 的。

可能就是在复述论文。

广义串并联上分块

和上面一样,纯口胡,谨慎观看。

比较有意思的东西。

沿用上面的收缩树定义。

对于一个 Top Tree,对这棵 Top Tree 进行树分块,由于使用 Top Tree 进行树分块,那么分块出来的簇一定是满足之前提到的 Top Tree 性质的。

定义分块后的一个节点是 Top Tree 上一个块的收缩树。

定义分块后的一个节点在 Top Tree 上的深度最小的点所对应的界点为这个节点的外界点。

那么有以下性质:

  1. 块数 \(O(\frac{n}{B})\),块内点边数量 \(O(B)\)。
  2. 每个点只会作为一个块的非外界点存在。
  3. 每条边只在一个块内。
  4. 对于块 \(C\),其最多仅会与两个块在非外界点处有交,并根据 Top Tree 分块的性质,这两个块一定是块 \(C\) 在 Top Tree 上的下界点的儿子,由于相交的两个块及其子树一步便可缩成一个簇,于是仅会与 \(C\) 在两个点相交,定义为 \(C\) 的内界点。

与树分块不同的是,块内的点间路径也有可能经过界点,并且跨块贡献需要分讨所在块的四个界点。

假设现在需要做带修广义串并联图上邻域数点。

给定 \(n\) 个点 \(m\) 条边的广义串并联图 \(G\),\(q\) 次操作:

  1. 给定 \(i, v\),将第 \(i\) 条边边权修改为 \(v\)。
  2. 给定 \(x, d\),查询到 \(x\) 最短路 \(\leq d\) 的点数。

对于询问 \(x, d\) 而言,分类讨论 \(x, y\) 最短路之间是否经过了界点,如果没有,则说明 \(x, y\) 在同一个块内,直接跑单源最短路,需要注意,即使在同一个块内的两点,最短路也可能经过界点,这样的 \(y\) 只有 \(O(B)\) 个,用下面的方式特判就行。

如果 \(x, y\) 最短路经过界点,那么可以统计形如 \(x\to u, u\to v, v\to y\),其中 \(u, v\) 为界点,并且 \(x\to u, v\to y\) 均不经过界点。

那么我们会发现,假设 \(x, y\) 均不为界点,那么 \(u\) 只能是 \(x\) 所在块内的四个界点,\(v\) 只能是 \(y\) 所在块内的四个界点。

如果我们能方便的求出 \(x\to v\) 的最短路径,那么将询问离线,对每个块的四个界点,可以求出 \(t = d - dis(x, v)\),将每一个都作为一维坐标,那么不合法的 \(y\) 就是到四个界点距离均大于对应 \(t\),这个可以四维数点。

那么怎么求出 \(x\to v\) 的最短路径呢?

也就是求出 \(x\to u \to v\) 其中 \(x\to u\) 不经过其他界点。

我们发现 \(x\to u\) 是好求的,于是只需要求出任意两个界点之间的最短路径。

这个还是不好维护,如果 \(u\to v\) 的最短路中经过了某个界点,那么可以直接用那个界点,否则就变成了统计 \(u\to x\to v\) 其中 \(u\to x, x\to v\) 不经过其他界点,这个可以在修改的时候暴力重构整个块实现。

最后在修改的时候将之前离线的询问查询掉,再更新最短路径即可。

复杂度的话取决于四维数点,\(O((q\sqrt{n} + n)\log^3 n)\) 或 \(O(\frac{nq}{w} + q\sqrt{n}\log{n})\) 的。

动态 Top Tree

就提一嘴 SATT。

首先需要会 Splay 以及 LCT。

在 LCT 中,我们对于一棵树,维护一堆实链,实链之间用虚边连起来,而如果我们将所有的虚边使用平衡树维护起来,这不就是 Top Tree 吗?

维护的虚边是 \(\operatorname{Rake}\) 树,而实链则是 \(\operatorname{Compress}\) 树。

于是我们魔改 Splay,维护中儿子表示虚儿子的平衡树,也就是 \(\operatorname{Rake}\) 树。

维护虚儿子的平衡树我们使用 leaf-Splay。

将维护虚儿子的平衡树中节点称为虚节点。

我们的 \(Splay(x)\) 便是将 \(x\) 旋转到当前 \(\operatorname{Rake}\) 树或 \(\operatorname{Compress}\) 树的根。

我们的 \(Access(x)\) 操作需要先 \(Splay(x)\),并将右儿子插入中儿子中,然后不断打通 \((x, fa_x)\),方式是将 \(x\) 提到根,此时 \(fa_x\) 是虚节点,然后将 \(fa_x\) 提到根,此时 \(fa_{fa_x}\) 便是 \(x\) 所在实链的链顶的父亲,将其提到根并把右儿子丢进中儿子,反复此操作即可。

进行子树查询的时候需要精细实现标记下放,除此之外不算太难写。

写的不知道对不对的代码。

例题:

P5649 sone1。

总结

这是一个极具美感并极其强大的数据结构,期待有兴趣的读者继续深入,挖掘更多有意思的 Top Tree 的应用!

全文用时数个晚上,感谢您的观看啦。

参考资料

多篇文章,不同角度,放在一起食用可能会有更深的理解。

  1. 程思元,《浅谈静态 Top Tree 在树和广义串并联图上的应用》,2023 国家集训队论文集。
  2. negiizhao,《Top tree 相关东西的理论、用法和实现》。
  3. ExplodingKonjac,《Top Tree 相关理论扯淡》。
  4. Richard_Whr,《Top Tree & Top Cluster 树分块》。
posted @ 2026-11-05 20:34  QEDQEDQED  阅读(26)  评论(4)    收藏  举报