Self-Adjusting Top Tree

掌握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 函数,这是最简单也算是最重要的函数,基本所有信息都有其维护,根据我们上面说的,可以很轻松写出如下代码:

/* by 01130.hk - online tools website : 01130.hk/zh/json2cs.html */
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;
        }
    }
}

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

/* by 01130.hk - online tools website : 01130.hk/zh/json2cs.html */
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等高级函数的环节了,注意到这棵树其实是一颗二叉树,可以直接进行操作:

// 判断是否为根节点
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

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

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\) 小,即深度更小的点显然在其左儿子上,直接往左儿子找就可以,没什么技术含量,直接看代码。

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》
# -*- coding: utf-8 -*- import numpy as np import cv2 import time import math from pyzbar import pyzbar from connect_uav import UPUavControl from multiprocessing import Process from multiprocessing.managers import BaseManager cap = cv2.VideoCapture(0) no_slice = 4 center = (240, 227) # 全局速度限制 MAX_SPEED = 250 MAX_TURN_SPEED = 180 MAX_ADJUST_SPEED = 100 class LineFollower(): def __init__(self): self.is_follow = True self.no_line_count = 0 self.last_angle = 0 self.turn_count = 0 # 添加当前速度状态 self.current_speed = {'forward': 0, 'turn': 0, 'lateral': 0} # 二维码相关属性 self.scan_content = "" self.adjusting_for_landing = False self.qr_detected = False self.qr_adjust_count = 0 self.no_qr_count = 0 self.last_qr_position = None # 记录上一次二维码位置 self.last_qr_size = 0 # 记录上一次二维码大小 # 简化控制参数 self.min_qr_size = 100 # 二维码最小尺寸(像素)用于降落判断 BaseManager.register('UPUavControl', UPUavControl) manager = BaseManager() manager.start() self.airplanceApi = manager.UPUavControl() controllerAir = Process(name="controller_air", target=self.api_init) controllerAir.start() time.sleep(1) self.airplanceApi.onekey_takeoff(60) time.sleep(5) # 起飞后向前飞行3秒 - 提高速度 print("起飞完成,开始向前飞行...") self.set_speed('forward', 200) # 提高初始飞行速度 time.sleep(3) # 飞行3秒 # 停止向前移动 self.set_speed('forward', 0) print("向前飞行完成,开始巡线任务") self.start_video() super(LineFollower, self).__init__() def set_speed(self, direction, speed): """平滑速度过渡并限制最大速度""" # 限制最大速度 speed = min(speed, MAX_SPEED) # 平滑过渡 - 限制加速度 current = self.current_speed[direction] if abs(speed - current) > 40: speed = current + 40 if speed > current else current - 40 # 更新API if direction == 'forward': self.airplanceApi.move_forward(speed) elif direction == 'turn': if speed > 0: self.airplanceApi.turn_left(min(speed, MAX_TURN_SPEED)) else: self.airplanceApi.turn_right(min(-speed, MAX_TURN_SPEED)) elif direction == 'lateral': if speed > 0: self.airplanceApi.move_right(min(speed, MAX_ADJUST_SPEED)) else: self.airplanceApi.move_left(min(-speed, MAX_ADJUST_SPEED)) # 更新状态 self.current_speed[direction] = speed return speed def api_init(self): print("飞行控制进程启动") time.sleep(0.5) self.airplanceApi.setServoPosition(90) def start_video(self): last_frame_time = time.time() while cap.isOpened(): start_time = time.time() ret, frame = cap.read() if not ret: break # 使用ROI减少处理区域 (保留中间区域) height, width = frame.shape[:2] roi_top = height // 4 roi_bottom = height * 3 // 4 roi_left = width // 4 roi_right = width * 3 // 4 roi_frame = frame[roi_top:roi_bottom, roi_left:roi_right] # 缩放处理 frame = cv2.resize(roi_frame, (0, 0), fx=0.75, fy=0.75) # 根据状态调整二维码检测频率 qr_check_interval = 1 if self.adjusting_for_landing else 3 if self.qr_adjust_count % qr_check_interval == 0: self.decode(frame) self.qr_adjust_count += 1 # 如果识别到降落二维码且满足条件,执行降落 if self.scan_content == "landed" and self.adjusting_for_landing: # 检查二维码是否在中心且足够大 if self.is_qr_centered_and_large(): print("二维码在中心且足够大,执行降落") self.airplanceApi.land() time.sleep(3) cap.release() cv2.destroyAllWindows() return # 结束程序 # 处理二维码丢失情况 if not self.qr_detected and self.adjusting_for_landing: self.no_qr_count += 1 if self.no_qr_count > 30: # 连续30帧未检测到二维码 print("二维码丢失,停止调整") self.adjusting_for_landing = False self.no_qr_count = 0 else: self.no_qr_count = 0 # 如果正在调整位置准备降落,跳过黑线识别 if self.adjusting_for_landing: # 显示调整状态 display_frame = frame.copy() status_text = "Adjusting for Landing" cv2.putText(display_frame, status_text, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow('line_tracking', display_frame) # 动态帧率处理 processing_time = time.time() - start_time delay = max(1, int(30 - processing_time * 1000)) # 保持约30FPS if cv2.waitKey(delay) & 0xFF == ord('q'): break continue # 正常巡线处理 img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) img = self.remove_background(img, True) slices, cont_cent = self.slice_out(img, no_slice) img = self.repack(slices) if all(c[0] == 0 and c[1] == 0 for c in cont_cent): self.no_line_count += 1 print(f"无法识别线条,计数: {self.no_line_count}") if self.no_line_count >= 2: # 更快响应 print("连续多帧识别不到黑线,尝试寻找") search_speed = 150 # 提高搜索速度 if self.no_line_count % 4 == 0: # 优化搜索模式 self.set_speed('turn', search_speed) print("向左寻找") elif self.no_line_count % 4 == 2: self.set_speed('turn', -search_speed) print("向右寻找") else: self.no_line_count = 0 self.line(img, center, cont_cent) # 显示图像 cv2.imshow('line_tracking', img) # 动态帧率处理 processing_time = time.time() - start_time delay = max(1, int(30 - processing_time * 1000)) # 保持约30FPS if cv2.waitKey(delay) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() def is_qr_centered_and_large(self): """检查二维码是否在中心区域且足够大""" if not self.last_qr_position: return False # 获取图像尺寸 height, width = 360, 480 # 假设图像尺寸为480x360 (0.75缩放后) # 中心区域定义 (中心±100像素) center_region_x = (width / 2 - 100, width / 2 + 100) center_region_y = (height / 2 - 100, height / 2 + 100) # 检查二维码中心位置 center_x, center_y = self.last_qr_position in_center = (center_region_x[0] <= center_x <= center_region_x[1] and center_region_y[0] <= center_y <= center_region_y[1]) # 检查二维码大小 large_enough = self.last_qr_size > self.min_qr_size print( f"QR Center: ({center_x:.1f}, {center_y:.1f}), Size: {self.last_qr_size}, In Center: {in_center}, Large Enough: {large_enough}") return in_center and large_enough def get_contour_center(self, contour): m = cv2.moments(contour) if m["m00"] == 0: return [0, 0] x = int(m["m10"] / m["m00"]) y = int(m["m01"] / m["m00"]) return [x, y] def process(self, image): _, thresh = cv2.threshold(image, 100, 255, cv2.THRESH_BINARY_INV) contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) if contours: main_contour = max(contours, key=cv2.contourArea) cv2.drawContours(image, [main_contour], -1, (150, 150, 150), 2) contour_center = self.get_contour_center(main_contour) cv2.circle(image, tuple(contour_center), 2, (150, 150, 150), 2) else: return image, (0, 0) return image, contour_center def slice_out(self, im, num): cont_cent = [] height, width = im.shape[:2] sl = int(height / num) sliced_imgs = [] for i in range(num): part = sl * i crop_img = im[part:part + sl, 0:width] processed = self.process(crop_img) sliced_imgs.append(processed[0]) cont_cent.append(processed[1]) return sliced_imgs, cont_cent def remove_background(self, image, b): if b: lower = np.array([0], dtype="uint8") upper = np.array([80], dtype="uint8") mask = cv2.inRange(image, lower, upper) image = cv2.bitwise_and(image, image, mask=mask) image = cv2.bitwise_not(image, image, mask=mask) image = (255 - image) return image def repack(self, images): im = images[0] for i in range(1, len(images)): im = np.concatenate((im, images[i]), axis=0) return im def calculate_path_angle(self, cont_cent): """计算路径弯曲角度""" valid_points = [c for c in cont_cent if c != (0, 0)] if len(valid_points) < 2: return self.last_angle top_point = valid_points[0] bottom_point = valid_points[-1] dx = bottom_point[0] - top_point[0] dy = (len(valid_points) - 1) * 65 if dy == 0: return self.last_angle angle_rad = math.atan2(dx, dy) angle_deg = math.degrees(angle_rad) self.last_angle = angle_deg return angle_deg def line(self, image, center, cont_cent): cv2.line(image, (0, 65), (480, 65), (30, 30, 30), 1) cv2.line(image, (0, 130), (480, 130), (30, 30, 30), 1) cv2.line(image, (0, 195), (480, 195), (30, 30, 30), 1) cv2.line(image, (240, 0), (240, 260), (30, 30, 30), 1) prev_point = None for i, point in enumerate(cont_cent): if point != (0, 0): y_pos = point[1] + 65 * i cv2.circle(image, (point[0], y_pos), 3, (0, 0, 255), -1) if prev_point is not None: cv2.line(image, prev_point, (point[0], y_pos), (0, 255, 0), 2) prev_point = (point[0], y_pos) path_angle = self.calculate_path_angle(cont_cent) weights = [0.1, 0.2, 0.3, 0.4] valid = [(c[0], w) for c, w in zip(cont_cent, weights) if c != (0, 0)] if not valid: print("未检测到有效中心点") return offset_center = sum((x - 240) * w for x, w in valid) print(f"cont_cent: {cont_cent}") print(f"offset_center: {offset_center:.2f}") print(f"Path angle: {path_angle:.2f} degrees") if self.is_follow: # 角度控制优化 if abs(path_angle) > 15: self.turn_count += 1 if self.turn_count > 2: # 减少等待帧数 # 动态速度 - 角度越大转得越快 turn_speed = min(MAX_TURN_SPEED, 100 + abs(int(path_angle))) actual_speed = self.set_speed('turn', turn_speed if path_angle > 0 else -turn_speed) print(f"转向 {'左' if path_angle > 0 else '右'}, 速度: {abs(actual_speed)}") else: self.set_speed('forward', 150) print("准备转向中...") else: self.turn_count = 0 # 偏移控制优化 - 动态阈值 offset_threshold = max(60, 120 - abs(path_angle) * 2) if abs(offset_center) > offset_threshold: # 动态速度 - 偏移越大移动越快 move_speed = min(MAX_ADJUST_SPEED, 120 + int(abs(offset_center) / 2)) actual_speed = self.set_speed('lateral', move_speed if offset_center > 0 else -move_speed) print(f"{'右移' if offset_center > 0 else '左移'}, 速度: {abs(actual_speed)}") else: # 提高直行速度 actual_speed = self.set_speed('forward', 220) print(f"前进, 速度: {actual_speed}") # 优化二维码识别方法 def decode(self, image): """识别图像中的二维码""" # 重置本次检测状态 self.qr_detected = False # 图像预处理 - 提高识别率 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 增强对比度 gray = cv2.convertScaleAbs(gray, alpha=1.5, beta=20) # 中值滤波去噪 gray = cv2.medianBlur(gray, 3) # 尝试识别二维码 barcodes = pyzbar.decode(gray) for barcode in barcodes: (x, y, w, h) = barcode.rect # 过滤太小的二维码(可能是误识别) if w < 30 or h < 30: continue cv2.rectangle(image, (x, y), (x + w, y + h), (255, 0, 0), 2) try: barcodeData = barcode.data.decode("utf-8") except: continue text = "{}".format(barcodeData) cv2.putText(image, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2) center_x = x + w / 2 center_y = y + h / 2 cv2.circle(image, (int(center_x), int(center_y)), 5, (0, 255, 0), -1) self.qr_detected = True self.last_qr_position = (center_x, center_y) self.last_qr_size = max(w, h) # 记录二维码大小 # 只处理内容为"landed"的二维码 if barcodeData == "landed": self.scan_content = "landed" if not self.adjusting_for_landing: print(f"识别到降落二维码: {text}") self.adjusting_for_landing = True # 简化调整逻辑:只关注二维码位置和大小 # 获取图像尺寸 height, width = image.shape[:2] # 中心区域定义 (中心±100像素) center_region_x = (width / 2 - 100, width / 2 + 100) center_region_y = (height / 2 - 100, height / 2 + 100) # 检查二维码中心位置 in_center = (center_region_x[0] <= center_x <= center_region_x[1] and center_region_y[0] <= center_y <= center_region_y[1]) # 调整无人机位置 if not in_center: # 水平调整 - 提高速度并添加比例控制 h_offset = abs(center_x - width/2) h_speed = min(MAX_ADJUST_SPEED, 40 + int(h_offset / 4)) if center_x < width / 2: self.airplanceApi.move_left(h_speed) print(f"向左移动, 速度: {h_speed}") else: self.airplanceApi.move_right(h_speed) print(f"向右移动, 速度: {h_speed}") # 垂直调整 - 提高速度 v_speed = 40 if center_y < height / 2: self.airplanceApi.move_up(v_speed) print(f"向上移动, 速度: {v_speed}") else: self.airplanceApi.move_down(v_speed) print(f"向下移动, 速度: {v_speed}") else: # 当在中心区域时,停止水平移动 self.airplanceApi.move_left(0) self.airplanceApi.move_right(0) # 如果二维码不够大,下降 if self.last_qr_size < self.min_qr_size: self.airplanceApi.move_down(40) print(f"向下移动靠近二维码, 速度: 40") else: self.airplanceApi.move_down(0) print("二维码大小合适") # 强制降落条件:当二维码在中心且足够大时 if in_center and self.last_qr_size > self.min_qr_size: print("二维码在中心且足够大,准备降落") break # 一次只处理一个二维码 if __name__ == '__main__': line_follower = LineFollower() 帮我看看这个代码怎么样
07-08
内容概要:本文详细介绍了“秒杀商城”微服务架构的设计与实战全过程,涵盖系统从需求分析、服务拆分、技术选型到核心功能开发、分布式事务处理、容器化部署及监控链路追踪的完整流程。重点解决了高并发场景下的超卖问题,采用Redis预减库存、消息队列削峰、数据库乐观锁等手段保障数据一致性,并通过Nacos实现服务注册发现与配置管理,利用Seata处理跨服务分布式事务,结合RabbitMQ实现异步下单,提升系统吞吐能力。同时,项目支持Docker Compose快速部署和Kubernetes生产级编排,集成Sleuth+Zipkin链路追踪与Prometheus+Grafana监控体系,构建可观测性强的微服务系统。; 适合人群:具备Java基础和Spring Boot开发经验,熟悉微服务基本概念的中高级研发人员,尤其是希望深入理解高并发系统设计、分布式事务、服务治理等核心技术的开发者;适合工作2-5年、有志于转型微服务或提升架构能力的工程师; 使用场景及目标:①学习如何基于Spring Cloud Alibaba构建完整的微服务项目;②掌握秒杀场景下高并发、超卖控制、异步化、削峰填谷等关键技术方案;③实践分布式事务(Seata)、服务熔断降级、链路追踪、统一配置中心等企业级中间件的应用;④完成从本地开发到容器化部署的全流程落地; 阅读建议:建议按照文档提供的七个阶段循序渐进地动手实践,重点关注秒杀流程设计、服务间通信机制、分布式事务实现和系统性能优化部分,结合代码调试与监控工具深入理解各组件协作原理,真正掌握高并发微服务系统的构建能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值