<目录>
0.NewNode 操作——————————–(新建一个节点)
1.Splay、Rotate 操作 ————————(Splay 旋转<核心>)
2.GetPre、GetNext 操作 ——————–(获得前驱后继)
3.GetMin、GetMax 操作 ——————– (获得最小值,最大值)
4.Insert 操作 ————————————(插入节点,以二叉排序树原则)
5.Delete 操作 ———————————– (删除节点)
6.Find_Kth 操作 ——————————— (获取第K大点)
7.Insert_Kth 操作 —————————— (将节点插入序列第K个位置)
8.PushUp操作 ——————————— (维护区间信息)
9.Merge 操作 ———————————— (将两个子树合并)
10.CoutAns 操作 ———————————-(输出整个序列)
11.GetSequence 操作 ———————— (获得一段序列)
12.Insert_Sequence 操作 ——————— (将序列插入现序列的K位置)
13.PushDown 操作 —————————–(维护’线段树’Lazy下放标记)
14.Delete_UpK 操作 —————————–(删除 大于或小于K的所有数)
15.Same 操作 ————————————–(当操作涉及重复元素)
16.极简代码
快速链接:
-
-
-
- 前言
- NewNode 操作新建一个节点
- SplayRotate 操作 Splay 旋转核心
- GetPreGetNext 操作 获得前驱后继
- GetMinGetMax 操作 获得最小值最大值
- Insert 操作 插入节点以二叉排序树原则
- Delete 操作 删除节点
- Find_Kth 操作 获取第K大点
- Insert_Kth 操作 将节点插入序列第K个位置
- PushUp操作 维护区间信息
- Merge 操作 将两个子树合并
- CoutAns 操作 输出整个序列
- GetSequence 操作获得一段序列操作
- Insert_Sequence 操作 将序列插入现序列的K位置
- PushDown 操作 维护线段树Lazy下放标记
- Delete_UpK 操作 删除大于或小于K的所有数
- Same 操作 当操作涉及重复元素
- 极简代码
- 一些练习的题目
-
-
前言:
本文中,ll为define long long ,x为splay节点,tarfa为目标父亲位置,
ls为左儿子,rs为右儿子,fa为父亲,sz为子树大小,root为当前树的根。
splay的精华在于每次Insert等后都进行Splay旋转,这样就保证了树的高度永远为logN级别。
0.NewNode 操作(新建一个节点)
直接上代码:
IL void NewNode(RG int Value){ //新建一个权值为Value的结点
fa[++cnt] = ls[cnt] = rs[cnt] = 0;
blg[cnt] = cnt; val[cnt] = Value; sum[cnt] = 1; return;
}
此函数中的各个变量用处会在下文中有所展现。
1.Splay、Rotate 操作 (Splay 旋转<核心>)
没什么好说的。
Left_Rotate:
IL void Left_Rotate(RG ll x){
RG ll y = fa[x],z = fa[y];
fa[ls[x]] = y; rs[y] = ls[x];
fa[x] = z; if(z){if(ls[z]==y)ls[z]=x; else rs[z]=x;}
fa[y] = x; ls[x] = y; Update(y); Update(x);
}
Right_Rotate:
IL void Right_Rotate(RG ll x){
RG ll y = fa[x],z = fa[y];
fa[rs[x]] = y; ls[y] = rs[x];
fa[x] = z; if(z){if(ls[z]==y)ls[z]=x; else rs[z]=x;}
fa[y] = x; rs[x] = y; Update(y); Update(x);
}
Splay:
IL void Splay(RG ll x,RG ll tarfa){ //把x splay到tarfa的儿子
while(fa[x] != tarfa){
RG ll y = fa[x],z = fa[y];
if(z == tarfa){
if(ls[y] == x)Right_Rotate(x);
else if(rs[y] == x)Left_Rotate(x);
}
else if(z != tarfa){
if(ls[z] == y && rs[y] == x){Left_Rotate(x); Right_Rotate(x);}
else if(rs[z] == y && ls[y] == x){Right_Rotate(x); Left_Rotate(x);}
else if(ls[z] == y && ls[y] == x){Right_Rotate(x); Right_Rotate(x);}
else if(rs[z] == y && rs[y] == x){Left_Rotate(x); Left_Rotate(x);}
}
if(!tarfa)root = x;
}return;
}
2.GetPre、GetNext 操作 (获得前驱后继)
以获取node节点的前驱,后继为例。
GetPre(获取前驱):
IL ll Get_pre(RG ll x){
Splay(x,0); x = ls[x];
while(rs[x])x = rs[x]; return x;
}
Pre_node = Get_pre(ls[node]); //即左子树中最大的。
GetNext(获取后继):
IL ll Get_next(RG ll x){
Splay(x,0); x = rs[x];
while(ls[x])x = ls[x]; return x;
}
Next_node = Get_Next(rs[node]); //即右子树中最小的。
3.GetMin、GetMax 操作 (获得最小值,最大值)
即序列的最左端点与最右端点,类似GetPre,GetNext。
GetMin:
IL void GetMin(RG ll x){while(ls[x])x = ls[x]; return x;}
Min = GetMin(root);
GetMax:
IL void GetMax(RG ll x){while(rs[x])x = rs[x]; return x;}
Max = GetMax(root);
4.Insert 操作 (插入节点,以二叉排序树原则)
与二叉排序树一样,以按照val从大到小为排序原则为例。
void Insert(RG ll rt,RG ll nw){
if(val[rt]>val[nw] && !ls[rt]){ls[rt]=nw; fa[nw]=rt; return;}
else if(val[rt]>val[nw] && ls[rt])Insert(ls[rt],nw);
else if(val[rt]<val[nw] && !rs[rt]){rs[rt]=nw; fa[nw]=rt; return;}
else if(val[rt]<val[nw] && rs[rt])Insert(rs[rt],nw);
}
//把x节点插入: Insert(root,x);
5.Delete 操作 (删除节点)
以删除x节点为例子。
IL void Delete(RG ll x){
Splay(x,0);
if(!ls[x] && !rs[x]){root = 0; return;}
if(!ls[x] && rs[x]){fa[rs[x]] = 0; root = rs[x]; return;}
if(!rs[x] && ls[x]){fa[ls[x]] = 0; root = ls[x]; return;}
RG ll Rc = GetMin(rs[x]); Splay(Rc,0);
ls[Rc] = ls[x]; fa[ls[x]] = Rc; fa[Rc] = 0;
root = Rc; Splay(Rc); return;
}
解释一下:
首先预先找到x节点。 然后Splay x 到根节点。
这时候分4中情况讨论
<1> (!ls[x] && !rs[x]):树中已经没有节点,root = 0;
<2> (!ls[x] && rs[x]): 让右儿子成为新的根即可。
<3> (!rs[x] && ls[x]):让左儿子成为新的根即可。
<4> (ls[x] && rs[x]):这种情况比较麻烦,见下面详解:
{
我们拟定把左子树接到右子树上面去。
先找到右子树的Min,Rc = GetMin(rs[x]);
然后把Rc Splay到根部去。
那么此时Rc即为ls[x]的后继。
此时让Rc成为新的根,root = Rc,fa[Rc] = 0;
连接一下即可:ls[Rc] = ls[x],fa[ls[x]] = Rc;
}
6.Find_Kth 操作 (获取第K大点)
也类似二叉排序树:
ll Find(RG ll rt,RG ll K){
if(sz[ls[rt]] == K-1)return rt;
else if(sz[ls[rt]] >= K && ls[rt])return Find(ls[rt],K);
else return Find(rs[rt],K-sz[ls[rt]]-1); //注意要减1(自己)
}
Kth = Find(root,K);
7.Insert_Kth 操作 (将节点插入序列第K个位置)
注意一下return判断的方式即可。 以把x节点插入到第K个位置为例。
void Ins_Kth(RG ll rt,RG ll nw,RG ll K){
if(K == 1 && !ls[rt]){ls[rt]=nw; fa[nw]=rt; return;}
if(K == 2+sz[ls[rt]] && !rs[rt]){rs[rt]=nw; fa[nw]=rt; return;}
if(K<=1+sz[ls[rt]])Ins_Kth(ls[rt],nw,K);
else Ins_Kth(rs[rt],nw,K-1-sz[ls[rt]]); //注意减1(自己)
}
8.PushUp操作 (维护区间信息)
类似线段树的PushUp,以维护子树大小为例:
IL void PushUp(RG ll x){
sz[x] = 1;
if(ls[x])sz[x]+=sz[ls[x]];
if(rs[x])sz[x]+=sz[rs[x]]; return;
}
这里归纳一下需要PushUp的地方(当然条件为相关函数存在)。
<1>.Rotate<以右旋为例>:
IL void Right_Rotate(RG ll x){
RG ll y = fa[x],z = fa[y];
......
PushUp(y); PushUp(x); return; //请注意PushUp的顺序
}
<2>.Insert:
void Insert(RG ll rt,RG ll nw){
//cout<<rt<<" "<<" "<<nw<<endl;
if(val[rt]>val[nw] && !ls[rt]){... Update(rt); return;}
else if(val[rt]>val[nw] && ls[rt]){... Update(rt); return;}
else if(val[rt]<val[nw] && !rs[rt]){... Update(rt); return;}
else if(val[rt]<val[nw] && rs[rt]){... Update(rt); return;}
}
//把x节点插入: Insert(root,x);
<3>Insert_Kth 、Insert_Sequence:与Insert类似
<4>Delete:
IL void Delete(RG ll x){
Splay(x,0);
if(!ls[x] && !rs[x]){root = 0; return;}
if(!ls[x] && rs[x]){fa[rs[x]] = 0; root = rs[x]; return;}
if(!rs[x] && ls[x]){fa[ls[x]] = 0; root = ls[x]; return;}
......
root = Rc; PushUp(Rc); Splay(Rc); return;
}
9.Merge 操作 (将两个子树合并)
用并查集维护是否需要合并,如果需要那么暴力启发式合并即可。
每个节点最多合并复杂度为logN<执行一次Insert操作>,所以总复杂度为N*logN是正确的。
以将 x1所在树 合并到 x2所在树为例。
其中bzj[i]为并查集数组,FindRoot为并查集操作。
void Merge_Visit(RG ll rt,RG ll x2){
Insert(x2,rt); //将rt节点插入到 x2所在树中
if(ls[rt])Merge_Visit(ls[rt],x2);
if(rs[rt])Merge_Visit(rs[rt],x2);
}
void Merge_Start(RG ll x1,RG ll x2){
Splay(x1,0); Splay(x2,0);
if(sz[x1] > sz[x2])swap(x1,x2); //x1 ----> x2,x1尽量小。
Merge_Visit(x1,x2);
return;
}
IL void Merge(){
RG ll x = gi(),y = gi();
RG ll f1 = FindRoot(x),f2 = FindRoot(y);
if(f1 != f2){ Merge_Start(x,y); bzj[f1]=f2; }
return;
}
10.CoutAns 操作 (输出整个序列)
理解Splay原理后很容易知道,当前序列即为树的中序遍历。所以中序遍历一遍即可。
void CoutAns(RG ll rt){
if(ls[rt])CoutAns(ls[rt]);
printf("%lld ",rt);
if(rs[rt])CoutAns(rs[rt]);
}
//CoutAns(root);
*、
11.GetSequence 操作(获得一段序列操作)
以提取[ a , b ]为例。
一篇博客:http://blog.51cto.com/sbp810050504/1029553
注意一下上面那个博客中,把b+1提到a-1的儿子说的太复杂了,直接Splay(b+1,a-1)即可。
那么这里还是来概括一下具体步骤。
<1>Splay(a-1 , 0);
<2>Splay(b+1 , a-1 ) ;
<3>所求区间即为ls[ b+1 ]所在子树。
<4>可以以ls[b+1]为根,把一个区间当做一个点,进行移动,删除等操作。
这里有一个问题,a == 1 与 b == N 时会有鬼( T_T )。
处理方案有两个(这里Ans 表示提取的区间树的根节点):
【1】讨论思想(弱智思路无解释):
IL void GetSequence(){
RG ll a = gi() , b = gi();
RG ll L = Find(root,a) , R = Find(root,b);
RG ll Min = GetMin(),Max = GetMax();
RG ll Ro = Lo = GetPre(L),GetNext(R);
if(L== Min && R == Max){Ans = root; return;}
if(L == Min){Splay(Ro,0); Ans = ls[root]; return;}
if(R == Max){Splay(Lo,0); Ans = rs[root]; return;}
else {Splay(Lo,0); Splay(Ro,Lo); Ans = ls[Ro]; return;}
}
这里极其不推荐这种方法,这种方法会导致常数超级巨大,建议使用下面那种方法。
【2】虚点思想:
建立两个虚点,N+1表示最右端的那个点,N+2表示最左端那个点。
注意一下根据排序原则把val[N+1] = INF,val[N+2] = -INF确保其位置不变 。
对于PushUp等操作,把它当做正常点做就行了。
那么注意需要改变的是:
<1>Find(root,K)
应该为 Find(root,K+1)
<2>CoutAns(root)
时,如果rt == (N+1 || N+2) 则不输出。
IL void GetSequence(){
RG ll a = gi() , b = gi();
RG ll L = Find(root,a) , R = Find(root,b+2);
// Find(root,a-1) ⇒ Find(root,a) ; Find(root,b+1) ⇒ Find(root,b+2) ;
Splay(L,0);Splay(R,L);
Ans = ls[R] return;
}
void CoutAns(RG ll rt){
if(ls[rt])CoutAns(ls[rt]);
if(rt != N+1 && rt != N+2)printf("%lld ",rt);
if(rs[rt])CoutAns(rs[rt]); return;
}
int main(){
......
val[N+1] = INF Insert(root,N+1);
val[N+2] = -INF; Insert(root,N+2);
......
}
12.Insert_Sequence 操作 (将序列插入现序列的K位置)
以把序列插入原来序列的K位置为例。
通过GetSequence , 得到 待插入序列 的根Rot;
<1>Rot = GetMin(Rot);
<2>Insert_Kth( root , Rot , K );
代码略,其实就是两个步骤合起来。
13.PushDown 操作 (维护’线段树’Lazy下放标记)
与线段树类似,但是一定要注意该PushDown的位置一定要PushDown。
这里以维护区间翻转问题为例,dt[i]为lazy标记,主动修改lazy的 视题目而定函数 略。
【0】PushDown函数:
IL void PushDown(RG ll x){
RG ll rson = rs[x],lson = ls[x];
dt[rson] ^= 1; dt[lson] ^= 1; dt[x] = 0;
ls[x] = rson; rs[x] = lson; return;
}
//
那么需要PushDown的部分有:
【1】Splay :
IL void Splay(RG ll x,RG ll tarfa){
RG ll tt = x,cnt = 0;
while(tt != tarfa){tmp[++cnt] = tt; tt = fa[tt];}
while(cnt--)if(dt[tmp[cnt]])PushDown(tmp[cnt]);
while(fa[x] != tarfa){
......
}return;
}
具体来说,在splay的while之前,先:
<1>把splay向上翻转路径上的点都抠出来。
<2>对于这条路径,从上往下先把lazy标记都放了。
这样在Rotate时,就排除了lazy标记的干扰。
.
【2】Find:
ll Find(RG ll rt,RG ll K){
if(dt[rt])PushDown(rt);
......
}
【3】CoutAns:
void CoutAns(RG ll rt){
if(dt[rt])PushDown(rt);
......
}
【4】(!) GetMin、GetMax;
在跳转之前先要PushDown!! (最易错的地方); 以GetMax为例:
IL ll GetMax(){
RG ll t = root;
while(1){
if(dt[t])PushDown(t);
if(rs[t])t=rs[t]; else break;
} return t;
}
注:、GetPre、GetNext操作由于需要进行Splay,所以不需要。
【5】Insert,Insert_Kth,Insert_Sequence:
以Insert函数为例:
void Insert(RG ll rt,RG ll nw){
if(dt[rt])PushDown(rt);
......
}
//把x节点插入: Insert(root,x);
【6】视题目而定,反正能PushDown一下就PushDown一下。
14.Delete_UpK 操作 (删除大于或小于K的所有数)
以删除小于K的所有数为例:
IL void Delete_DnK(){
RG ll Mix = K-1; if(!root)return; //记得减1,确保 ==K 不会被剪出去
fa[++cnt] = ls[cnt] = rs[cnt] = 0; val[cnt] = Mix;
Insert(root,cnt);
pre = GetPre(cnt);
if(!pre){root = 0; return;}
Splay(pre); rs[pre] = 0;
}
具体来说,步骤为:
<1> 向树中插入一个大小为K-1的节点
<2> 将这个节点Splay到根。
<3>找到这个点的前驱pre,Splay pre到根节点
<4>此时pre右侧的所有点即为小于等于K-1的点,直接剪掉右子树(包含新增点)即可。
15.Same 操作 (当操作涉及重复元素)
重复元素时,我们只需对于每一个点记录一个sum值,表示这个点的对应点数。
但是注意要标记每个点的映射关系,初始blg[i]=i,用处下文讲;
具体来说,
<1>Insert类操作:
void Insert(RG ll rt,RG ll nw){
if(val[rt] == val[nw]){sum[rt]++; blg[nw] = rt;return;}
.....
}
<2>Find类操作:
ll Find(RG ll rt,RG ll K){
if(sz[ls[rt]]+1 <= K && K <= sz[ls[rt]]+sum[rt])return rt;
if(sz[ls[rt]] >= K)return Find(ls[rt],K);
else return Find(rs[rt],K-sum[rt]-sz[ls[rt]]);
}
然后是blg的作用:当你插入一个点后要操作时,请注意对应关系。
下面给一个例子:
Insert(root,x); Splay(x);
这个是会出问题的。因为如果有重复点,那么你只是把一个点Splay了。
正确做法是这样:
Insert(root,x); ll t = (blg[x]==x)?(x:blg[x]); Splay(t);
16.极简代码:
IL void Rot(int x){
RG int y = fa[x],z = fa[y],c = Son(x);
if(z)ch[z][Son(y)] = x; fa[x] = z;
ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
ch[x][!c] = y; fa[y] = x; PushUp(y);
}
IL void Splay(int x){
//PushDown:
RG int top = 0; S[++top] = x;
for(RG int i = x; fa[i]; i = fa[i])S[++top] = fa[i];
while(top)PushDown(S[top--]);
//Splay:
for(RG int y = fa[x]; fa[x]; Rot(x) , y = fa[x])
if(z) Son(x) ^ Son(y) ? Rot(x) : Rot(y);
PushUp(x);
}
嗯,就这么多吧。
一些练习的题目:
- P3224 [HNOI2012]永无乡 https://www.luogu.org/problemnew/show/P3224
- P3391 文艺平衡树(Splay)https://www.luogu.org/problemnew/show/P3391
- P1486 郁闷的出纳员 https://www.luogu.org/problemnew/show/P1486
- P2596 [ZJOI2006]书架 https://www.luogu.org/problemnew/show/P2596