OJ中常用平衡树,Treap树堆详解

文章目录

    • Treap定义
    • Treap的可行性
    • Treap的构建
      • 节点定义
      • 旋转
        • 左单旋
        • 右单旋
        • 旋转的代码实现
      • 插入
        • 插入的代码实现
      • 删除
      • 遍历
      • 查找
      • Treap对权值的扩展
      • Treap对size的扩展
        • 扩展size域后的节点定义和旋转,插入,删除操作
        • 查询第k小的元素
        • 求元素的排名
      • 查询后继、前驱
      • Treap类的代码实现
      • Treap的特点
    • 无旋式Treap
      • 无旋式Treap 定义
      • 无旋式Treap 的特点
      • 无旋式Treap 的节点定义
      • 无旋式Treap 的操作
        • 分裂(split)
        • 合并(merge)
        • 插入(insert)
        • 删除(erase)

Treap定义

T r e a p = T r e e + H e a p Treap = Tree + Heap Treap=Tree+Heap

顾名思义,Treap其实就是树和堆的结合,其本质是一种平衡树。

我们最基础的BST在插入有序或接近有序的数据时,会退化成单链的结构,所以我们基于BST有着很多优化方案如AVL树引入平衡因子,红黑树引入颜色等等。

而我们的Treap引入堆其实就是为了维护平衡。其维护平衡的方法就是在BST的基础上加入修正值,修正值满足最小堆的性质(也可以是最大堆,本文基于最小堆实现),即根的修正值小于其子树的修正值。

Treap其实就是满足如下性质的二叉树:

  1. 如果左子树非空,那么左子树节点的值都小于根节点的值,并且根节点的修正值都小于左子树节点的修正值
  2. 如果右子树非空,那么右子树节点的值都小于根节点的值,并且根节点的修正值都小于右子树节点的修正值
  3. 它的左右子树也都是一棵Treap

下图例为一棵Treap

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

修正值的获取方法其实是一个随即生成的数字,没有规律。

Treap的可行性

前面提到了BST退化为单链是因为插入有序或者接近有序的数据,但我们修正值是一个随机数,获得有序随机数的概率很小,所以我们的Treap是趋于平衡并不是AVL树或者红黑树那种严格平衡,

Treap的构建

节点定义

struct TreapNode
{TreapNode *_left, *_right;//节点左右孩子指针int val, fix;//节点值以及修正值//int _weight;
};

旋转

我们常用的几种平衡树像是AVL树,RB树,为了维护其平衡性都要用到旋转操作,Treap也一样,但是Treap的只用到了左单旋和右单旋,调整比较简单,没有AVL树或是RB树那样繁琐的双旋。

我们记根节点为root,其左右孩子分别为SubL,SubR,左右孩子的左右孩子记为SubLL/SubLR和SubRL/SubRR

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

左单旋

左单旋一棵子树:SubRL作为root的右孩子,root作为SubR的左孩子
在这里插入图片描述

右单旋

右单旋一棵子树:SubLR作为root的左孩子,root作为SubL的右孩子

在这里插入图片描述

旋转操作的本质就是在不改变这棵树的中序遍历的前提下让这颗树的层数可能减小,不改变中序遍历自然维护了BST的性质,不过我们Treap中旋转还可以用来维护修正值的堆序。如:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

旋转后既维护了BST的性质又修正了修正值的堆序。

旋转的代码实现
void RotateL(TreapNode *&root)//左单旋
{TreapNode *SubR = root->_right;root->_right = SubR->_left;SubR->_left = root;root = SubR;
}
void RotateR(TreapNode *&root)//右单旋
{TreapNode *SubL = root->_left;root->_left = SubL->_right;SubL->_right = root;root = SubL;
}

插入

Treap的插入也BST相似(这里的插入暂时允许重复元素插入)

找到插入位置,建立新的节点,如果修正值异常那么就旋转维护

插入过程如下:

  1. 从根节点开始遍历
  2. 如果插入值小于当前节点值,那么在左子树插入,插入完成后,如果当前节点修正值大于左孩子修正值,那么右单旋
  3. 如果插入值大于当前节点值,那么在右子树插入,插入完成后,如果当前节点修正值大于右孩子修正值,那么左单旋
  4. 当前节点为空,那么找到了插入位置,在该位置进行插入

以下图为例

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

插入的代码实现
void insert(TreapNode *&cur, int val)
{if (!cur){cur = new TreapNode(val, rand());}else if (cur->val > val){insert(cur->_left, val);if (cur->fix > cur->_left->fix) // 左孩子修正值小于当前节点修正值,右旋当前节点RotateR(cur);}else{insert(cur->_right, val);if (cur->fix > cur->_right->fix) // 右孩子修正值小于当前节点修正值,左旋当前节点RotateL(cur);}
}

删除

Treap中删除元素相对于AVL树和RB树简直是简单爆了,一共只有三种情况(其实是成两种)

情况一:被删除节点是叶子节点

没有孩子没有牵挂,生不带来死不带去,直接删除即可

情况二:被删除节点有一个孩子节点非空

子承父业,用孩子节点代替当前节点

情况三:两个孩子节点都非空

不同于BST的替代删除法,我们是通过旋转将待删除结点调整到叶子节点或者只有一个孩子节点非空的位置,然后按照情况一二进行删除。

旋转调整的过程类似于堆调整算法中的向下调整(AdjustDown)算法

如果左孩子修正值小于右孩子修正值,那么右单旋,否则左单旋。如此往下调整直到调整为情况一二。

我们以下图删除结点15为例

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们删除的期望时间复杂度为O(logN)

代码实现:

void erase(TreapNode *&root, int val)
{if (root->val > val){erase(root->_left, val);}else if (root->val < val){erase(root->_right, val);}else // 找到待删除节点{if (!root->_right || !root->_left){ // 情况一二,该节点可以直接被删除TreapNode *tmp = root;if (!root->_right)root = root->_left; // 用左子节点代替它elseroot = root->_right; // 用右子节点代替它delete tmp;              // 删除该节点}else{ // 情况三if (root->_left->fix < root->_right->fix){ // 左子节点修正值较小,右旋RotateR(root);erase(root->_right, val);}else{ // 左子节点修正值较小,左旋RotateL(root);erase(root->_left, val);}}}
}

遍历

和普通的二叉树遍历方式一样

    void _InOrder(TreapNode *root){if (!root)return;_InOrder(root->_left);cout << root->val << ":" << root->fix << " ";_InOrder(root->_right);}void _PrevOrder(TreapNode *root){if (!root)return;cout << root->val << ":" << root->fix << " ";_PrevOrder(root->_left);_PrevOrder(root->_right);}

查找

和BST一样

    bool Find(int val){TreapNode *cur = p;while (cur){if (cur->val > val)cur = cur->_left;else if (cur->val < val)cur = cur->_right;elsereturn true;}return false;}

Treap对权值的扩展

我们的Treap其实还存在一个问题,那就是它允许了重复元素的插入,对此我们可以再进一步的优化,那就是把重复的节点合并,即在节点定义里面增加一个权域weight,它存储的是当前值节点的数目

这样我们插入时如果当前值已存在,那么把对应节点的weight+1即可,相应的,删除时只要把对应节点的weight-1即可,只有当weight为0才真正删除。

虽然增加了节点的大小,但是这样避免了一定程度的旋转,是一笔划算的买卖。

扩展了weight域后的节点定义和插入删除操作

struct TreapNode
{TreapNode(TreapNode *l, TreapNode *r, int v, int f) : _left(l), _right(r), val(v), fix(f), w(1){}TreapNode *_left, *_right;int val, fix, w;
};void insert(TreapNode *&cur, int val){if (!cur){cur = new TreapNode(nullptr, nullptr, val, rand());}else if (cur->val > val){insert(cur->_left, val);if (cur->fix > cur->_left->fix) // 左孩子修正值小于当前节点修正值,右旋当前节点RotateR(cur);}else if (cur->val < val){insert(cur->_right, val);if (cur->fix > cur->_right->fix) // 右孩子修正值小于当前节点修正值,左旋当前节点RotateL(cur);}else//已经存在,w++{cur->w++;}}void erase(TreapNode *&root, int val){if (root->val > val){erase(root->_left, val);}else if (root->val < val){erase(root->_right, val);}else // 找到待删除节点{if (!(--root->w))//如果w为0,在真正删除{if (!root->_right || !root->_left){ // 情况一二,该节点可以直接被删除TreapNode *tmp = root;if (!root->_right)root = root->_left; // 用左子节点代替它elseroot = root->_right; // 用右子节点代替它delete tmp;              // 删除该节点}else{ // 情况三if (root->_left->fix < root->_right->fix){ // 左子节点修正值较小,右旋RotateR(root);erase(root->_right, val);}else{ // 左子节点修正值较小,左旋RotateL(root);erase(root->_left, val);}}}}}

Treap对size的扩展

除了对权域weight的扩展外,我们还可以再增加一个size域,size存储的是以当前节点为根的子树的大小。(我们称一棵子树中节点的数目为子树的大小)

为什么要记录当前子树大小呢?

这是为了满足对于第k大元素的查询。

在root所在子树里面找第k大节点,可以根据左右子树的size转化为在左/右子树中找第j大节点

增加了域自然要在相应的操作中对域进行维护。这里涉及三种操作旋转、插入、删除。

旋转:在旋转后对子节点和根节点分别重新计算其子树大小。

插入:新插入节点的size自然是1,对于插入路径上的每一个节点相应的size+1即可。

删除:对于待删除节点,对于删除路径上的每一个节点相应的size-1即可。

对于插入和删除的size维护都比较无脑,主要是保证旋转的维护逻辑无误。

扩展size域后的节点定义和旋转,插入,删除操作
struct TreapNode
{TreapNode(TreapNode *l, TreapNode *r, int v, int f) : _left(l), _right(r), val(v), fix(f), w(1), size(1){}int lsize()//左子树size{return _left ? _left->size : 0;}int rsize()//右子树size{return _right ? _right->size : 0;}TreapNode *_left, *_right;int val, fix, w, size;
};void erase(TreapNode *&root, int val){if (root->val > val){erase(root->_left, val);root->size--;}else if (root->val < val){erase(root->_right, val);root->size--;}else // 找到待删除节点{if (!(--root->w)){if (!root->_right || !root->_left){ // 情况一二,该节点可以直接被删除TreapNode *tmp = root;if (!root->_right)root = root->_left; // 用左子节点代替它elseroot = root->_right; // 用右子节点代替它delete tmp;              // 删除该节点}else{ // 情况三if (root->_left->fix < root->_right->fix){ // 左子节点修正值较小,右旋RotateR(root);erase(root->_right, val);}else{ // 左子节点修正值较小,左旋RotateL(root);erase(root->_left, val);}}}elseroot->size--;}}void RotateL(TreapNode *&root){TreapNode *SubR = root->_right;root->_right = SubR->_left;SubR->_left = root;root = SubR;// 此时root为新根,显然要先调节子节点,再调节rootSubR = root->_left; // 命名有点迷惑性了,但是节省空间就这样了SubR->size = SubR->lsize() + SubR->rsize() + SubR->w;root->size = root->lsize() + root->rsize() + root->w;}void RotateR(TreapNode *&root){TreapNode *SubL = root->_left;root->_left = SubL->_right;SubL->_right = root;root = SubL;// 此时root为新根,显然要先调节子节点,再调节rootSubL = root->_right; // 命名有点迷惑性了,但是节省空间就这样了SubL->size = SubL->lsize() + SubL->rsize() + SubL->w;root->size = root->lsize() + root->rsize() + root->w;}void insert(TreapNode *&cur, int val){if (!cur){cur = new TreapNode(nullptr, nullptr, val, rand());}else if (cur->val > val){insert(cur->_left, val);cur->size++;if (cur->fix > cur->_left->fix) // 左孩子修正值小于当前节点修正值,右旋当前节点RotateR(cur);}else if (cur->val < val){insert(cur->_right, val);cur->size++;if (cur->fix > cur->_right->fix) // 右孩子修正值小于当前节点修正值,左旋当前节点RotateL(cur);}else{cur->size++;cur->w++;}}
查询第k小的元素

有了size域之后,我们访问第k小元素就很方便了,其实就是一个简单的分治问题。

对于一个节点root,它在当前子树内的排名显然是[lsize + 1 , lsize + w]
当k在区间左侧,说明要到左子树去找第k小元素
当k在区间右侧,说明要到右子树去找第k - lsize - w小元素
否则当前节点对应的值就是第k小元素

这里也能看出来我们对于weight域的扩展其实是很有必要的~

  1. 从节点root开始访问、
  2. 如果root.lsize + 1<= k <= root.lsize + w,则返回当前节点对应值
  3. 如果k < root.lsize + 1,则分治到左子树查找第k小
  4. 如果k > root.lsize + w,则分治到右子树查找第k - root.lsize - w小

代码如下:

    T kth(TreapNode *root, int k){if (!root){assert(false);}if (k < root->lsize() + 1){return kth(root->_left, k);}else if (k > root->lsize() + root->w){return kth(root->_right, k - root->lsize() - root->w);}else{return root->val;}}
求元素的排名

既然能够查询第k小的元素,自然能够查询元素的排名,同样是利用分治的思想。

对于一个元素val,它的元素排名的贡献来自于值比它小的节点个数,而对于这个个数我们是可以在查找的路径上实现的。

过程为:

  1. 当前访问节点为root,路径前面的节点对于排名的贡献总和为prev,初始从根节点开始查询,prev初始为0

  2. 如果root的值为val,那么它的排名区间为[lsize + prev + 1 , lsize + prev + w] (说是区间是因为val的节点有w个)

  3. 如果root的值大于val,那么就分治到左子树,prev向下传递

  4. 如果root的值小于val,那么就分治到右子树,prev + lsize + w向下传递

代码如下:

    int getKth(TreapNode *root, const T &val, int prev){if (!root){assert(false);}if (root->val > val){return getKth(root->_left, val, prev);}else if (root->val < val){return getKth(root->_right, val, prev + root->lsize() + root->w);}else{return root->lsize() + prev + 1;}}

查询后继、前驱

很常规的一个操作,查前驱就一直记录小于val的节点,同时访问右孩子

查后继就一直记录大于val的节点,同时访问左孩子

遇到空就return

    TreapNode *ans;void prev(const T &val){prev(p, val);}void sub(const T &val){sub(p, val);}void prev(TreapNode *root, const T &val){if (!root)return;if (root->val < val)ans = root, prev(root->_right, val);elseprev(root->_left, val);}void sub(TreapNode *root, const T &val){if (!root)return;if (root->val > val)ans = root, sub(root->_left, val);elsesub(root->_right, val);}

Treap类的代码实现

这里给出Treap的完整代码

template <class T>
struct TreapNode
{TreapNode(const T &v, int f) : _left(nullptr), _right(nullptr), val(v), fix(f), w(1), size(1){}int lsize(){return _left ? _left->size : 0;}int rsize(){return _right ? _right->size : 0;}TreapNode *_left, *_right;T val;int fix, w, size;
};
template <class T>
class Treap
{
private:typedef TreapNode<T> TreapNode;TreapNode *p = nullptr;public:void insert(const T &val){insert(p, val);}bool Find(const T &val){TreapNode *cur = p;while (cur){if (cur->val > val)cur = cur->_left;else if (cur->val < val)cur = cur->_right;elsereturn true;}return false;}void erase(const T &val){erase(p, val);}void InOrder(){_InOrder(p);}void PrevOrder(){_PrevOrder(p);}T kth(int k){return kth(p, k);}int getKth(const T &val){return getKth(p, val, 0);}private:int getKth(TreapNode *root, const T &val, int prev){if (!root){assert(false);}if (root->val > val){return getKth(root->_left, val, prev);}else if (root->val < val){return getKth(root->_right, val, prev + root->lsize() + root->w);}else{return root->lsize() + prev + 1;}}T kth(TreapNode *root, int k){if (!root){assert(false);}if (k < root->lsize() + 1){return kth(root->_left, k);}else if (k > root->lsize() + root->w){return kth(root->_right, k - root->lsize() - root->w);}else{return root->val;}}void erase(TreapNode *&root, const T &val){if (root->val > val){erase(root->_left, val);root->size--;}else if (root->val < val){erase(root->_right, val);root->size--;}else // 找到待删除节点{if (!(--root->w)){if (!root->_right || !root->_left){ // 情况一二,该节点可以直接被删除TreapNode *tmp = root;if (!root->_right)root = root->_left; // 用左子节点代替它elseroot = root->_right; // 用右子节点代替它delete tmp;              // 删除该节点}else{ // 情况三if (root->_left->fix < root->_right->fix){ // 左子节点修正值较小,右旋RotateR(root);erase(root->_right, val);}else{ // 左子节点修正值较小,左旋RotateL(root);erase(root->_left, val);}}}elseroot->size--;}}void _InOrder(TreapNode *root){if (!root)return;_InOrder(root->_left);cout << root->val << ":" << root->fix << ":" << root->size << " ";_InOrder(root->_right);}void _PrevOrder(TreapNode *root){if (!root)return;cout << root->val << ":" << root->fix << ":" << root->size << " ";_PrevOrder(root->_left);_PrevOrder(root->_right);}void RotateL(TreapNode *&root){TreapNode *SubR = root->_right;root->_right = SubR->_left;SubR->_left = root;root = SubR;// 此时root为新根,显然要先调节子节点,再调节rootSubR = root->_left; // 命名有点迷惑性了,但是节省空间就这样了SubR->size = SubR->lsize() + SubR->rsize() + SubR->w;root->size = root->lsize() + root->rsize() + root->w;}void RotateR(TreapNode *&root){TreapNode *SubL = root->_left;root->_left = SubL->_right;SubL->_right = root;root = SubL;// 此时root为新根,显然要先调节子节点,再调节rootSubL = root->_right; // 命名有点迷惑性了,但是节省空间就这样了SubL->size = SubL->lsize() + SubL->rsize() + SubL->w;root->size = root->lsize() + root->rsize() + root->w;}void insert(TreapNode *&cur, const T &val){if (!cur){cur = new TreapNode(val, rand());}else if (cur->val > val){insert(cur->_left, val);cur->size++;if (cur->fix > cur->_left->fix) // 左孩子修正值小于当前节点修正值,右旋当前节点RotateR(cur);}else if (cur->val < val){insert(cur->_right, val);cur->size++;if (cur->fix > cur->_right->fix) // 右孩子修正值小于当前节点修正值,左旋当前节点RotateL(cur);}else{cur->size++;cur->w++;}}
};

Treap的特点

  • 期望深度为O(logn)有着严格的数学证明
  • 实现简单,实际OJ中的代码编写十分简洁。
  • 原理简单,只有两种单旋,而且只需维护两种基础数据结构的规则
  • 稳定性佳,虽然不是严格平衡,但是其简单的代码加上期望平衡性能够很好的应用于算法题目中

无旋式Treap

无旋式Treap 定义

无旋式Treap,即无旋转操作的Treap,不同于我们其他的平衡树,他独有的调整操作是:分裂、合并。也正是其操作方式的特性使其具有支持维护序列和可持久化等特性。因此我们可以用封装的无旋式Treap实现类似C++STL中set的效果。

无旋式Treap 的特点

优点:支持可持久化,操作种类多(支持区间操作)

缺点:比众多平衡树效率低,且相比于旋转式Treap也要慢一些。

无旋式Treap 的节点定义

无旋式Treap 的节点定义和有旋式Treap一样

template <class T>
struct TreapNode
{TreapNode(const T &v, int f) : _left(nullptr), _right(nullptr), val(v), fix(f), w(1), size(1){}TreapNode *_left, *_right;T val;int fix, w, size;
};

无旋式Treap 的操作

分裂(split)

Treap的分裂有两种:按权值分裂和按排名分裂。思想一样,会一种另一种也就会了。这里介绍按权值分裂。

按照权值分裂

将指定根节点root分裂为两个Treap,第一个Treap所有节点的权值小于等于给定val值,第二个Treap所有节点的权值大于给定val值。(注意前者是小于等于,后者是严格大于)

    using PII = pair<TreapNode* , TreapNode*>;PII split(TreapNode *root, const T &val){\\...}

操作流程

  1. 判断root所指向的节点是否为空;为空就直接返回nullptr对

  2. 将当前root节点所指向的值与给定的key值进行比较:

    • 如果root → val > val ,则说明root所指向的节点及其右子树全部属于第二个Treap,同时向左子树继续分裂;

    • 如果root → val <= val,则说明root所指向的节点及其左子树全部属于第一个Treap,同时向右子树继续分裂。

  3. 将root和子树递归分裂后得到的两个子树中的一个连接,和另一个子树组成pair返回

  4. root分裂完要更新size值(update)

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    代码如下:

    /*void update(TreapNode *root){if (!root)return;root->size = root->rsize() + root->lsize() + 1;}*/PII split(TreapNode *root, const T &val){if (!root)return make_pair(nullptr, nullptr);if (root->val > val){PII lp = split(root->_left, val);root->_left = lp.second;update(root);return make_pair(lp.first, root);}else{PII rp = split(root->_right, val);root->_right = rp.first;update(root);return make_pair(root, rp.second);}}
合并(merge)

有分裂自然有合并。由于我们合并一定是为了修补分裂,而分裂是由权值划分为了两个子树,所以一定满足一个子树的val全都小于另一个子树。

所以我们的merge的两个参数为两个根节点指针u,v,要求u树的val全都小于v树的val

然后按照u和v的修正值进行合并,u->fix < v->fix的话,把u的右子树和v合并,否则把v的左子树和u合并

每次合并完都要更新size值(update操作)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

TreapNode *merge(TreapNode *u, TreapNode *v) // u全都小于v{if (!u)return v;if (!v)return u;if (u->fix < v->fix){u->_right = merge(u->_right, v);update(u);return u;}else{v->_left = merge(u, v->_left);update(v);return v;}}
插入(insert)

从插入和删除就能看出无旋式Treap的优越之处了,代码十分简单。

先按照val分裂,再把新结点和左子树合并,再合并两个子树

    void insert(const T &val){PII pp = split(p, val);if (pp.first && pp.first->val == val)pp.first->w++, pp.first->size++;elsepp.first = merge(pp.first, new TreapNode(val, rand()));p = merge(pp.first, pp.second);}
删除(erase)

先按照val分成三段,中间的一段就是val,把两边的两段合并即可

    void erase(const T &val){PII oo = split(p, val - 1);PII pp = split(p, val);if (pp.first && pp.first->val == val)delete pp.first;p = merge(oo.first, pp.second);}

查询区间排名等操作与旋转式Treap一样

完整代码

#include <iostream>
#include <string>
#include <algorithm>
#include <cassert>
#include <vector>
using namespace std;
#define int long long
template <class T>
struct TreapNode
{TreapNode(const T& v, int f) : _left(nullptr), _right(nullptr), val(v), fix(f), w(1), size(1){}int lsize(){return _left ? _left->size : 0;}int rsize(){return _right ? _right->size : 0;}TreapNode* _left, * _right;T val;int fix, w, size;
};
template <class T>
class Treap
{private:typedef TreapNode<T> Node;Node* p = nullptr;using PII = pair<Node*, Node*>;public:Node* ans;void prev(const T& val){prev(p, val);}void sub(const T& val){sub(p, val);}void insert(const T& val){PII pp = split(p, val);if (pp.first && pp.first->val == val)pp.first->w++, pp.first->size++;elsepp.first = merge(pp.first, new Node(val, rand()));p = merge(pp.first, pp.second);}void erase(const T& val){PII oo = split(p, val - 1);PII pp = split(p, val);if (pp.first && pp.first->val == val)delete pp.first;p = merge(oo.first, pp.second);}bool Find(const T& val){Node* cur = p;while (cur){if (cur->val > val)cur = cur->_left;else if (cur->val < val)cur = cur->_right;elsereturn true;}return false;}void InOrder(){_InOrder(p);}void PrevOrder(){_PrevOrder(p);}T kth(int k){return kth(p, k);}int getKth(const T& val){PII pp = split(p, val - 1);int ret = (pp.first ? pp.first->size : 0) + 1;p = merge(pp.first, pp.second);return ret;}private:void update(Node* root){if (!root)return;root->size = root->rsize() + root->lsize() + 1;}PII split(Node* root, const T& val){if (!root)return make_pair(nullptr, nullptr);if (root->val > val){PII lp = split(root->_left, val);root->_left = lp.second;update(root);return make_pair(lp.first, root);}else{PII rp = split(root->_right, val);root->_right = rp.first;update(root);return make_pair(root, rp.second);}}Node* merge(Node* u, Node* v) // u全都小于v{if (!u)return v;if (!v)return u;if (u->fix < v->fix){u->_right = merge(u->_right, v);update(u);return u;}else{v->_left = merge(u, v->_left);update(v);return v;}}void prev(Node* root, const T& val){if (!root)return;if (root->val < val)ans = root, prev(root->_right, val);elseprev(root->_left, val);}void sub(Node* root, const T& val){if (!root)return;if (root->val > val)ans = root, sub(root->_left, val);elsesub(root->_right, val);}T kth(Node* root, int k){if (root->lsize() + 1 == k)return root->val;else if (root->lsize() + 1 > k){return kth(root->_left, k);}else{return kth(root->_right, k - root->lsize() - root->w);}}void _InOrder(Node* root){if (!root)return;_InOrder(root->_left);cout << root->val << ":" << root->fix << ":" << root->size << " ";_InOrder(root->_right);}void _PrevOrder(Node* root){if (!root)return;cout << root->val << ":" << root->fix << ":" << root->size << " ";_PrevOrder(root->_left);_PrevOrder(root->_right);}
};

OJ练习

P3369 【模板】普通平衡树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这里直接用静态版无旋Treap

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
using namespace std;
#define int long longint root, idx, lc[100010] = {0}, rc[100010] = {0}, sz[100010] = {0}, rnd[100010] = {0}, key[100010] = {0};int newnode(int val)
{idx++;key[idx] = val, rnd[idx] = rand(), sz[idx] = 1;return idx;
}void update(int x)
{sz[x] = sz[lc[x]] + sz[rc[x]] + 1;
}void split(int cur, int k, int &l, int &r) // l树权值小于等于k r大于k
{if (!cur)l = r = 0;else{if (key[cur] <= k){l = cur;split(rc[cur], k, rc[cur], r);}else{r = cur;split(lc[cur], k, l, lc[cur]);}update(cur);}
}int merge(int l, int r)
{if (!l || !r)return l + r;if (rnd[l] < rnd[r]){rc[l] = merge(rc[l], r);update(l);return l;}else{lc[r] = merge(l, lc[r]);update(r);return r;}
}void insert(int k)
{int l, r;split(root, k, l, r);root = merge(merge(l, newnode(k)), r);
}void erase(int k)
{int x, y, z;split(root, k, x, z);split(x, k - 1, x, y);y = merge(lc[y], rc[y]);root = merge(merge(x, y), z);
}int kth(int k) // 排名
{int l, r;split(root, k - 1, l, r);int ret = sz[l] + 1;root = merge(l, r);return ret;
}int getkth(int p, int k)
{if (sz[lc[p]] + 1 == k)return key[p];else if (sz[lc[p]] + 1 > k){return getkth(lc[p], k);}else{return getkth(rc[p], k - sz[lc[p]] - 1);}
}
int ans;
void prev(int p, int val)
{if (!p)return;if (key[p] < val)ans = p, prev(rc[p], val);elseprev(lc[p], val);
}void sub(int p, int val)
{if (!p)return;if (key[p] > val)ans = p, sub(lc[p], val);elsesub(rc[p], val);
}signed main()
{ios::sync_with_stdio(false);cin.tie(0), cout.tie(0);srand(time(0));int n;cin >> n;root = idx = 0;while (n--){int s, x;cin >> s >> x;if (s == 1)insert(x);else if (s == 2)erase(x);else if (s == 3)cout << kth(x) << '\n';else if (s == 4)cout << getkth(root, x) << '\n';else if (s == 5){prev(root, x);cout << key[ans] << '\n';}else{sub(root, x);cout << key[ans] << '\n';}}return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/163750.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

qt报错permission denied

写fk项目的时候&#xff0c;报这个错&#xff0c;然后网上查&#xff0c;说的是因为之前运行的qt进程没有关闭&#xff0c;然后我在任务管理器上查看&#xff0c;却没有看见有我正在运行的qt程序&#xff0c;我再出现清除 qmake也不可以&#xff0c;然后我再去删除out目录下的所…

apache-tomcat-9.0.29 安装配置教程

链接&#xff1a;https://pan.baidu.com/s/100buXYpn8w8xjI2KdvHk2Q?pwd2mwc 提取码&#xff1a;2mwc 1.将压缩包解压到指定文件夹下 2.进入bin文件夹下 3.找到setclasspath.bat文件 4.推荐用notepad打开文件&#xff0c;并做如下配置&#xff08;可解决tomcat启动闪退问题&…

机器学习模型,超级全面总结!

机器学习是一种通过让计算机自动从数据中学习规律和模式&#xff0c;从而完成特定任务的方法。按照模型类型&#xff0c;机器学习可以分为两大类&#xff1a;监督学习模型和无监督学习模型。 附注&#xff1a;除了以上两大类模型&#xff0c;还有半监督学习和强化学习等其他类…

AD9371 官方例程裸机SW 和 HDL配置概述(二)

AD9371 系列快速入口 AD9371ZCU102 移植到 ZCU106 &#xff1a; AD9371 官方例程构建及单音信号收发 ad9371_tx_jesd -->util_ad9371_xcvr接口映射&#xff1a; AD9371 官方例程之 tx_jesd 与 xcvr接口映射 AD9371 官方例程 时钟间的关系与生成 &#xff1a; AD9371 官方…

CLIP Surgery论文阅读

CLIP Surgery for Better Explainability with Enhancement in Open-Vocabulary Tasks&#xff08;CVPR2023&#xff09; M norm ⁡ ( resize ⁡ ( reshape ⁡ ( F i ˉ ∥ F i ‾ ∥ 2 ⋅ ( F t ∥ F t ‾ ∥ 2 ) ⊤ ) ) ) M\operatorname{norm}\left(\operatorname{resize}\…

公开IP属地信息如何保护用户的隐私?

公开IP属地信息通常涉及与用户或组织的隐私有关&#xff0c;因此在公开此类信息时需要非常小心&#xff0c;以避免侵犯他人的隐私权。以下是触碰底线的几种情况以及如何保护网络安全和用户隐私&#xff1a; 个人隐私保护&#xff1a; 公开IP属地信息可能泄露用户的物理位置&…

nginx--install

1. ubuntu 1.1 下载并解压依赖 每个包去各自官网下载 stable 版就行。 tar xzvf nginx-1.24.0.tar.gz tar xzvf openssl-3.1.4.tar.gz tar xzvf pcre2-10.42.tar.gz tar xzvf zlib-1.3.tar.gz1.2 配置及安装 参数含义详见 nginx 官网 cd nginx-1.24.0./configure --pre…

【ES专题】ElasticSearch集群架构剖析

目录 前言阅读对象阅读导航要点笔记正文一、ES集群架构1.1 为什么要使用ES集群架构1.2 ES集群核心概念1.2.1 节点1.2.1.1 Master Node主节点的功能1.2.1.2 Data Node数据节点的功能1.2.1.3 Coordinate Node协调节点的功能1.2.1.4 Ingest Node协调节点的功能1.2.1.5 其他节点功能…

第一百六十五回 wheelChooser组件

文章目录 1. 概念介绍2. 使用方法3. 代码与效果3.1 示例代码3.2 运行效果 4. 内容总结 我们在上一章回中介绍了"如何实现Numberpicker"相关的内容&#xff0c;本章回中将介绍 wheelChoose组件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本…

pyqt5的组合式部件制作(一)

以多选一的选择器为例&#xff0c;来实践一下工程实用级别的组合式部件设计。自己之前做的自定义的组合式部件&#xff0c;结构不够简单优化&#xff0c;在实际的工程里面&#xff0c;使用部件的过程比较繁琐。所以&#xff0c;这里来做一个优化的实验。 之所以做这个组合部件&…

iOS实现代码混淆

iOS实现代码混淆 目录 前言 ipaguard界面概览 ipaguard启动界面 ipaguard代码混淆界面 资源文件混淆界面 重签名界面 前言 本文章向大家介绍iOS实现代码混淆&#xff0c;主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项&#xff0c;具有一定的参考价值…

AR眼镜硬件解决方案_AR/VR智能眼镜安卓主板芯片方案介绍

随着近两年来增强现实(AR)技术的逐渐成熟&#xff0c;采用MT8788芯片解决方案的AR眼镜已经问世。众所周知&#xff0c;AR技术可以帮助开发者打造一个既强大而又实用的混合现实世界&#xff0c;将虚拟与真实世界相结合。 据了解&#xff0c;MT8788芯片采用了多芯片分布式处理系统…