树堆(tree+heap)Treap是一种平衡二叉搜索树。
在常规的二叉树中,树的形状被输入的顺序所影响,最差的情况下,树会退化成一个链表;而最好的情况下树应该是平衡的(秩最低),Treap的解决方法是:给每一个结点随机地赋予优先级,然后按优先级而非键值插入结点。在后文中,我们将会详细地解释优先级、键值在Treap中起到的作用以及Treap的实现模板。
优先级和键值-Treap的两个维度
二叉树是一种二维的数据结构,它具有宽度和高度。在常规的树中,键值(key)决定的了树的宽度梯度(左高右低),而输入的时间先后决定了树的高度梯度(上先下后),也就是越晚输入的数在树的越下面,而键值越大的数在树的越右边。
那么在Treap中有什么不同呢?我们为了不让输入顺序影响我们树的形状,因此引入了优先级(rank)去代替输入顺序,来决定我们树的高度梯度。这个优先级往往是随机的,因此尽管往往不会达到平衡的最优解,但期望的复杂度是O(log2n)的。
清楚了优先级和键值的作用地位,你几乎已经了解了Treap树的一切。你会发现对于键值来说,这是一棵二叉搜索树,对于优先级来说,这是一个堆(根结点的优先级总是最大的)。
最后在我们开始实现它之前,再强调一下:Treap的两个维度是互相垂直的,互不影响,因此左右儿子不存在优先级的绝对关系,而它们的优先级都比它们的根节点低;同样的,优先级关系确定的结点也无法确定键值的关系。
Treap结点
一个Treap结点应该有以下接口:
(1)键值 key
(2)优先级 rank
(3)当前结点的子树的结点总数(用于名次树) size
(4)左二子son[0] , 右儿子son[1]
(5)重载<用于比较两个结点优先级的大小(不一定需要)
(6)一个比较两个结点键值大小且返回0,1值的函数(用于对应左右儿子)
(7)一个用于更新结点size的函数(需要3时则需要)
struct Node{
int size; //当前节点的子树节点总数,用于名次树
int rank; //优先级
int key; //键值
Node* son[2]; //0是左儿子,1是右儿子
bool operator<(const Node& a)const{return rank<a.rank;} //重载<
int cmp(int x)const{ //const代表函数是只读的
if(x==key) return -1; //Treap树不应存在相同键值的节点
return x<key?0:1; //x本身是一个键值,判断x和当前节点的优先级大小
}
void update(){ //更新size
size = 1;
if(son[0]!=NULL) size += son[0]->size;
if(son[1]!=NULL) size += son[1]->size;
}
};
Treap的动态调整-左旋和右旋
我们可以将树所有的结点的信息都储存完毕了,再按键值和优先级插入它们,但这会浪费大量栈的空间,为了更好,我们先根据键值朴素地插入新结点,这个时候的树只是普通的二叉树,然后通过动态调整的手段根据新的结点的优先级调整它的位置,覆盖掉输入顺序的影响,使其称为Treap树——Treap的旋转。
Treap的旋转是一个比较抽象的概念,左和右是根据Treap在旋转后平面上的变化命名的。旋转之所以比较抽象和复杂,是因为它的变化同时影响了key和rank两个维度,我们不得不在遵守key梯度规则的同时修正树使得其满足rank梯度规则。无论如何,我们可以这样记住旋转的过程:
我们将要进行旋转的结点称为主(图中的k),主结点原本的根结点称为根(图中的o);
左旋:根的右变成主的左,主的左变成根,根变成主;
右旋:根的左变成主的右,主的右变成根,根变成主。
void rotate(Node* &o, int d){ //d=0左旋,d=1右旋
Node *k = o->son[d^1]; //^异或运算符,同0异1,此处等于1-d
o->son[d^1] = k->son[d]; //根的右/左变成主的左/右
k->son[d] = o; //主的左/右变成根
o->update(); //更新size
k->update();
o = k; //根变成主
}
Treap结点的插入
void insert(Node* &o, int x){ //把x插入到树
if(o==NULL){
o = new Node(); ()调用默认构造函数进行初始化
o->son[0] = o->son[1] = NULL;
o->rank = rand(); //随机赋予优先级
o->key = x;
o->size = 1;
}
else{
int d = o->cmp(x); //判断x和o的键值大小
insert(o->son[d],x);
o->update();
if(o<o->son[d]) //判断o和o被修改的子树的优先级是否违法
rotate(o,d^1);
}
}
Treap结点的删除
对于Treap结点的删除有两种情况:
(1)如果结点是叶子结点(没有子结点),则直接删除。
(2)结点有子结点,判断子节点哪个优先级高,往反方向(优先级低的方向)旋转,将优先级高的子结点转上来,保证优先级的顺序不变。
代码实现步骤:
(1)查找结点位置。
(2)按情况删除结点。
(3)回溯,更新size
void Delete(node* &root,int x){
int d=root->cmp(x); //判断键值
if(d==-1){ //键值一样,找到了
node* u=root;
if(root->son[0]!=NULL&&root->son[1]!=NULL){
int d2=(root->son[0]>root->son[1]?1:0); //判断优先级大小
rotate(root,d2); //向反方向旋转
remove(root->son[d2],x);
}
else{
if(root->son[0]==NULL)
root=root->son[1];
else
root=root->son[0];
delete u;
}
}
else remove(root->son[d],x);
if(root!=NULL)
o->update(); //在回溯中更新size
}
Treap结点名次的查询
在treap树中存在名次,所谓名次即这个结点是第几大的。名次是一个键值维度的量,优先级大小与结点名次无关。
让我们来解释一下size、名次和名次查询的逻辑。
在下面这棵树中,我们将size(由该结点作为根节点的树的结点总数)标注在结点上:
随后,我们分别把这棵树的键值大小和优先级进行标注,首先把键值大小标注在结点下面(第几大),随后把优先级用虚线区分:
仔细比对可以发现:size和优先级没有绝对关系,而右子树的size和名次是正相关的,左子树的size和名次是负相关的。 由于size没有左右的区别,而名次梯度是从右到左的,这是size和名次的左右相反性。
先单纯考虑简单的情况:如果我们要找的结点在整棵树的右边,也就是寻找的路径没有左转,一直在往右子树深入,则显然:右儿子的size+1等于根节点的名次。
当然,这个结论在路径出现左转的时候由于名次的左右相反性出现错误,为了修正这个错误,当每次路径左转时,其右儿子的(size+1)*2才是真实的名次。
接下来放出模板函数,如果觉得理解思路比较困难,可以用纸笔多模拟一下搜索的过程。
找到元素k的名次:
int Find(Node* o, int k){ //返回元素k的名次
if(o==NULL) return -1; //树为空,没找到
int d = o->cmp(k); //判断k和o的键值大小
if(d==-1) //k和o的键值一样
return o->son[1]==NULL?1:o->son[1]->size+1; //返回名次
else if(d==1) return Find(o->son[d],k); //k的键值大,去右子树找
else{ //k的键值小
int tmp = Find(o->son[d],k); //去左子树找
if(tmp == -1) return -1; //树为空,没找到
else //找到了
return o->son[1] == NULL? tmp+1 : tmp+1+o->son[1]->size;
//左右名次相反性,回溯中恢复正确名次
}
}
找到名次为k的元素:
int kth(Node* o,int k){ //返回第k大的数
if(o==NULL||k<=0||k>o->size) return -1; //树是空的、k过小、过大
int s = o->son[1]==NULL?0:o->son[1]->size; //s是右儿子的size,s+1是o的名次
if(k==s+1) return o->key; //说明k和o的名次一样,找到了
else if(k<=s) return kth(o->son[1],k);//k比o的名次小,先去它的右子树找
else return kth(o->son[0],k-s-1); //k比o的名次大,去它的左子树找,同时k=k-s-1
}//对于右子树而言,size和名次是等价的,对于左子树而言,名次=k-s-1(名次的左右相反性)
Treap的合并
Treap的合并思路比较暴力,时间复杂度是O(nlogn),在小数据范围内还行。逻辑就是先找到根节点优先级较大的那个树(假设为x),再将另一棵树(y)的结点一个个拆散插入到x中,所以实际上是Delete和Insert函数的合成:
void Merge(node* &x,node* &y) //x根结点的优先级更高
{
if(y->son[0]!=NULL) Merge(x,y->son[0]);
if(y->son[1]!=NULL) Merge(x,y->son[1]);
insert (x,y->key); //找到y的叶子结点,插入进x
delete y;
y=NULL; //在y中删除这个叶子结点,回溯
}
Treap的一些补充操作
基本的Treap操作已经写完了,作为数据结构的笔记,老规矩在最后放出Treap的一些补充操作(也许会更新……)
求x的前驱(小于x的最大数)
int Pre(Node* &root,int x){ //求x前驱(小于x的最大的数)
if(root==NULL) //若为空
return INT_MIN; //返回极小值
if(root->key>=x) //若当前点点权大于等于x
return Pre(root->son[0],x); //向左子树搜索
return max(root->key,Pre(root->son[1],x));
//否则返回当前点点权和向其右子树中返回值(若右子树为空则会直接返回极小值)
}
求x的后继(大于x的最小数)
int Next(Node* &root,int x){ //求x后继(大于x的最小的数)
if(root==NULL) //若为空
return INT_MAX; //返回极大值
if(root->key<=x) //若当前点点权小于等于x
return Next(root->son[1],x); //向右子树搜索
return min(root->key,Next(root->son[0],x));
//否则返回当前点点权和向其左子树中返回值(若左子树为空则会直接返回极大值)
}
题目参考
POJ1442 Black Box
HDU4584 Shaolin
HDU3726 Graph and Queries