Splay 学习笔记
Splay
概述
Splay也称伸展树,是二叉搜索树(BST)的一种近似平衡的类型,由Daniel Sleator 和 Robert Tarjan 于 1985 年发明。有着极其优秀的复杂度(均摊\(O(log_2n)\))。
可以实现Splay(旋转某节点到根),Split(分裂),Merge(合并),Insert(插入),Delete(删除),Get_Rank(根据权值找排名),Get_Num(根据排名找权值),Get_Pre(找前驱),Get_Next(找后继)。
在学习Splay之前,先确保你会普通二叉搜索树。
基础操作
New&Get
New
即为新建一个节点,一个Splay树的节点需要包含以下元素:
1)LeftS,RightS(左右子节点)
2)Value,Count(当前点的权值以及点的个数)
3)Father(当前点的父亲节点--我们定义根节点的父亲为0号节点)
4)Size(当前节点为根的树的大小)
Get
即为判断其是否为其父亲的左子节点,方便以后的操作。
以下为这两个操作的Code
int New(int Value){
Node[++cnt]={0,0,0,Value,1,1};
return cnt;
}
bool Get(int Now){
return Now==Node[Fa].LeftS;
}
Update(Maintain)
Update即为更新当前点的Size,\(Size_x=Size_{LeftS}+Size_{RightS}+Count_x\)
单独提这个操作出来是因为这个操作太过于重要了,这个操作如果缺少了那么就会出现很多问题。
一定不要忘了!!!
以下为Code
void Update(int Now){
Node[Now].Size=Node[LSon].Size+Node[RSon].Size+Node[Now].Count;
}
Zig&Zag(Rotate)
想要实现Splay这个操作,那么旋转是不可或缺的,旋转分为左旋和右旋,我们以右旋举例。
实现旋转操作,可以实现下图中的变换
而Zig操作,我们可以看到,需要将当前节点\(x\)的父亲与\(x\)的右子节点,并且将\(x\)与其父亲的父子关系互换,并且如果\(x\)原本的父亲有父亲,那么\(x\)应该与其原本的父亲的父亲相连。
旋转完之后记得Update。
以下为两种旋转的Code(当然,两个可以合成一个写)
void Zig(int Now){
int Grand=Node[Fa].Father;
Node[Fa].LeftS=RSon;
Node[RSon].Father=Fa;
if(Grand){
if(Get(Fa)) Node[Grand].LeftS=Now;
else Node[Grand].RightS=Now;
}
else{Root=Now;}
RSon=Fa;
Node[Fa].Father=Now;
if(Grand) Node[Now].Father=Grand;
else Node[Now].Father=0;
Update(RSon);Update(Now);
}
void Zag(int Now){
int Grand=Node[Fa].Father;
Node[Fa].RightS=LSon;
Node[LSon].Father=Fa;
if(Grand){
if(Get(Fa)) Node[Grand].LeftS=Now;
else Node[Grand].RightS=Now;
}
else{Root=Now;}
LSon=Fa;
Node[Fa].Father=Now;
if(Grand) Node[Now].Father=Grand;
else Node[Now].Father=0;
Update(LSon);Update(Now);
}
void Rotate(int Now){
if(Now==Root) return ;
else{
if(Get(Now)) Zig(Now);
else Zag(Now);
}
Update(Now);
}
Splay
Splay操作即是指定一个节点\(x\)将其旋转至根节点。
为了保证树的平衡性,我们在将一个节点旋转至根节点的过程中,我们并不只执行单旋转。
我们知道每进行一次旋转,当前节点的深度就会提高1,那么在其还有祖父节点时,我们就可以进行双旋转,而在其父亲节点即为根节点时,我们就进行单旋转。
双旋转分为一字旋和之子旋:
1)一字旋:当节点\(x\)与其父亲的儿子属性相同时(儿子属性:这里指其是其父亲的什么类型的儿子),我们就可以进行一字旋,这时先旋转其父亲节点,之后在旋转\(x\)本身。
2)之子旋:当节点\(x\)与其父亲的儿子属性不同时,我们就可以进行之子旋,这时旋转两次\(x\)本身即可。
利用了这两种操作的Splay操作,才可以保证Splay树的平均树高。
以下为Splay的Code
void Splay(int Now){
while(Now!=Root){
if(Fa==Root) Rotate(Now);
else{
if(Get(Fa)==Get(Now)) Rotate(Fa),Rotate(Now);
else Rotate(Now),Rotate(Now);
}
Update(Now);
}
Update(Now);
}
Insert
跟普通二叉查找树一样,如果当前值小于节点值,那么向左走,如果大于,那么向右走,如果等于,那么当前节点的Count+1。走到无路可走的话,就直接在当前位置新建节点。
每次插入一个节点,就要将当前节点旋转至根节点。
以下为Insert的Code
void Insert(int Value){
int Now=New(Value),Pos;
if(!Root){Root=Now;return;}
Pos=Root;
while(true){
if(Node[Pos].Value>Node[Now].Value){
if(Node[Pos].LeftS)Pos=Node[Pos].LeftS;
else{
Node[Pos].LeftS=Now;Node[Now].Father=Pos;
Update(Now);
Update(Pos);
Splay(Now);
break;
}
}
else if(Node[Pos].Value==Node[Now].Value){
Node[Pos].Count++;
Splay(Pos);
break;
}
else{
if(Node[Pos].RightS)Pos=Node[Pos].RightS;
else{
Node[Pos].RightS=Now;Node[Now].Father=Pos;
Update(Now);
Update(Pos);
Splay(Now);
break;
}
}
}
}
Get_Rank
根据权值求排名也是跟普通的二叉搜索树一样。每次与当前节点的值比较来判断是向左还是向右走,如果向右走那么答案累加左子树的大小以及当前点的Count,如果权值相同或者走到了空节点,那么就返回累计的值+1。
以下为Get_Rank的Code
int Get_Rank(int Num){
int Now=Root,Ret=0;
while(Now){
if(Num<Node[Now].Value){
Now=LSon;
}
else{
Ret+=Node[LSon].Size;
if(Num==Node[Now].Value){
Splay(Now);
return Ret+1;
}
Ret+=Node[Now].Count;
Now=RSon;
}
}
return Ret+1;
}
Get_Num
这部分也和普通平衡树一样。
每次向右儿子走的时候将Rank减去左子树的Size和当前点的Count即可。如果\(Rank\le0\) 说明此时已经找到了数值,返回即可。
以下是Get_Num的代码
int Get_Num(int Rank){
int Now=Root;
while(Now){
if(LSon&&Rank<=Node[LSon].Size){
Now=LSon;
}
else {
Rank-=(Node[LSon].Size+Node[Now].Count);
if(Rank<=0){
Splay(Now);
return Node[Now].Value;
}
Now=RSon;
}
}
}
Get_Pre&Get_Next
这两个操作很相似,我们以前驱举例。
求前驱则是从根节点开始,每次比较当前节点与所求值的大小,如果当前点的值小于所求值,那么尝试更新前驱的值,如果更新了前驱的值,那么就更新前驱所在的节点,并向右子树走,如果当前点的值小于所求值,就向左子树走,直到走到空节点。后继同理。
每次找到的前驱和后记都要将其旋转至根节点。
以下是Get_Pre&Get_Next的代码
int Get_Pre(int Num){
int Now=Root,Ret=-(1<<30),Pre=Root;
while(Now){
if(Node[Now].Value<Num){
if(Node[Now].Value>Ret) Pre=Now;
Ret=max(Node[Now].Value,Ret);
Now=RSon;
}
else if(Node[Now].Value>=Num){
Now=LSon;
}
}
Splay(Pre);
return Ret;
}
int Get_Next(int Num){
int Now=Root,Ret=1<<30,Next=Root;
while(Now){
if(Node[Now].Value>Num){
if(Node[Now].Value<Ret) Next=Now;
Ret=min(Node[Now].Value,Ret);
Now=LSon;
}
else if(Node[Now].Value<=Num){
Now=RSon;
}
}
Splay(Next);
return Ret;
}
Delete
删除操作比较复杂。
首先我们先将要删除的权值的点旋至根节点。
分情况讨论;
1)当前点的Count>1 ,将当前点的Count-1
2)当前点没有左子节点,那么直接立右子节点为根,让后将右子节点的父亲赋为0
3)当前点没有右子节点,那么直接立左子节点为跟,让后将左子节点的父亲赋为0
4)整棵树只有一个点,直接将根改为0
5)当前点既有左子节点,又有右子节点,那么我们在左子树中找到当前点的前驱节点作为新的根节点,因为只有这样的节点,右子树才为空,然后直接将原根的右子节点与新根相连即可。
以下是Delete的代码
void Delete(int Num){
Get_Rank(Num);
int Now=Root;
if(Node[Now].Count>1){Node[Now].Count--; Update(Now);}
else if((!LSon)&&(!RSon)) Root=0;
else if((!LSon)&&RSon){Root=RSon; Node[RSon].Father=0;}
else if((!RSon)&&LSon){Root=LSon; Node[LSon].Father=0;}
else if(LSon&&RSon){
Root=LSon;
Node[LSon].Father=0;
Get_Pre(Num);
Node[RSon].Father=Root;
Node[Root].RightS=RSon;
}
}
例题
luogu P3369【模板】普通平衡树
直接就是Splay的模板,用来Debug正好
luogu P1486 [NOI2004] 郁闷的出纳员
思路
仔细分析,每次对工资的操作都是对所有人而言的,那么我们可以用一个Tag来存总体偏移量,每次更新的时候查询最小的元素,如果当前值与偏移量的和小于最小值那么就将其删除,直到找不到这样的最小的数即可。
每次对于一个加入的操作,我们只需要将其的值减去偏移量然后再加入平衡树中即可。