首页 > 其他分享 >[lnsyoj118/luoguP3369]普通平衡树

[lnsyoj118/luoguP3369]普通平衡树

时间:2024-06-13 21:43:17浏览次数:13  
标签:lnsyoj118 return int tr key luoguP3369 平衡 节点 size

题意

维护一个数据结构,要求支持插入,删除,根据排名查数,根据数查排名,查询前驱,查询后继\(6\)个操作

sol

考虑到后四个查询的操作,会发现使用二叉搜索树(BST)完全可以实现
为了完成这四个操作,需要在每个节点记录\(3\)个值:

  1. \(key\) 表示当前节点的数
  2. \(cnt\) 表示当前节点的数的个数(为了防止出现同一数字出现多次)
  3. \(size\) 表示当前子树的数的个数(为了方便查询排名)

根据排名查数

当处于节点\(u\)时,设当前需要查询的排名为\(rank\),如果此时节点\(u\)为空节点,说明不存在该数,否则分情况讨论:

  1. 如果\(u.lson.size \ge rank\),说明此时要查询的数一定位于\(u\)的左子树,因此答案为左子树中排名为\(rank\)的数
  2. 如果\(u.lson.size + u.cnt \ge rank\),说明此时要查询的数为\(u.key\),因此答案就为\(u.key\)
  3. 前两条均不满足,则说明此时要查询的数一定位于\(u\)的右子树,又由于需要去除掉左子树和\(u\)的所有数,因此答案为右子树中排名为\(rank - u.lson.size - u.cnt\)的数

代码

int get_key(int u, int rank){
    if (!u) return INF;
    if (rank <= tr[tr[u].l].size) return get_key(tr[u].l, rank);
    if (rank <= tr[tr[u].l].size + tr[u].cnt) return tr[u].key;
    return get_key(tr[u].r, rank - tr[tr[u].l].size - tr[u].cnt);
}

根据数查排名

当处于节点\(u\)时,设当前需要查询的数为\(x\),如果此时节点\(u\)为空节点,说明不存在该数,否则分情况讨论:

  1. 如果\(x < u.key\),说明此时要查询的数一定位于\(u\)的左子树,因此答案为左子树中数\(x\)的排名
  2. 如果\(x = u.key\),说明此时要查询的数为\(u.key\),因此答案为\(u.lson.size + 1\)
  3. 前两条均不满足,则说明此时要查询的数一定位于\(u\)的右子树,又由于需要加上左子树和\(u\)的所有数,因此答案为右子树中数\(x\)的排名\(+u.lson.size + u.cnt\)

代码

int get_rank(int u, int key){
    if (!u) return 0;
    if (key < tr[u].key) return get_rank(tr[u].l, key);
    if (key == tr[u].key) return tr[tr[u].l].size + 1;
    return tr[tr[u].l].size + tr[u].cnt + get_rank(tr[u].r, key);
}

需要注意的是,部分时候为了方便,我们会在BST中加入两个哨兵节点\(-\infty\)与\(+\infty\),此时由于\(-\infty\)的存在,根据排名查数时的\(rank\)需要\(+1\),而根据数查排名时的查得的答案需要\(-1\)

查询前驱

当处于节点\(u\)时,设当前需要查询的数为\(x\),如果此时节点\(u\)为空节点,说明不存在该数,否则分情况讨论:

  1. 若\(x \le u.key\),说明此时要查询的数一定不位于\(u\)的右子树,因此答案为左子树中数\(x\)的前驱
  2. 若\(x > u.key\),说明此时要查询的数可能为\(u.key\),也可能位于\(u\)的右子树,因此答案为右子树中数\(x\)的前驱与\(u.key\)中的最大值

代码

int get_prev(int u, int key){
    if (!u) return -INF;
    if (tr[u].key >= key) return get_prev(tr[u].l, key);
    return max(tr[u].key, get_prev(tr[u].r, key));
}

查询后继

当处于节点\(u\)时,设当前需要查询的数为\(x\),如果此时节点\(u\)为空节点,说明不存在该数,否则分情况讨论:

  1. 若\(x \ge u.key\),说明此时要查询的数一定不位于\(u\)的左子树,因此答案为右子树中数\(x\)的前驱
  2. 若\(x < u.key\),说明此时要查询的数可能为\(u.key\),也可能位于\(u\)的左子树,因此答案为左子树中数\(x\)的前驱与\(u.key\)中的最小值

代码

int get_next(int u, int key){
    if (!u) return INF;
    if (tr[u].key <= key) return get_next(tr[u].r, key);
    return min(tr[u].key, get_next(tr[u].l, key));
}

这样一来,这四个查询操作及两个修改操作的复杂度为\(O(h)\),\(h\)为BST高度。在随机数据下,\(h\)趋向于\(\log n\),但由于BST容易被卡的优秀性质,只需递增/递减数据就可以将BST卡成一条链,从而使\(h=n\),因此,我们需要一些手段来使BST的\(h\)无论何时都接近于\(\log n\),平衡树应运而生

旋转

BST有一条很好的性质:容易被卡中序遍历是单调递增的,反过来也成立,如果我们可以通过一些操作,使中序遍历不变,那么这棵树仍是本质相同的BST,而这个能够使中序遍历不变的操作即为旋转,旋转是几乎所有平衡树都需要使用的操作(部分除外,如FHQ-Treap
image
两图中序遍历都为\(A,Q,B,P,C\)
在执行\(zig\)操作时,需要进行三次改变:\(p.lson \to q.rson(B), q.rson \to p, p \to q\)
同理,在执行\(zag\)操作时,也需要进行三次改变:\(q.rson \to p.lson(B), p.lson \to q, q \to p\)
代码

void zig(int &u){
    int q = tr[u].l;
    tr[u].l = tr[q].r, tr[q].r = u, u = q;
}

void zag(int &u){
    int q = tr[u].r;
    tr[u].r = tr[q].l, tr[q].l = u, u = q;
}

需要注意的是,这里的\(u\)指代的是根节点或某个节点的子节点,当执行\(zig\)或\(zag\)时,所对应的节点也要改变,因此需要在函数中传递引用。旋转操作可以视为是BST上三条边所指节点的交换操作

Treap

Treap是OI中较常用的一种平衡树
Treap是Tree和Heap的结合体,它的原理非常简单粗暴:既然BST在随机数据下趋于\(\log n\),那么我们就把所有数据打乱顺序再插入就好了。显然,在\(99.99\%\)的情况之下,这种方法都是有效的。不过因为大多数平衡树解决的问题都是在线问题,因此我们无法简单地将数据打乱。
Treap给出的解决方案是这样的:对于每一个节点,在插入时赋予它一个随机权值\(val\),由于可以通过\(zig\)和\(zag\)操作将BST的任一一对父子节点交换而不改变BST的本质,因此我们可以参考二叉堆,插入到对应位置后再向上调整,直到BST中的\(val\)仍然满足二叉堆的性质
对于插入操作,我们先将一个节点插入BST中,然后从下往上判断它是否需要调序;而对于删除操作,我们在BST中找到该节点后,为了方便操作,我们将该节点先调整到叶子结点上,再进行删除。具体代码见下:

void insert(int &u, int key){
    if (!u) u = create(key); // 没有该节点的话,就创建一个新节点
    else if (key == tr[u].key) tr[u].cnt ++ ; // 否则直接在节点上添加标记
    else if (key < tr[u].key){
        insert(tr[u].l, key);
        if (tr[tr[u].l].val < tr[u].val) zig(u); // 向上调序
    }
    else {
        insert(tr[u].r, key);
        if (tr[tr[u].r].val < tr[u].val) zag(u); // 向上调序
    }
}

void erase(int &u, int key){
    if (!u) return ; // 没有该节点的话,无需处理
    else if (key == tr[u].key){
        if (tr[u].cnt > 1) tr[u].cnt -- ; // 如果存在多个标记,直接删除标记
        else if (tr[u].l || tr[u].r){
            if (!tr[u].r || tr[tr[u].l].val > tr[tr[u].r].val){
                zig(u); // 先向下调序
                erase(tr[u].r, key);
            }
            else{
                zag(u); // 先向下调序
                erase(tr[u].l, key);
            }
        }
        else u = 0; // 调到叶子节点后直接删除
    }
    else if (key < tr[u].key) erase(tr[u].l, key);
    else erase(tr[u].r, key);
}

需要注意的是,本题的\(size\)是会在旋转、插入、删除操作中随时改变的,类比线段树,我们还需要一个方法来根据子结点的数据反推节点的\(size\),即PUSHUP
代码:

void pushup(int u){
    tr[u].size = tr[tr[u].l].size + tr[tr[u].r].size + tr[u].cnt;
}

这样的话,我们就通过精巧的操作使BST基本平衡,平均时间复杂度也随之下降为\(O(n \log n)\),不过值得注意的是,其最坏复杂度仍为\(O(n^2)\),只是如果真的卡出来了,概率堪比十连十金

代码

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdlib>

using namespace std;

const int N = 100005, INF = 0x3f3f3f3f;

struct Node{
    int l, r;
    int key, val;
    int cnt, size;
}tr[N];

int root, idx;
int n;

int create(int key){
    tr[ ++ idx].key = key;
    tr[idx].val = rand();
    tr[idx].cnt = tr[idx].size = 1;
    return idx;
}

void pushup(int u){
    tr[u].size = tr[tr[u].l].size + tr[tr[u].r].size + tr[u].cnt;
}

void zig(int &u){
    int q = tr[u].l;
    tr[u].l = tr[q].r, tr[q].r = u, u = q;
    pushup(tr[u].r);
}

void zag(int &u){
    int q = tr[u].r;
    tr[u].r = tr[q].l, tr[q].l = u, u = q;
    pushup(tr[u].l);
}

void build(){
    create(-INF), create(INF);
    root = 1, tr[1].r = 2;
    pushup(root);
}

void insert(int &u, int key){
    if (!u) u = create(key);
    else if (key == tr[u].key) tr[u].cnt ++ ;
    else if (key < tr[u].key){
        insert(tr[u].l, key);
        if (tr[tr[u].l].val < tr[u].val) zig(u);
    }
    else {
        insert(tr[u].r, key);
        if (tr[tr[u].r].val < tr[u].val) zag(u);
    }
    pushup(u);
}

void erase(int &u, int key){
    if (!u) return ;
    else if (key == tr[u].key){
        if (tr[u].cnt > 1) tr[u].cnt -- ;
        else if (tr[u].l || tr[u].r){
            if (!tr[u].r || tr[tr[u].l].val > tr[tr[u].r].val){
                zig(u);
                erase(tr[u].r, key);
            }
            else{
                zag(u);
                erase(tr[u].l, key);
            }
        }
        else u = 0;
    }
    else if (key < tr[u].key) erase(tr[u].l, key);
    else erase(tr[u].r, key);
    
    pushup(u);
}

int get_rank(int u, int key){
    if (!u) return 0;
    if (key < tr[u].key) return get_rank(tr[u].l, key);
    if (key == tr[u].key) return tr[tr[u].l].size + 1;
    return tr[tr[u].l].size + tr[u].cnt + get_rank(tr[u].r, key);
}

int get_key(int u, int rank){
    if (!u) return INF;
    if (rank <= tr[tr[u].l].size) return get_key(tr[u].l, rank);
    if (rank <= tr[tr[u].l].size + tr[u].cnt) return tr[u].key;
    return get_key(tr[u].r, rank - tr[tr[u].l].size - tr[u].cnt);
}

int get_prev(int u, int key){
    if (!u) return -INF;
    if (tr[u].key >= key) return get_prev(tr[u].l, key);
    return max(tr[u].key, get_prev(tr[u].r, key));
}

int get_next(int u, int key){
    if (!u) return INF;
    if (tr[u].key <= key) return get_next(tr[u].r, key);
    return min(tr[u].key, get_next(tr[u].l, key));
}

int main(){
    scanf("%d", &n);
    build();
    while (n -- ){
        int op, x;
        scanf("%d%d", &op, &x);
        switch(op){
            case 1: insert(root, x); break;
            case 2: erase(root, x); break;
            case 3: printf("%d\n", get_rank(root, x) - 1); break;
            case 4: printf("%d\n", get_key(root, x + 1)); break;
            case 5: printf("%d\n", get_prev(root, x)); break;
            case 6: printf("%d\n", get_next(root, x)); break;
            default: break;
        }
    }
    return 0;
}

蒟蒻犯的若至错误

  • \(zig\)和\(zag\)的时候没有PUSHUP导致整颗BST的\(size\)都计算错误

标签:lnsyoj118,return,int,tr,key,luoguP3369,平衡,节点,size
From: https://www.cnblogs.com/XiaoJuRuoUP/p/18244891/lnsyoj118_luoguP3369

相关文章

  • 平衡树
    1.splay普通权值平衡树【模板】普通平衡树二叉查找树:又:二叉搜索树、二叉排序树。它或者是一棵空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;它的左、右子树也分别......
  • C语言二叉平衡搜索树
    AVL(二叉平衡搜索树)的概念和思路任意一个节点左子树高度-右子树高度<=1要想让BST保持平衡,必须在每一次插入、删除之后,检查是否其左右子树满足平衡的定义如果不满足,就做“旋转”操作,使其恢复平衡加入以上平衡策略算法后的BST,称为AVL,AVL是一种绝对平衡的二叉树#include......
  • Day17| 110.平衡二叉树、 257. 二叉树的所有路径 、 404.左叶子之和
    110.平衡二叉树(优先掌握递归)再一次涉及到,什么是高度,什么是深度,可以巩固一下。题目链接/文章讲解/视频讲解:https://programmercarl.com/0110.平衡二叉树.html#Definitionforabinarytreenode.#classTreeNode:#def__init__(self,val=0,left=None,right=None):......
  • 【YOLOv8改进】SlideLoss损失函数,解决样本不平衡问题
    YOLO目标检测创新改进与实战案例专栏专栏目录:YOLO有效改进系列及项目实战目录包含卷积,主干注意力,检测头等创新机制以及各种目标检测分割项目实战案例专栏链接:YOLO基础解析+创新改进+实战案例介绍摘要近年来,基于深度学习的人脸检测算法取得了很大进展。这些......
  • AI与学术写作:如何平衡便利性与原创性
    告诉大家一个非常残忍的答案,以后所有论文都会被查ai的。在考虑使用AI撰写学术论文的便捷性时,你可能会问:学术界难道没有预见到这种行为吗?答案是肯定的。学术界不仅关注传统的抄袭问题,还针对AI生成内容(AIGC)增加了一项名为“AIGC检测”的指标。这一检测的目的,正是为了识别和惩处......
  • SMOGN算法Python实现:解决回归分析中的数据不平衡
      本文介绍基于Python语言中的smogn包,读取.csv格式的Excel表格文件,实现SMOGN算法,对机器学习、深度学习回归中,训练数据集不平衡的情况加以解决的具体方法。  在不平衡回归问题中,样本数量的不均衡性可能导致模型在预测较少类别的样本时表现较差;为了解决这个问题,可以使用SMOTE(Sy......
  • 聊聊 普通平衡树 的 几种做法
    权值线段树#include<bits/stdc++.h>usingnamespacestd;constintMAXN=5e5+10;intn,m,a[MAXN],x,y,op;//Segmenttree#definel(x)tree[x].ls#definer(x)tree[x].rs#definesum(x)tree[x].sumintSegmentsum;structSegmentTree{ intl,r,len,ls,rs,sum,......
  • 隐私计算在智能城市建设中的应用:平衡公共安全与个人隐私
    PrimiHub一款由密码学专家团队打造的开源隐私计算平台,专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。随着智能城市建设的快速推进,各种数据采集技术和设备在城市管理中的应用越来越广泛。这些技术和设备在提升城市管理效率、优化资源分配和提高公......
  • 大摩“凑热闹”:当前氧化铝紧平衡,任何供给冲击都将导致价格急剧波动
    大摩认为,全球有590万吨氧化铝供应受阻,相等于扣除中国后全球供应约一成,而氧化铝需求可能有所上升,同时氧化铝库存缓冲有限,因此任何供给冲击都将导致价格急剧波动。文章内容今年以来氧化铝期货价格一路上行,氧化铝主力合约AO2407从年初的3000元/吨飙升至4000多/吨,五个月涨幅超30......
  • (二刷)代码随想录第17天|● 110.平衡二叉树 ● 257. 二叉树的所有路径 ● 404.左叶子之
    110.平衡二叉树math.abs指的是绝对值;这棵树的左右子树的高度差小于1的时候,同时该树的左右子树都是平衡二叉树的时候,这棵树才是平衡二叉树;classSolution{publicbooleanisBalanced(TreeNoderoot){returngetHeight(root)!=-1;}privateint......