Treap简介
Treap 是指有一个随机附加域、满足堆的性质的二叉搜索树。其结构相当于以随机数据插入的二叉搜索树。其基本操作的期望时间复杂度为 O(logn) 。相对于其他的二叉搜索树,Treap 的特点是实现简单,且能基本实现随机平衡的结构。
Treap特性
Treap 在基本的二叉搜索树基础上增加了一个随机附加域,整棵树不仅要满足二叉搜索树左儿子小右儿子大的性质,同时也要满足按照随机附加域成一个堆。正因为附加域的随机性,使得 Treap 可以保持一个较为随机的结构,其平均树高为 O(logn) 级别,这 也是它能够实现基本操作时间复杂度 O(logn) 的原因。
Treap的存储
通常来说我们用一个结构体来存储一个Treap节点:
struct treap_node{
int value, size, key;
//value为点权值
//key为随机附加域
//size为子树的大小
treap_node *ch[2];//记录左右儿子的指针
}
因为平衡树是一个动态的结构,所以节点需要动态的开。在C++里面,new操作符是比较慢的。所以建议大家不要使用。建议用内存池写法。
先估计一下,大约需要用到多少个Treap节点。
treap_node pool[max_node];
int tot = 0;
//记录当前内存池中哪些节点已经使用
获取一个新节点:
inline treap_node *get_new(int value){
treap_node *now = pool + ++top;
now->value = value;
now->size = 1;
now->key = rand();
now->ch[0] = now->ch[1] = null;
return now;
}
上面的代码里面出现了一个null,注意和空指针NULL区分。因为访问空指针会引起RE,为了避免麻烦,我们自己造一个假的空指针,这个指针null指向一个没有用的结构体。null的size=0,key=INF,这样根据堆的性质能保证它待在最底下而不会跑上来。
Treap的旋转
堆里面为了将内部元素调整成满足性质的,有下放和上放两个操作。分别是和儿子/父亲进行比较,如果不满足大小关系的限制就交换。
但是我们在平衡树中不能直接交换,直接交换就不满足二叉搜索树左儿子小右儿子大的性质。
我们如何既能实现树形态的改变(以满足堆的性质),又不影响其二叉搜索树的本质?
旋转旋转!
//取now是权值为3的节点
//wh为0
//(即左儿子旋转到now的位置)
void rotate(treap_node *&now, int wh){
treap_node *child = now->ch[wh];
now->ch[wh] = child->ch[wh ^ 1];
child->ch[wh ^ 1] = now;
now->update();
child->update();
now = child;
}
update操作是什么?
我们的Treap节点会维护一些和子树有关的信息,比如size(子树里面节点的个数)。当子树发生变化的时候,就需要更新这些信息:
void treap_node::update(){
size=1+ch[0]->size+ch[1]->size;
}
在实际应用中,update函数还有可能维护一些别的信息,比如子树里面节点权值的最大者,节点权值和等等。
Treap的插入
如何找插入位置?
- 从根节点开始找,如果新节点权值小于当前节点权值,去左子树;反之,去右子树。(我们这里先假设所有元素权值不相等)
- 当我们到达一个叶子节点的时候,如果新点权值比叶子节点小,把新节点放左边;反之,放右边。
如何调整使其满足堆的性质?
- 如果当前新节点的随机附加域的大小小于自己的父亲,我们就向上旋转。
void insert(treap_node *&now, int value) {
if (now == null) {
now = get_new(value);
return;
}
if (now->value == value)
return;
int wh = value < now->value ? 0 : 1;
insert(now->ch[wh], value);
now->update();
int minwh =
now->ch[0]->key < now->ch[1]->key ? 0 : 1;
if (now->ch[minwh]->key < now->key)
rotate(now, minwh);
}
Treap的删除
如果一个节点是叶子节点,我们能够很方便的删除。所以我们的思路是,把要删除的点挪到叶子节点的位置。一路旋转,注意还要维护堆的性质。
找两个儿子中key值较大的那个,和它进行旋转操作,直到自己变成叶子节点。
void del(treap_node *&now, int value) {
if (now == null) return;
if (now->value == value) {
int minwh =
now->ch[0]->key < now->ch[1]->key ? 0 : 1;
if (now->ch[minwh] != null) {
rotate(now, minwh);
del(now->ch[minwh ^ 1], value);
now->update();
}
else
now = null;
}
else {
int wh = value < now->value ? 0 : 1;
del(now->ch[wh], value);
now->update();
}
}
Treap的查找
和insert,del函数中的查找方式一样。值小向左走,值大向右走。
Treap计算Rank
计算一个数字当前是第几小(大)的
首先不停的查找这个数字。如果当前要向左走,答案不变;如果当前要向右走,答案要+左子树的大小+1。
int cou(treap_node *now, int value)
{
if (now == null) return 0;
int left_size = now->ch[0]->size;
if (now->value == value)
return left_size;
else if (value < now->value)
return cou(now->ch[0], value);
else
return left_size + 1 + cou(now->ch[1], value);
}
Treap找第k大
这个过程和计算Rank正好相反。
看一下k和当前左子树大小的关系:
如果k<=ch[0]->size,往左走。
如果ch[0]->size+1==k,bingo
如果ch[0]->size+1
int kth(treap_node *now, int k)
{
if (now == null) return INF;
int left_size = now->ch[0]->size;
if (k <= left_size)
return kth(now->ch[0], k);
else if (k == left_size + 1)
return now->value;
else
return kth(now->ch[1], k - left_size - 1);
}
例题:SPOJ 3273 Order Statistic Set
题目大意:
你要实现一个动态集合,支持以下两种操作:
1.Insert(S,x):如果x不在集合S中,插入x。
2.Delete(S,x):如果x在集合S中,删除x。
并且支持以下两种查询操作:
1.K-th(k):返回集合S中第k小的元素。
2.Count(S,x):返回集合S中比x小的元素的个数。
输入:
第一行一个整数Q(Q<=200000)表示操作和查询的个数。
接下来每行,由一个大写字母和一个数字构成。I表示Insert,D表示Delete,K表示K-th,C表示Count。后面的数字即为操作中的x或者k。其中x绝对值小于1000000000,k为1到1000000000之间的整数。
输出:
对于每一个K-th操作,输出一行整数表示结果。若集合中元素不足k个,输出invalid。对于每一个Count操作,输出一行整数表示结果。
Treap总结
Treap相对于其它平衡树的优点是实现方便,且常数比较小。
缺点是功能不够强大。所以我们竞赛中通常采用splay而非treap。