Self-Adjusting Top Tree

简单介绍:

Self-Adjusting Top Tree , 也叫做SATT或者TopTree , 是2005年由Robert E. Tarjan 和 Renato F. Werneck 等人发布的论文:《Self_Adjusting_Top_Tree》中提到的,一种全新的处理动态树问题的方法或者说数据结构。非常的全能,可以支持一些ETT或者LCT没有办法支持的东西,比如动态直径,动态重心等等。但是由于代码过长,而且非常晦涩难懂,所以没有什么人去使用(当然像Sone1这种史诗级重工业你当我没说),那么这篇文章,就带你学习以下那些OIer闻风丧胆的SATT,当然,为了学好SATT,你可能需要一些基础的图论以及动态树的前置Cheese,不然非常难理解哦!好了,除法把!

树的收缩:

在Oi-Wiki上就说过这么一句话"对于任意一棵树,我们都可以运用 树收缩 理论来将它收缩为一条边。" 这个树收缩听起来神乎其神,但其实最难理解的是它的英文名字,也就是Compress和Rake,简单来讲,Compress主要实现以下的东西:

对于三个度数为 \(1\) 或者 \(2\) 的节点,把它们看作一条链,
graph TD 1 <--> 2 2 <--> 3

其中 \(1\) 和 \(3\) 的度数为 1 ,\(2\) 度数为 \(2\) ,然后,Compress(2) 操作就要通过把 \(2\) 号节点通过一种方法给收缩掉,类似于缩点的感觉。把 \(1 \to 2\) , \(2 \to 3\) 的这两条边,包括 \(2\) 号节点统统删掉,然后新建一条 \(1 \to 3\) 的边,把上面删掉的部分得到的信息全部储存到这一条边上,然后图就变成了
graph TD 1 <--> 3

二号节点就消失了,但是这样还不够,假设我有这样的一颗树呢?
graph TD 1 <--> 2 2 <--> 3 3 <--> 4 2 <--> 5

然后你就会发现通过若干次Compress操作之后,要么把 \(1 , 2 , 5\) 合并成为一条边,要么把 \(1,2,3,4\) 合并成一条边 ,而且还会存在无法合并的情况,无论如何没有办法把分支弄进来,难道说Oi-Wiki写错了?不不不,其实在论文中还提出了一种操作,也就是Rake,其主要的用处就是我们上面所说的,把分叉给弄掉,形如:
graph TD 1 <--> 2 2 <--> 3 3 <--> 4 2 <--> 5

为了把 \(2\) 其中一个边给消除,使其变成一个可以Compress的链状结构,我们可以尝试把 \(2 \to 5\) 这条边的信息放到 \(2 \to 3\) 去处理,然后删除这条边,显而易见,变成了:
graph TD 1 <--> 2 2 <--> 3 3 <--> 4

此时可以Compress处理,但是注意 , Rake操作选中的删除的那一条边,必须没有子树,也就是说我们无法选中具有子树的 \(2 \to 3\) 直接Rake,因为Rake之后将会产生多个连通块而不是树,无法正常统计信息,当然也可以选择把 \(3\) 号节点的儿子全部处理之后再Rake,方法是不唯一的

但是说了这么多,Compress 和 Rake 到底有什么用处呢?在说这个之前,我们现需要了解,什么是簇。

简单来说,簇是一个联通子图 ,是若干次树树收缩(当然可以为0次)

中包含的节点,簇具有以下性质:

  • 首先,连通性(Connectedness) , 意味着任何一个簇都是原树的一个联通子图,理由很简单,树收缩的过程中不会改变联通性。
  • 具有边界(Bounded Boundary),也就是说,每一个簇有且仅有两个端点联通外界,这个也非常好证明,因为树的收缩会将一个簇收缩成为一条边,而边的端点也就是簇的端点。
  • 合并性(Composability):
    任意两个簇之间可以进行合并,过程就是若干次树收缩。这个性质非常重要,影响到后面TopTree的结构,务必了解清楚。
  • 稳定性(Dynamic Consistency):
    当原树通过Link或者Cut发生变化之后,簇的集合经过调整之后依然可以维护所有的信息,这也是为什么有TopTree的原因。

接下来,我们利用性质2来标记一个簇,我们定义 \(\text{Cu}(x , y)\) 表示边界节点是 \(x\) 和 \(y\) 的簇,可以证明这种簇是唯一的。当一个有 \(n\) 个节点的树,其中 \(\text{Cu}(1 , n)\) ,也就是棵树,我们叫做根簇 ,而对于 \(i\) 有 \(i \in n\) 我们定义 \(\text{Cu}(i,i)\) 叫做叶簇 或者基簇 。根据性质4我们可以知道对于任意一个簇而言,进行一次树收缩必然会将原有的两个簇合并,比如:
flowchart TD subgraph S1 1 <--> 2 1 <--> 5 end subgraph S2 2 <--> 3 3 <--> 4 end

经过一次Compress操作之后,可以发现两个簇被合并了,但是有什么用呢?我们依然没有办法维护动态加边/删除边,那么现在,我们正式引入TopTree!

TopTree

首先,你必须知道,TopTree和LCT是一样的,和原树可谓毫无关系,TopTree展现的是原树的一种树收缩的方法,比如对于:
graph TD 1 <--> 2 1 <--> 3 2 <--> 4 2 <--> 5 3 <--> 6 3 <--> 7

我们可以建出一个TopTree如下:
flowchart TD %% 定义所有簇 C12("Cu(1-2)") C13("Cu(1-3)") C24("Cu(2-4)") C25("Cu(2-5)") C36("Cu(3-6)") C37("Cu(3-7)") Rake2("Rake₂<br>点簇: 合并Cu(2,4), Cu(2,5)") Rake3("Rake₃<br>点簇: 合并Cu(3,6), Cu(3,7)") Compress2("Compress₂<br>边簇: 合并Cu(1,2), Rake₂") Compress3("Compress₃<br>边簇: 合并Cu(1,3), Rake₃") Root("Root<br>点簇: 合并Compress₂, Compress₃") %% 构建连接关系 C24 --> Rake2 C25 --> Rake2 C36 --> Rake3 C37 --> Rake3 C12 --> Compress2 Rake2 --> Compress2 C13 --> Compress3 Rake3 --> Compress3 Compress2 --> Root Compress3 --> Root

你会非常惊奇的发现,这个 TopTree没有可以展示节点之间的关系,而是展示了簇与簇之间的合并关系,这么做用意何在,我们又要怎么构建或者维护一颗TopTree呢?欸,这就是我们接下来要说的,不过在此之前,你应该先了解以下TopTree的相关性质:

  • 首先,TopTree的叶子节点代表的一定是叶簇,而且任何一个节点一定是通过子节点Rake或者Compress得来的,根节点则代表了整棵树。
  • 其次,对于原树中的任何一条边,存在且仅存在于一个叶簇中,这样计算会变得非常方便。
  • 然后,TopTree呈现的是原树的一种收缩方式,一个节点可能有三个儿子(这个后面细说)

了解了基本性质,改学一下怎么构建一颗TopTree了?别急,你还需要知道,什么是Compress Tree和Rake Tree

Compress & Rake 2

Compress tree 的出现,就好比动态树里来了一个救世主。容易发现如果对于一条长路径逐个逐个搜索时间复杂度将会非常高,询问的瓶颈也就在这里,所以为了将长的,连续的路径压缩成一条边,我们需要引入这种神奇的树,简单来讲,Compress Tree 的操作过程如下:

首先:

  • 选择一条路径来进行分解,一个非常经典的标准就是选用重链组成的路径来进行划分。
  • 接下来将将选定的路径全部压缩成一条边。
  • 那么这个边会有两个边界节点,即原路径的起点以及重点。
  • 原本与路径产生了分叉的子树暂时保持不变,将他们看作是悬挂在这个边上面。
  • 通过一层一层展开这一条路径,就得到了我们最终的Compress Tree。
    你可能会感到奇怪,什么是展开嘞?为什么没有处理分叉嘞?好问题,第一个问题你可以理解为类似线段树的做法,把路径中取出一个点,把这一条路径拆成两个更小的路径然后逐层递归,至于为什么没有考虑悬挂的边,这是因为我们将要引入第二个东西,Rake Tree。

所谓 Rake Tree , 其策略恰好与Compress Tree 逻辑相反,其不关心长路径,而是关心悬挂在那些长路径上的边组成的簇。

简单来讲。为了Compress Tree 能够好好收拾那些长路径,那么Rake Tree 就要把 多余的边通过在 Compress Tree 递归暴露节点的时候插上去,是不是有些复杂,我们简单点说:

Rake Tree 的工作原理主要是:

  • 首先,找到所有叶子节点。
  • 接下来,对于一个叶子节点 \(u\) 和其父亲节点 \(v\) , 将 \(u \to v\) 和节点 \(u\) 装的所有信息全部转移到点 \(v\) 上, 这样就形成了一个新的簇。但是注意, \(v\) 不一定是一个Compress Tree 上的节点,Rake tree 上也并非只有 Rake 操作,是可以进行Compress 操作的哦!

但是怎么说,Compress Tree 和 Rake Tree 还是八竿子打不着的关系,怎么联系到一起呢?我们先对于一棵树,建好所有的 Rake Tree 和 Compress Tree 。接下来,为了产生Top Tree ,我们需要它们两个的组合技!合并!

Top Tree 2

当你还在思索 Top Tree 怎么用一种特殊的二叉树表示的时候,Tarjan微微一笑,TopTree的设计是非常反人类的,使用了一种三叉树的结构,就为了节点的挂载问题。当我们把Compress Tree 展开,直至可以放置挂载的 Rake Tree 节点,没错,上面这句非常关键,你可以理解一下,为了把 Rake Tree 挂载到 Compress Tree 上, 假设 Rake Tree 的挂载节点为 \(x\) , 那么挂载的位置至少就要暴露一个端点来用于挂载,也就是说,假设我们挂载的位置是 \(3\) , Compress Tree 的根节点区间为 \(\text{Cu}(1 , 5)\) , 接下来就要分裂,假设我们拆成了如下形状:
graph TD nd1("Cu(1,5)") nd2("Cu(1,3)") nd3("Cu(4,5)") nd4("Cu(1,2)") nd5("Cu(3,3)") nd6("Cu(4,4)") nd7("Cu(5,5)") nd1 <--> nd2 nd1 <--> nd3 nd2 <--> nd4 nd2 <--> nd5 nd3 <--> nd6 nd3 <--> nd7

这样就把一颗 Comprese Tree 构建出来了,但是好问题,Rake Tree 要放在哪里呢?显然在图中,我们有两个位置可以挂,那么我们要给出以下挂载的要求了:

首先,如果要合格地进行 RAKE(a,b) 我们要满足:

  • \(b\) 必须是一个点簇(Vertex Cluster) ,即它只能有一个边界节点(虽然有点反常识,但是确实存在,可以自己举例子说明),我们称这个边界节点名字为 \(u\) , 则 \(|\partial b| = 1\) 而且 \(\partial b = {u}\) 。
  • 接下来 ,簇 \(b\) 的边界节点必须和簇 \(a\) 的某个边界点 \(v\) 在原树中是邻居。
  • 最后,\(a\) 和 \(b\) 不相交。
    满足以上三个条件之后,可以证明这样的位置是唯一的。

经过我们的挂载之后,可以发现,一个TopTree Node 可能有三个儿子,分别是两个分裂的 Compress Node 和一个 Rake Node ,特别的 , Rake Node 一般作为中儿子,具体原因后面会说。讲完了TopTree的基本构造,是时候上点硬菜了,作为一个LCT必须有的,也就是函数部分。

函数の实现

声明:内容为博主原创但是限于文笔和思路问题,以下结构可能和OI-Wiki相似,也确实存在相关借鉴问题,不喜轻喷。其实Top Tree 有两种实现方法,一种是我们说过的三叉实现,还有一种依然是由Tarjan提出的,二叉实现。你可能大吃一惊,一个节点上明明就会分叉出两个节点,有可能还会附加一个 Rake Node , 怎么可能可以二叉呢?这正是二叉的精妙之处,即区分节点的类型,也要区分节点边界的意义 ,首先我们把 Compress Node 标记为非 Rake 节点,为什么要这么做,原因后面会说。而且我们知道,对于一个Compress Node 连接起来的必然是一条链,其左儿子必然在当前节点上方,右儿子必然在当前节点下方,其维护的值也是链上的值。然后,对于所有 Rake Node 全部标记上是 Rake Node ,我们可以将这个节点理解为一个某一个链上的分支,其左儿子是链本身,右儿子是挂在链上的某一个节点。

然后,Compress节点可以通过合并左右儿子的信息来得到比如路径和,路径上最大等信息,而 Rake Node 可以从左儿子继承路径星系,同时合并左右儿子的子树信息(比如子树和)。

你可能感到疑惑,这种方法和上面说的毫无一点相似之处,甚至方法你无法看懂,没事,我给出一个 Top Tree 示例,你马上就能懂,首先我们给出一个树形如:
graph TD 1 <--> 2 2 <--> 3 1 <--> 4 3 <--> 5 3 <--> 6

那么其二叉树形式就是:
graph TD %% 根节点 Root[R<br/>is_rake=true] %% 第一层 R1[R<br/>is_rake=true] E[E: 3-6] %% 第二层 R2[R<br/>is_rake=true] D[D: 3-5] %% 第三层 C1[C<br/>is_rake=false] C[C: 1-4] %% 第四层 A[A: 1-2] B[B: 2-3] %% 连接关系 Root --> R1 Root --> E R1 --> R2 R1 --> D R2 --> C1 R2 --> C C1 --> A C1 --> B %% 样式 classDef rake fill:#e1f5fe,stroke:#01579b,stroke-width:2px classDef compress fill:#f3e5f5,stroke:#4a148c,stroke-width:2px classDef leaf fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px class Root,R1,R2 rake class C1 compress class A,B,C,D,E leaf %% 说明文字 linkStyle 0,1,2,3,4,5,6 stroke:gray,stroke-width:1px

那问题又来了,怎么构建一个完美的二叉 TopTree 呢?首先我们先找出所有的叶簇,然后第一次,选出一号和二号叶簇合并,看合并的节点是 Compress 还是 Rake , 记录一下,然后第二次,由第一次合并后的结果和第三个叶簇合并,看是 Compress 还是 Rake , 然后以此类推,不难。难就难在怎么使用函数,首先就是 pushup 函数,这是最简单也算是最重要的函数,基本所有信息都有其维护,根据我们上面说的,可以很轻松写出如下代码:

cpp 复制代码
void update(SATNode* x) {
    if (!x) return;
    
    push(x->ch[0]);
    push(x->ch[1]);
    
    if (!x->is_rake) {
        // COMPRESS 节点: 合并路径信息
        x->path_sum = x->val;
        x->subtree_sum = x->val;
        x->max_val = x->val;
        x->size = 1;
        
        if (x->ch[0]) {
            x->path_sum += x->ch[0]->path_sum;
            x->subtree_sum += x->ch[0]->subtree_sum;
            x->max_val = max(x->max_val, x->ch[0]->max_val);
            x->size += x->ch[0]->size;
        }
        if (x->ch[1]) {
            x->path_sum += x->ch[1]->path_sum;
            x->subtree_sum += x->ch[1]->subtree_sum;
            x->max_val = max(x->max_val, x->ch[1]->max_val);
            x->size += x->ch[1]->size;
        }
    } else {
        // RAKE 节点: 继承路径信息,合并子树信息
        x->path_sum = x->ch[0] ? x->ch[0]->path_sum : 0;
        x->max_val = x->ch[0] ? x->ch[0]->max_val : -1e9;
        x->subtree_sum = x->val;
        x->size = 1;
        
        if (x->ch[0]) {
            x->subtree_sum += x->ch[0]->subtree_sum;
            x->size += x->ch[0]->size;
        }
        if (x->ch[1]) {
            x->subtree_sum += x->ch[1]->subtree_sum;
            x->size += x->ch[1]->size;
        }
    }
}

然后就是下方懒标记,实现同样容易:

cpp 复制代码
void update(SATNode* x) {
    if (!x) return;
    
    push(x->ch[0]);
    push(x->ch[1]);
    
    if (!x->is_rake) {
        // COMPRESS 节点: 合并路径信息
        x->path_sum = x->val;
        x->subtree_sum = x->val;
        x->max_val = x->val;
        x->size = 1;
        
        if (x->ch[0]) {
            x->path_sum += x->ch[0]->path_sum;
            x->subtree_sum += x->ch[0]->subtree_sum;
            x->max_val = max(x->max_val, x->ch[0]->max_val);
            x->size += x->ch[0]->size;
        }
        if (x->ch[1]) {
            x->path_sum += x->ch[1]->path_sum;
            x->subtree_sum += x->ch[1]->subtree_sum;
            x->max_val = max(x->max_val, x->ch[1]->max_val);
            x->size += x->ch[1]->size;
        }
    } else {
        // RAKE 节点: 继承路径信息,合并子树信息
        x->path_sum = x->ch[0] ? x->ch[0]->path_sum : 0;
        x->max_val = x->ch[0] ? x->ch[0]->max_val : -1e9;
        x->subtree_sum = x->val;
        x->size = 1;
        
        if (x->ch[0]) {
            x->subtree_sum += x->ch[0]->subtree_sum;
            x->size += x->ch[0]->size;
        }
        if (x->ch[1]) {
            x->subtree_sum += x->ch[1]->subtree_sum;
            x->size += x->ch[1]->size;
        }
    }
}

然后就是我们的Splay和Accese等高级函数的环节了,注意到这棵树其实是一颗二叉树,可以直接进行操作:

cpp 复制代码
// 判断是否为根节点
bool is_root(SATNode* x) {
    return x->fa == nullptr || 
          (x->fa->ch[0] != x && x->fa->ch[1] != x);
}

// 获取节点在父节点中的方向
int dir(SATNode* x) {
    if (x->fa == nullptr) return -1;
    return x->fa->ch[1] == x ? 1 : 0;
}

// 设置子节点
void set_child(SATNode* parent, SATNode* child, int direction) {
    if (parent) parent->ch[direction] = child;
    if (child) child->fa = parent;
}

void rotate(SATNode* x) {
    SATNode* y = x->fa;
    SATNode* z = y->fa;
    int dir_x = dir(x);
    int dir_y = dir(y);
    
    // 处理x的兄弟节点
    set_child(y, x->ch[1 - dir_x], dir_x);
    
    // 提升x
    set_child(x, y, 1 - dir_x);
    
    // 连接到祖父节点
    if (z && !is_root(y)) {
        set_child(z, x, dir_y);
    } else {
        x->fa = z;
    }
    
    update(y);
    update(x);
}
void splay(SATNode* x) {
    // 先push所有祖先的标记
    vector<SATNode*> path;
    for (SATNode* curr = x; curr; curr = curr->fa) {
        path.push_back(curr);
    }
    for (int i = path.size() - 1; i >= 0; i--) {
        push(path[i]);
    }
    
    while (!is_root(x)) {
        SATNode* y = x->fa;
        if (!is_root(y)) {
            SATNode* z = y->fa;
            if (dir(x) == dir(y)) {
                rotate(y);  // zig-zig
            } else {
                rotate(x);  // zig-zag
            }
        }
        rotate(x);
    }
    push(x);
    update(x);
}

接下来就是非常重要的函数,决定了我们能不能写出一份优秀的 TopTree , 我们一个一个来讲。

1. expose(x)

起作用非常简单,将节点 \(x\) 到原树根的路径变成 优先路径(Preferred Path) , 并使得 \(x\) 成为其所在的辅助树上的根,流程很简单,我们定义 last = nullcur = x 然后循环以下步骤:

  • cur Splay 提到局部的根的位置。
  • 如果当前的 cur 有右儿子,那么临时断开其右儿子。
  • 如果没有右儿子,直接连接 last
  • 设置 cur 的右儿子为 last
  • 更新 'cur 和 'last

可以很轻松写出如下代码:

cpp 复制代码
SATNode* expose(SATNode* x) {
    SATNode* last = nullptr;  // 记录已经处理好的路径段
    SATNode* curr = x;        // 当前正在处理的节点
    
    while (curr) {
        splay(curr);  // 将curr提到当前辅助树的根
        
        // 关键步骤1: 处理旧的右儿子(如果存在)
        if (curr->ch[1]) {
            // 临时断开右儿子,但不删除
            // 只是让它的fa指针暂时为null
            curr->ch[1]->fa = nullptr;
            
            // 注意:这里没有设置 curr->ch[1] = nullptr!
            // 旧的右儿子仍然可以通过其他方式访问
        }
        
        // 关键步骤2: 连接新的路径段
        if (last) {
            // 将已经处理好的路径段挂载为右儿子
            curr->ch[1] = last;
            last->fa = curr;
        }
        
        // 更新指针,继续向上处理
        last = curr;      // 当前节点成为新的"已处理段"
        curr = curr->fa;  // 处理父节点
    }
    
    splay(x);  // 最后将x提到最顶部
    return x;
}

2. findroot(x)

findroot 也很简单,就是找到包含节点 \(x\) 的树的根,根据Splay的性质容易得到,如果 \(x\) 处于当前辅助树的树根,那么编号比 \(x\) 小,即深度更小的点显然在其左儿子上,直接往左儿子找就可以,没什么技术含量,直接看代码。

cpp 复制代码
SATNode* find_root(SATNode* x) {
    expose(x);      // 先将x提到辅助树根
    
    // 然后一直向左走(因为左儿子是朝向根的方向)
    while (x->ch[0]) {
        x = x->ch[0];
        push(x);    // 记得下传标记
    }
    
    splay(x);       // 将真正的根提到辅助树根
    return x;
}

3. makeroot

将 \(x\) 变成整棵树的根节点位置。思路非常巧妙 , 首先,如果我们使用expose打通与根节点之间的连接,形成一条链,那么意味着这个节点必然是这一条链上下面的端点,那么此时为了让当前节点成为上面的端点,直接反转整个路径就可以了,这里需要下传懒标记哦!

在 \(u\) 到 \(v\) 之间连接一条边,问题来了,我们的辅助树是一个完全由 Rake Tree 和 Compress Tree 构建的辅助树,怎么添加一条边呢?我们前面就说了 , Compress / Rake Tree 添加边的过程实际上就是合并两个簇,那么加边同理,我们可以通过两次makeroot让两个节点相邻,让后创建一个新的簇把 \(u\) 当作左儿子, \(v\) 当作右儿子然后更新节信息,cut 同理。

二叉树的 SATT 其实算难的,但是学完下来感觉......也就那样?也许你可能没看懂,没事,还有一种更加适合新手体制的三叉树结构!

三叉树结构实现 SATT

三叉树,在Oi-Wiki上叫做三度化实现,原理非常简单,和我上面说过的一样,即一个节点分成了两个子节点和一个Rake节点,构建是非常简单的,但是函数是非常有难度的,比如一个很简单的问题,Splay怎么进行?儿子到底怎么放置,有没有什么技巧?这个都是我们接下来要探讨的问题,一个一个来看:

Push类操作:

首先先说一下Pushdn,这个Pushdn是那种没有人能够理解的巧妙,因为是动态树肯定要makeroot,那对于一个三叉树怎么翻转呢?我们上面提到了,对于一个Compress树的结构其左儿子是上面的部分,右儿子是下面的部分,而中间儿子是悬挂在当前节点的 Rake Node , 当我们翻转整个路径的时候,对于当前层来讲,我们只关心路径上的反转,和节点没有关系,这点需要你回顾一下LCT,你会惊奇的发现,我们在LCT下放Reverse标记的时候,对于当前节点而言永远自己是不会变动位置的,只是交换了左子树和右子树,那么意思很明确了,我们只要把Rake Node 放在中儿子就不会收到影响,直接反转就可以了,代码很短。然后对于更新的操作我们和LCT差不多,就不过多赘述。

旋转操作:

三叉的旋转操作和Splay以及Accese是最最最难的部分!没有之一!一定要认真看。三叉树的旋转是一个难题,我们先理清一下思路,思考一下,二叉树中的旋转我们有考虑过中儿子吗?没有,那同理,三叉树也不需要管理中儿子,虽然这一句话看着奇怪,但是是有真正意义在的,注意到我们在旋转或者Splay的时候,更多关心的是节点的左右儿子和关系,而中儿子就像个挂件一样随着当前节点移动而移动,要更新随时可以更新,那么我们就理清了Splay的方法了。这里也不给代码(可以自己想的AwA)

Accese

根据Oi-Wiki上讲述的,对于一个处于Compress局部根节点的节点其父亲必然是RAKE NODE , 处理的方法我们也说了,把父亲节点旋转到RakeNode的根节点,此时其爷爷节点必然是Compress节点,此时分情况讨论:

  • 如果其爷爷节点有右儿子,那么直接和其爷爷节点的右儿子互换,原因很简单,在Accese中,我们需要保证当前路径是连续的,避免破坏了其他部分。因为当前节点是仅次于爷爷节点的,深度最小的Compress 节点,性质与其右儿子性质相同,可以调整位置关系。
  • 如果其爷爷节点没有右儿子,那么看作是特殊的第一种情况,但是要把 \(x\) 的所有子树给处理掉,确保树的联通。
    然后重复步骤我们就搞定了,其实讲完了 Accese 就没什么难点了,接下来的代码和上面的步骤是一样的。

那么还是要恭喜你呀!骚年,又多学会了一种数据结构!

Reference

  1. Self_Adjusting_Top_Tree》,作者Robert E. Tarjan,和Renato F. Werneck
  2. Oi-Wiki
    3.《Deterministic Self-Adjusting Tree Networks Using Rotor Walks
相关推荐
helloyaren5 天前
Docker Desktop里搭建Mysql 9.4主从复制的保姆级教程
mysql·技术·主从复制
吃奶酪的猫25 天前
浅谈后缀自动机
技术·oi
KakaDBL1 个月前
SSH连接服务器正常显示GUI程序
技术
在未来等你2 个月前
Java企业技术趋势分析:AI驱动下的Spring AI、LangChain4j与RAG系统架构
java·spring·ai·编程·技术
Itsuka_Kotori3 个月前
关于接下来一年的计划和目标
oi
我会唱天意3 个月前
【Electron】electron-vue 借助 element-ui UI 库助力桌面应用开发
技术
我会唱天意4 个月前
Redis总结(六)redis持久化
技术
我会唱天意4 个月前
Java设计模式: 实战案例解析
技术
我会唱天意4 个月前
【iview】es6变量结构赋值(对象赋值)
技术