SplayTree,即伸展树,是竞赛中很常用的一种平衡树,它可以实现序列的分裂合并,并且保证均摊复杂度是O(nlogn)。
与Treep类似的,SplayTree也是基于旋转的,Splay,即伸展操作的实质就是在一棵子树中找到一个节点并把它旋转到根,复杂度靠谱的Splay有三种旋转(注意不要擅自修改旋转的方式,因为SplayTree复杂度的证明正是基于它的旋转方式的,否则可能会变成复杂度不靠谱的SpalyTree等):
1.目标节点是根节点的儿子节点:
…………..root………..
…………./………………
…………k……….(图略劣质别介意)
这时只需要进行一次旋转操作
2.目标节点是根节点下两层节点并且与根节点成直线(即是根节点的左儿子的左儿子,或是根节点的右儿子的右儿子)
………..root……
………/……………
……..s…………….
……./……………..
……k………………
这时只要连续两次旋转根节点就可以
3.目标节点是根节点下两层节点并且与根节点成折线(即是根节点的左儿子的右儿子,或是根节点的右儿子的左儿子)
………..root………..
………../………………
……….s………………
………..|………………
………..k…………….(由于\显示不出来,所以用|代替)
这时只要先将k旋到s的位置,再对root进行一次旋转就可以
看起来好像情况很复杂,但是,看了某汝佳的书后才发现代码可以这么缩:
void splay(pnode &p,int k){
p->pushdown();
int d1=p->cmp(k);
if(d1!=-1){
p->ch[d1]->pushdown();int d2=p->ch[d1]->cmp(k);
if(d2!=-1){
splay(p->ch[d1]->ch[d2],k);
if(d1==d2)rot(p,d1^1);
else rot(p->ch[d1],d2^1);
}
rot(p,d1^1);
}
}
另外,为了便于写代码,对cmp函数做一个小改动,即在比较的同时对k进行一个改动,若搜索的是右子树,就将k减去(左子树的个数+1),这样方便下一步的splay,代码如下:
int cmp(int &k){
if(k<ch[0]->num+1)return 0;
if(k==ch[0]->num+1)return -1;
k-=ch[0]->num+1;return 1;
}
这样,splay操作就完成了,再次提醒:
千万不要自作聪明的修改某些操作,建议将每种旋转后的树形都画一下,然后你会发现,和自己想出来的旋转方式最终的结果是不一样的。
SplayTree最常见的应用是维护一个可删除插入的序列,有了splay这个操作,我们就可以方便的实现各种操作了。
删除:
删除一段序列,只要把要删除的序列的第一个元素前一个元素旋到树根,再把要删除的序列的最后一个元素的后一个元素旋到树根的右儿子,这时,树根的右儿子的左儿子就是需要删除的序列。不要忘记,删除完之后标记要更新。
插入:
为了插入一段序列,首先要把插入的序列变成一个SplayTree(