伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。
旋转操作(左旋右旋)和前面的treap一样,伸展树最重要的操作是伸展操作,它把一个指定结点x自底向上旋转到根结点。分三种情况:
情况一:节点x的父节点y是根节点。这时,如果x是y的左孩子,我们进行一次Zig(右旋)操作;如果x是y的右孩子,则我们进行一次Zag(左旋)操作。经过旋转,x成为二叉查找树S的根节点,调整结束。即:如果当前结点父结点即为根结点,那么我们只需要进行一次简单旋转即可完成任务,我们称这种旋转为单旋转。
情况二:节点x的父节点y不是根节点,y的父节点为z,且x与y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。这时,我们进行一次Zig-Zig操作或者Zag-Zag操作。即:设当前结点为X,X的父结点为Y,Y的父结点为Z,如果Y和X同为其父亲的左孩子或右孩子,那么我们先旋转Y,再旋转X。我们称这种旋转为一字形旋转。
情况三:节点x的父节点y不是根节点,y的父节点为z,x与y中一个是其父节点的左孩子而另一个是其父节点的右孩子。这时,我们进行一次Zig-Zag操作或者Zag-Zig操作。即:这时我们连续旋转两次X。我们称这种旋转为之字形旋转。
struct Node{
Node* ch[2];
int s;
int v;
int flip;
int cmp(int k) const{
int d=k-ch[0]->s;
if(d==1) return -1;
return d<=0?0:1;
}
void maintain(){
s=ch[0]->s+ch[1]->s+1;
}
};
伸展操作,利用刚才那三种操作,把第k个元素旋转到根。
//k>=1
void splay(Node*& o,int k){
o->pushdown();
int d=o->cmp(k);
if(d==1) k-=o->ch[0]->s+1;
if(d!=-1){
Node* p=o->ch[d];
p->pushdown();
int d2=p->cmp(k);
int k2=(d2==0?k:k-p->ch[0]->s-1);
if(d2!=-1){
splay(p->ch[d2],k2);
if(d2==d) rotate(o,d^1);
else rotate(o->ch[d],d);
}
rotate(o,d^1);
}
}
merge操作,把序列S1,S2连接在一起,S1在左边,S2在右边,返回新序列。
Node* merge(Node* left,Node* right){
splay(left,left->s);
left->ch[1]=right;
left->maintain();
return left;
}
split操作,把S分裂成两个连续的子序列,左子序列边包含左边k个元素,右子序列包含其余元素。k>=1,也就是说左边至少有一个元素。
void split(Node* o,int k,Node*& left,Node*& right){
splay(o,k);
left=o;
right=o->ch[1];
o->ch[1]=null;
left->maintain();
}
range(L,R),截出[L,R)这一段放在root->ch[1]->ch[0],[L,R)是加了一个起始虚拟结点之后的区间。
Node*& range(int L,int R){
splay(root,L);
splay(root->ch[1],R-L+1);
return root->ch[1]->ch[0];
}
由于split左边不能为空,因此解决问题时通常加一个开始结点和一个结束结点。
伸展树用于处理序列问题,插入删除旋转之类的问题。