Treap(树堆)入门
- 作者:zifeiy
- 标签:Treap
首先,我么要知道:Treap=Tree+Heap。
这里:
- Tree指的是二叉排序树;
- Heap指的是堆。
所以在阅读这篇文章之前需要大家对 二叉查找树 和 堆(Heap) 有一定的认识。
Treap支持如下操作:
- 插入x数
- 删除x数(若有多个相同的数,应只删除一个)
- 查询x数的排名(排名定义为比当前数小的数的个数+1。若有多个相同的数,应输出最小的排名)
- 查询排名为x的数
- 求x的前驱(前驱定义为小于x,且最大的数)
- 求x的后继(后继定义为大于x,且最小的数)
二叉排序树是这样的一棵树:
- 它是一棵二叉树;
- 任意节点的左儿子(如果有)的权值都小于该节点的权值;
- 任意节点的右儿子(如果有)的权值都大于该节点的权值。
二叉排序树可以实现上述6个功能,但是最坏情况下每一步操作的时间复杂度都会达到 \(O(n)\) 。
所以我们需要在二叉查找树的基础上引入堆的性质,形成一个 \(\Rightarrow\) Treap。
Treap的基本内容
首先,我们需要开一些数组来保存信息:
size[i]
:以i为根节点的子树的节点总数;v[i]
:i节点的权值;num[i]
:由于可能有多个节点具有相同的权值,所以,我们将权值一样的存在同一个节点里面,num[i]
存放的是i节点存的数的个数(即:num[i]
表示有多少个数的权值为v[i]
);
sum[i][2]
:用于存储i节点的儿子编号,其中:son[i][0]
表示i节点的左儿子,son[i][1]
表示i节点的右儿子;rd[i]
:i节点的随机值。
那么,rd[i]
的左右是什么呢?
每次创建一个新节点i的时候,都会为i节点分配一个随机值 rd[i]
。
堆就是在这里派上用场的——我们要让全部节点按照这个随机值排成一个堆。
这就引出了平衡树中最重要的一个概念——旋转。
rotate操作——旋转
旋转分两种:左旋和右旋,它们的共同特点是不改变Treap的二叉查找树的性质,同时让Treap更加平衡。
旋转可以维护Treap堆的性质,然后巧妙地防止Treap退化成链,使得操作的时间复杂度趋于 \(O(\log n)\) 。
Treap的基本操作
void pushup(int p) {
sz[p] = sz[ son[p][0] ] + sz[ son[p][1] ] + num[p];
}
用于重新统计以p为根节点的子树中元素个数。
void rot(int &p, int d) {
int k = son[p][d^1];
son[p][d^1] = son[k][d];
son[k][d] = p;
pushup(p);
pushup(k);
p = k;
}
旋转操作,d0:左旋;d1:右旋。
1. 插入一个数x
void ins(int &p, int x) {
if (!p) {
p = ++sum;
sz[p] = num[p] = 1;
v[p] = x;
rd[p] = rand();
return;
}
if (v[p] == x) {
num[p] ++;
sz[p] ++;
return;
}
int d = (x > v[p]);
ins(son[p][d], x);
if (rd[p] < rd[ son[p][d] ]) rot(p, d^1);
pushup(p);
}
如果 p==0
(即 !p
),那么就说明当前节点是一个空节点,此时我们开辟一个新节点;
如果 v[p]==x
,那么就说明当前要插入的位置p上面刚好存了一个x,直接放到这个点上面就OK了;
否则,我们需要递归的进一个子树进行插入,当 x<v[p]
时, d=0
,进左子树;当 x>v[p]
时,d=1
,进右子树递归地插入。
如果进左儿子插入x后,p节点的rd值小于它左儿子的rd值(即:\(rd[p] \lt rd[son[p][0]]\)),则右旋;
如果进右儿子插入x后,p节点的rd值小于它右儿子的rd值(即:\(rd[p] \lt rd[son[p][1]]\)),则左旋。
( 重点,想一想,为什么这样转不破坏堆的性质 )(我暂时还没有想明白~)
2. 删除一个数x
void del(int &p, int x) {
if (!p) return;
if (x < v[p]) del(son[p][0], x);
else if (x > v[p]) del(son[p][1], x);
else {
if (!son[p][0] && !son[p][1]) {
num[p] --; sz[p] --;
if (!num[p]) p = 0;
}
else if (!son[p][1]) {
rot(p, 1);
del(son[p][1], x);
}
else if (!son[p][0]) {
rot(p, 0);
del(son[p][0], x);
}
else {
int d = (rd[ son[p][0] > rd[ son[p][1] ] ]);
rot(p, d);
del(son[p][d], x);
}
}
pushup(p);
}
如果是空节点,则直接返回;
如果 x
和 p[x]
不相等,直接去相应子树递归删除;
如果 x==v[p]
,则:
- 如果x是叶子结点,直接扣掉个数,如果个数变为0则删掉节点;
- 如果x只有一个子节点,直接把子节点旋转上来,然后去相应子树解决;
- 如果有两个子节点,把大的那个转上来,然后去另一个子树解决。
3. 查询x数的排名
int get_rank(int p, int x) {
if (!p) return 0;
if (v[p] == x) return sz[ son[p][0] ] + 1;
if (v[p] < x) return sz[ son[p][0] ] + num[p] + get_rank(son[p][1], x);
if (v[p] > x) return get_rank(son[p][0], x);
}
如果不存在这个节点(到达了一个空节点),直接返回0;
如果x==v[p]
,那么左子树的全部节点都必定小于x,直接返回左子树节点数+1;
如果x>v[p]
,则x位于右子树,答案就是左子树元素个数+该节点元素个数+右子树中x的排名;
如果x<v[p]
,则x位于左子树,答案就是x在左子树的排名。
4. 查询排名为x的数
int func_find(int p, int x) {
if (!p) return 0;
if (sz[ son[p][0] ] >= x) return func_find(son[p][0], x);
else if (sz[ son[p][0] ] + num[p] < x)
return func_find(son[p][1], x-num[p]-sz[ son[p][0] ]);
else return v[p];
}
空节点没有排名,直接返回0;
如果左子树中节点个数大于x,则进左子树找;
否则,如果左子树加根节点的个数大于等于x,直接返回根节点的值v[p]
;
否则,说明左子树加根节点的个数小于x,进右子树找第 \(x-最节点和根节点元素个数\) 个元素。
5. 求x的前驱
int pre(int p, int x) {
if (!p) return -INF;
if (v[p] >= x) return pre(son[p][0], x);
else return max(v[p], pre(son[p][1], x));
}
如果是空节点,则没有前驱;
如果x是根或在右子树,去左子树找;
否则要么是根要么右子树,取一个max就可以了(前驱定义为小于x,且最大的数)。
6. 求x的后缀
int suc(int p, int x) {
if (!p) return INF;
if (v[p] <= x) return suc(son[p][1], x);
else return min(v[p], suc(son[p][0], x));
}
如果是空节点,则没有后缀;
如果在根或者左子树,去右子树找;
否则要么根要么左子树,取min就可以了(后继定义为大于x,且最小的数)。
洛谷上面有一道题是专门用于练习左偏树的题目:洛谷 P3369
实现代码如下:
#include <bits/stdc++.h>
using namespace std;
#define INF INT_MAX
const int maxn = 100010;
int sum = 0, R = 0;
int sz[maxn], v[maxn], num[maxn], rd[maxn], son[maxn][2];
// 用于重新统计以p为根节点的子树中元素个数
void pushup(int p) {
sz[p] = sz[ son[p][0] ] + sz[ son[p][1] ] + num[p];
}
// 左旋(d==0时),右旋(d==1时)
void rot(int &p, int d) {
int k = son[p][d^1];
son[p][d^1] = son[k][d];
son[k][d] = p;
pushup(p);
pushup(k);
p = k;
}
// 插入一个数x
void ins(int &p, int x) {
if (!p) {
p = ++sum;
sz[p] = num[p] = 1;
v[p] = x;
rd[p] = rand();
return;
}
if (v[p] == x) {
num[p] ++;
sz[p] ++;
return;
}
int d = (x > v[p]);
ins(son[p][d], x);
if (rd[p] < rd[ son[p][d] ]) rot(p, d^1);
pushup(p);
}
// 删除一个数x
void del(int &p, int x) {
if (!p) return;
if (x < v[p]) del(son[p][0], x);
else if (x > v[p]) del(son[p][1], x);
else {
if (!son[p][0] && !son[p][1]) {
num[p] --; sz[p] --;
if (!num[p]) p = 0;
}
else if (!son[p][1]) {
rot(p, 1);
del(son[p][1], x);
}
else if (!son[p][0]) {
rot(p, 0);
del(son[p][0], x);
}
else {
int d = (rd[ son[p][0] > rd[ son[p][1] ] ]);
rot(p, d);
del(son[p][d], x);
}
}
pushup(p);
}
// 查询x数的排名
int get_rank(int p, int x) {
if (!p) return 0;
if (v[p] == x) return sz[ son[p][0] ] + 1;
if (v[p] < x) return sz[ son[p][0] ] + num[p] + get_rank(son[p][1], x);
if (v[p] > x) return get_rank(son[p][0], x);
}
// 查询排名为x的数
int func_find(int p, int x) {
if (!p) return 0;
if (sz[ son[p][0] ] >= x) return func_find(son[p][0], x);
else if (sz[ son[p][0] ] + num[p] < x)
return func_find(son[p][1], x-num[p]-sz[ son[p][0] ]);
else return v[p];
}
// 求x的前驱
int pre(int p, int x) {
if (!p) return -INF;
if (v[p] >= x) return pre(son[p][0], x);
else return max(v[p], pre(son[p][1], x));
}
// 求x的后缀
int suc(int p, int x) {
if (!p) return INF;
if (v[p] <= x) return suc(son[p][1], x);
else return min(v[p], suc(son[p][0], x));
}
int T, op, x;
int main() {
cin >> T;
while (T --) {
cin >> op >> x;
if (op == 1) ins(R, x);
else if (op == 2) del(R, x);
else if (op == 3) cout << get_rank(R, x) << endl;
else if (op == 4) cout << func_find(R, x) << endl;
else if (op == 5) cout << pre(R, x) << endl;
else if (op == 6) cout << suc(R, x) << endl;
}
return 0;
}
然后我又用类封装了一下,C++类封装的代码如下:
#include <bits/stdc++.h>
using namespace std;
#define INF INT_MAX
const int maxn = 100010;
class Treap {
private:
int sum, R, sz[maxn], v[maxn], num[maxn], rd[maxn], son[maxn][2];
void pushup(int p) {
sz[p] = sz[ son[p][0] ] + sz[ son[p][1] ] + num[p];
}
void rot(int &p, int d) {
int k = son[p][d^1];
son[p][d^1] = son[k][d];
son[k][d] = p;
pushup(p);
pushup(k);
p = k;
}
void ins(int &p, int x) {
if (!p) {
p = ++sum;
sz[p] = num[p] = 1;
v[p] = x;
rd[p] = rand();
}
else if (v[p] == x) {
num[p] ++;
sz[p] ++;
}
else {
int d = (x > v[p]);
ins(son[p][d], x);
if (rd[p] < rd[ son[p][d] ]) rot(p, d^1);
pushup(p);
}
}
void del(int &p, int x) {
if (!p) return;
if (x < v[p]) del(son[p][0], x);
else if (x > v[p]) del(son[p][1], x);
else {
if (!son[p][0] && !son[p][1]) {
num[p] --; sz[p] --;
if (!num[p]) p = 0;
}
else if (!son[p][1]) {
rot(p, 1);
del(son[p][1], x);
}
else if (!son[p][0]) {
rot(p, 0);
del(son[p][0], x);
}
else {
int d = (rd[ son[p][0] > rd[ son[p][1] ] ]);
rot(p, d);
del(son[p][d], x);
}
}
pushup(p);
}
int get_rank(int p, int x) {
if (!p) return 0;
if (v[p] == x) return sz[ son[p][0] ] + 1;
if (v[p] < x) return sz[ son[p][0] ] + num[p] + get_rank(son[p][1], x);
if (v[p] > x) return get_rank(son[p][0], x);
}
int func_find(int p, int x) {
if (!p) return 0;
if (sz[ son[p][0] ] >= x) return func_find(son[p][0], x);
else if (sz[ son[p][0] ] + num[p] < x)
return func_find(son[p][1], x-num[p]-sz[ son[p][0] ]);
else return v[p];
}
int pre(int p, int x) {
if (!p) return -INF;
if (v[p] >= x) return pre(son[p][0], x);
else return max(v[p], pre(son[p][1], x));
}
int suc(int p, int x) {
if (!p) return INF;
if (v[p] <= x) return suc(son[p][1], x);
else return min(v[p], suc(son[p][0], x));
}
public:
Treap() {}
void Init() {
sum = R = 0;
memset(sz, 0, sizeof(sz));
memset(v, 0, sizeof(v));
memset(num, 0, sizeof(num));
memset(rd, 0, sizeof(rd));
memset(son, 0, sizeof(son));
}
void Insert(int x) { ins(R, x); }
void Delete(int x) { del(R, x); }
int GetRank(int x) { return get_rank(R, x); }
int Find(int x) { return func_find(R, x); }
int Pre(int x) { return pre(R, x); }
int Suc(int x) { return suc(R, x); }
} treap;
int main() {
treap.Init();
int T, op, x;
cin >> T;
while (T --) {
cin >> op >> x;
switch (op) {
case 1: treap.Insert(x); break;
case 2: treap.Delete(x); break;
case 3: cout << treap.GetRank(x) << endl; break;
case 4: cout << treap.Find(x) << endl; break;
case 5: cout << treap.Pre(x) << endl; break;
case 6: cout << treap.Suc(x) << endl; break;
default: break;
}
}
return 0;
}
Treap(树堆)入门的更多相关文章
- BZOJ3224/LOJ104 普通平衡树 treap(树堆)
您需要写一种数据结构,来维护一些数,其中需要提供以下操作:1. 插入x2. 删除x(若有多个相同的数,因只删除一个)3. 查询x的排名(若有多个相同的数,因输出最小的排名)4. 查询排名为x的数5. ...
- 可旋转Treap(树堆)总结
树堆,在数据结构中也称Treap,是指有一个随机附加域满足堆的性质的二叉搜索树,其结构相当于以随机数据插入的二叉搜索树.其基本操作的期望时间复杂度为O(logn).相对于其他的平衡二叉搜索树,Trea ...
- treap(树堆)
一棵treap是一棵修改了结点顺序的二叉查找树,如图,显示一个例子,通常树内的每个结点x都有一个关键字值key[x],另外,还要为结点分配priority[x],它是一个独立选取的随机数. 假设所有的 ...
- 查找——图文翔解Treap(树堆)
之前我们讲到二叉搜索树,从二叉搜索树到2-3树到红黑树到B-树. 二叉搜索树的主要问题就是其结构与数据相关,树的深度可能会非常大,Treap树就是一种解决二叉搜索树可能深度过大的还有一种数据结构. T ...
- 树堆(Treap)学习笔记 2020.8.12
如果一棵二叉排序树的节点插入的顺序是随机的,那么这样建立的二叉排序树在大多数情况下是平衡的,可以证明,其高度期望值为 \(O( \log_2 n )\).即使存在一些极端情况,但是这种情况发生的概率很 ...
- Treap树的基础知识
原文 其它较好的的介绍:堆排序 AVL树 树堆,在数据结构中也称Treap(事实上在国内OI界常称为Traep,与之同理的还有"Tarjan神犇发明的"Spaly),是指有一个随 ...
- Treap树
Treap树算是一种简单的优化策略,这名字大家也能猜到,树和堆的合体,其实原理比较简单,在树中维护一个"优先级“,”优先级“ 采用随机数的方法,但是”优先级“必须满足根堆的性质,当然是“大根 ...
- 6天通吃树结构—— 第三天 Treap树
原文:6天通吃树结构-- 第三天 Treap树 我们知道,二叉查找树相对来说比较容易形成最坏的链表情况,所以前辈们想尽了各种优化策略,包括AVL,红黑,以及今天 要讲的Treap树. Treap树算是 ...
- Treap树 笔记
预备知识:二叉查找树.堆(heap).平衡二叉树(AVL)的基本操作(左旋右旋) 定义: Treap.平衡二叉树.Tree+Heap.树堆. 每个结点两个键值(key.priority). 性质1. ...
- 真·浅谈treap树
treap树是一种平衡树,它有平衡树的性质,满足堆的性质,是二叉搜索树,但是我们需要维护他 为什么满足堆的性质?因为每个节点还有一个随机权值,按照随机权值维持这个堆(树),可以用O(logn)的复杂度 ...
随机推荐
- cf round 482D Kuro and GCD and XOR and SUM
题意: 开始有个空集合,现在有两种操作: $(1,x)$:给集合加一个数$x$,$x \leq 10^5$; $(2,x,k,s)$:在集合中找一个$a$,满足$a \leq s-x$,而且$k|gc ...
- 第一次接触node.JS
1.初识node.js node.js平时可以关注: 一.nodejs.org看看nodejs的版本升级,新特性的加入,重要bug的修复等 二.www.npmjs.com模块社区,看他人源代码,省力 ...
- spring boot 使用POI导出数据到Excel表格
在spring boot 的项目经常碰到将数据导出到Excel表格的需求,而POI技术则对于java操作Excel表格提供了API,POI中对于多种类型的文档都提供了操作的接口,但是其对于Excel表 ...
- Linux下安装zookeeper-3.4.13
转载至:https://yq.aliyun.com/articles/662422 1.zookeeper官网下载安装包http://mirrors.hust.edu.cn/apache/zookee ...
- Leetcode867.Transpose Matrix转置矩阵
给定一个矩阵 A, 返回 A 的转置矩阵. 矩阵的转置是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引. 示例 1: 输入:[[1,2,3],[4,5,6],[7,8,9]] 输出:[[1,4,7] ...
- 随机模块(import random)
随机的概念: 在某个范围内取到的每一个值的概率是相同的 随机小数: 1.random.random() #0-1之内的随机小数 2.random.unifom(1,5) #范围之内的随机小数 随机整数 ...
- Valgrind 初次接触
Valgrind 英文的意思是:堆内存 它有很多小工具,作用各不相同 学习于: http://blog.csdn.net/sduliulun/article/details/7732906 http: ...
- 【转载】【python】python练手项目
入门篇 1.Python - Python 图片转字符画 50 行 Python 代码完成图片转字符画小工具. <img src="https://pic3.zhimg.com ...
- 大数据技术之Hive
第1章 Hive入门 1.1 什么是Hive Hive:由Facebook开源用于解决海量结构化日志的数据统计. Hive是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张表,并提 ...
- Directx11教程(58) 鼠标控制摄像机
原文:Directx11教程(58) 鼠标控制摄像机 本篇教程我们实现鼠标旋转摄像机的操作.主要就是按下鼠标左键的时候,根据鼠标的移动对摄像机进行pitch, raw的组合旋转.具体修改 ...