#1329 : 平衡树·Splay
时间限制: 10000ms 单点时限: 1000ms 内存限制: 256MB
-
6 I 1 I 2 I 3 Q 4 D 2 2 Q 2
样例输出 -
3 1
- 第一次领略到splay tree的厉害。。
-
小Hi:Splay树,中文名一般叫做伸展树。
和Treap树相同,作为平衡树,它也是通过左旋和右旋来调整树的结构。
这里我们再复习一下左旋和右旋操作:
若以x作为参数(注意上一讲中是以p作为参数),其对应的伪代码分别为:hihocoder #1329 : 平衡树·Splay right-rotate(x): p = x.father x.father = p.father If (p.father is not empty) Then If (p.father.left == p) Then p.father.left = x Else p.father.right = x End If Else root = x End If p.left = x.right x.right.father = p x.right = p p.father = x left-rotate(x): p = x.father x.father = p.father If (p.father is not empty) Then If (p.father.left == p) Then p.father.left = x Else p.father.right = x End If Else root = x End If p.right = x.left x.left.father = p x.left = p p.father = x
和Treap树不同的是,Splay树不再用一个随机的权值来进行平衡,而是用固定的调整方式来使得调整之后的树会比较平衡。
在左旋右旋的基础上,Splay树定义了3个操作:
1. Zig
hihocoder #1329 : 平衡树·Splay 直接根据x节点的位置,进行左旋或右旋。
该操作将x节点提升了一层。
2. Zig-Zig
hihocoder #1329 : 平衡树·Splay 若p不是根节点,还有父亲节点g,且p和x同为左儿子或右儿子,则进行Zig-Zig操作:
当x,p同为左儿子时,依次将p和x右旋;
当x,p同为右儿子时,依次将p和x左旋。
注意此处不是将x连续Zig两次。该操作将x节点提升了两层。
3. Zig-Zag
hihocoder #1329 : 平衡树·Splay 若p不是根节点,则p还有父亲节点g。且p和x不同为左儿子或右儿子,则进行Zig-Zag操作:
当p为左儿子,x为右儿子时,将x节点先左旋再右旋;
当p为右儿子,x为左儿子时,将x节点先右旋再左旋。
该操作将x节点提升了两层。
进一步在Zig,Zig-Zig和Zig-Zag操作上,Splay树定义了"Splay"操作。
对于x以及x的祖先y,splay(x, y),表示对x节点进行调整,使得x是y的儿子节点:
splay(x, y): While (x.father != y) p = x.father If (p.father == y) Then // 因为p的父亲是y,所以只需要将x进行Zig操作 // 就可以使得x的父亲变为y If (p.left == x) Then right-rotate(x) Else left-rotate(x) End If Else g = p.father If (g.left == p) Then If (p.left == x) Then // x,p同为左儿子,Zig-Zig操作 right-rotate(p) right-rotate(x) Else // p为左,x为右,Zig-Zag操作 left-rotate(x) right-rotate(x) End If Else If (p.right == x) Then // x,p同为右儿子,Zig-Zig操作 left-rotate(p) left-rotate(x) Else // p为右,x为左,Zig-Zag操作 right-rotate(x) left-rotate(x) End If End If End If End While
在执行这个操作的时候,需要保证y节点一定是x节点祖先。
值得一提的是,大多数情况下我们希望通过splay操作将x旋转至整棵树的根节点。此时只需令y=NULL即可实现。
小Ho:旋转和Splay我懂了,但是要怎么运用上去呢?
小Hi:Splay树的插入和查询操作和普通的二叉搜索树没有什么大的区别,需要注意的是每次插入和查询结束后,需要对访问节点做一次Splay操作,将其旋转至根。
insert(key): node = bst_insert(key) // 同普通的BST插入, node为当前插入的新节点 splay(node, NULL) find(key): node = bst_find(key) // 同普通的BST查找, node为查找到的节点 splay(node, NULL)
同时由于Splay的特性,我们还有两个特殊的查询操作。在树中查找指定数key的前一个数和后一个数。
我们先将key旋转至根,那么key的前一个数一定是根节点左儿子的最右子孙,同时key的后一个数一定是根节点右儿子的最左子孙。
findPrev(key): splay( find(key), NULL ) node = root.left While (node.right) node = node.right Return node findNext(key): splay( find(key), NULL ) node = root.right While (node.left) node = node.left Return node
splay中的删除key操作:
splay的删除可以采用和一般二叉搜索树相同的方法:即先找到节点key,若key没有儿子则直接删去;若key有1个儿子,则用儿子替换掉x;若key有2个儿子,则通过找到其前(或后)一个节点来替换掉它,最后将该节点Splay到根。
同时,这里还有另一种方法来完成删除操作:
首先我们查找到key的前一个数prev和后一个数next。将prev旋转至根,再将next旋转为prev的儿子。
此时key节点一定是next的左儿子。那么直接将next的左儿子节点删去即可。
delete(key): prev = findPrev(key) next = findNext(key) splay(prev, NULL) splay(next, prev) next.left = NULL
这里你可能会担心如果key是数中最小或者是最大的数怎么办?
一个简单的处理方式是手动加入一个超级大和超级小的值作为头尾。
那么小Ho,这里有一个问题,假如要删除一个区间[a,b]的数该怎么做?
小Ho:我想想...我知道了!
因为要删除[a,b],那么我就要想办法把[a,b]的数旋转到一个子树上,再将这个子树删掉就行了。
方法和删除一个数相同,我首先将a的前一个数prev和b的后一个数next找出来。
同样将prev旋转至根,再将next旋转为prev的儿子。
那么此时next的左子树一定就是所有[a,b]之间的数了!
deleteInterval(a, b): prev = findPrev(a) next = findNext(b) splay(prev, NULL) splay(next, prev) next.left = NULL
小Hi:没错,那么下一个问题!如果a,b不在树中呢?
小Ho:这还不简单,把a,b插入树中,做完之后再删除不就好了!
小Hi:想不到小Ho你还蛮机智的嘛。
小Ho:那是,毕竟是我小Ho。(哼哼)
小Hi:Splay树由于splay操作的使得其相较于Treap具有更大的灵活性,并且不再有随机性。其插入、查找和删除操作的均摊时间复杂度也都是O(logn)的,具体的复杂度分析可以参考这里。那么最后小Ho你能够把Splay的实现出来么?
小Ho:没问题,看我的吧!
- AC:
-
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int inf = 0x3f3f3f3f; struct Node { int val; struct Node *father; struct Node *left,*right; Node(){} Node(int key) { this->val = key; this->father = this->left = this->right = NULL; } }; Node *root; void left_rotate(Node *x) { Node *p = x->father; x->father = p->father; if(p->father != NULL) { //左旋 if(p->father->left == p) { p->father->left = x; } else if(p->father->right == p) { p->father->right = x; } } else { //说明此时的p是根节点 root = x; } p->right = x->left; if(x->left != NULL) x->left->father = p; x->left = p; p->father = x; } void right_rotate(Node *x) { Node *p = x->father; x->father = p->father; if(p->father != NULL) { //右旋 if(p->father->left == p) { p->father->left = x; } else if(p->father->right == p) { p->father->right = x; } } else { //说明此时的p是根节点 root = x; } p->left = x->right; if(x->right != NULL) x->right->father = p; x->right = p; p->father = x; } void Splay(Node *x,Node *y) { //表示对x节点进行调整,使得x是y的儿子节点 while(x->father != y) { Node *p = x->father; if(p->father == y) { // 因为p的父亲是y,所以只需要将x进行Zig操作 // 就可以使得x的父亲变为y if(p->left == x) { right_rotate(x); } else { left_rotate(x); } } else { Node *g = p->father; if(p == g->left) { if(x == p->left) { // x,p同为左儿子,Zig-Zig操作 //当x,p同为左儿子时,依次将p和x右旋 //当x,p同为右儿子时,依次将p和x左旋 right_rotate(p); right_rotate(x); } else { // p为左,x为右,Zig-Zag操作 //当p为左儿子,x为右儿子时,将x节点先左旋再右旋 //当p为右儿子,x为左儿子时,将x节点先右旋再左旋 left_rotate(x); right_rotate(x); } } else { if(x == p->right) { // x,p同为右儿子,Zig-Zig操作 left_rotate(p); left_rotate(x); } else { // p为右,x为左,Zig-Zag操作 right_rotate(x); left_rotate(x); } } } } } Node *bst_insert(int key) { //类似平衡二叉树先插入节点 Node *p = new Node(key); if(root == NULL) { root = p; return root; } Node *t = root; while(true) { if(key > t->val) { //右枝走 if(t->right == NULL) { t->right = p; p->father = t; break; } t = t->right; } else if(key < t->val) { //左枝走 if(t->left == NULL) { t->left = p; p->father = t; break; } t = t->left; } else return t; //说明有重复元素 } return p; } Node *bst_find(int key) { //普通的平衡二叉树的查找 Node *t = root; while(t->val != key) { if(key > t->val) { if(t->right != NULL) { t = t->right; } else return NULL; } else { if(t->left != NULL) { t = t->left; } else return NULL; } } return t; } void insert(int key) { Node *p = bst_insert(key); Splay(p,NULL); } Node *find(int key) { Node *p = bst_find(key); if(p != NULL) { Splay(p,NULL); } return p; } Node *findPre(int key) { //查找指定数key的前一个数 Node *p = find(key); if(p == NULL) return NULL; Splay(p,NULL); Node *t = root->left; if(t == NULL) return root; while(t->right) { t = t->right; } return t; } Node *findNext(int key) { Node *p = find(key); if(p == NULL) return NULL; Splay(p,NULL); Node *t = root->right; if(t == NULL) return root; while(t->left) { t = t->left; } return t; } void Delete(int key) { Node *pre = findPre(key); Node *next = findNext(key); if(pre != NULL && next != NULL) { Splay(pre,NULL); Splay(next,pre); next->left = NULL; } } void deleteInterval(int a,int b) { insert(a); insert(b); Node *pre = findPre(a); Node *next = findNext(b); Splay(pre,NULL); Splay(next,pre); next->left = NULL; Delete(a); Delete(b); } int main() { int n,k,a,b; char op[2]; while(scanf("%d",&n)!=EOF) { root = NULL; insert(-1); insert(inf); while(n--) { scanf("%s",op); if(op[0] == 'I') { scanf("%d",&k); insert(k); } else if(op[0] == 'Q') { scanf("%d",&k); Node *p = find(k); if(p != NULL) { printf("%d\n",k); } else { insert(k); p = findPre(k); Delete(k); printf("%d\n",p->val); } } else { scanf("%d %d",&a,&b); deleteInterval(a,b); } } } return 0; }
描述
小Ho:小Hi,上一次你跟我讲了Treap,我也实现了。但是我遇到了一个关键的问题。
小Hi:怎么了?
小Ho:小Hi你也知道,我平时运气不太好。所以这也反映到了我写的Treap上。
小Hi:你是说你随机出来的权值不太好,从而导致结果很差么?
小Ho:就是这样,明明一样的代码,我的Treap运行结果总是不如别人。小Hi,有没有那种没有随机因素的平衡树呢?
小Hi:当然有了,这次我就跟你讲讲一种叫做Splay的树吧。而且Splay树能做到的功能比Treap要更强大哦。
小Ho:那太好了,你快告诉我吧!
输入
第1行:1个正整数n,表示操作数量,100≤n≤200,000
第2..n+1行:可能包含下面3种规则:
1个字母'I',紧接着1个数字k,表示插入一个数字k到树中,1≤k≤1,000,000,000,保证每个k都不相同
1个字母'Q',紧接着1个数字k。表示询问树中不超过k的最大数字
1个字母'D',紧接着2个数字a,b,表示删除树中在区间[a,b]的数。
输出
若干行:每行1个整数,表示针对询问的回答,保证一定有合法的解
样例输入