平衡树(Splay)
作者:互联网
平衡树
不同平衡树
有许多不同的平衡树
如:替罪羊树,AVI
,红黑树,Treap
,FHQ-Treap
(无旋Treap
),Splay
,SBT
等
其中比较重点的是上述后四种
目前只学习了 Splay
和 Treap
能够较为熟练的打出来的只有 Splay
有关 Splay
代码 (luoguP3396) :
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
#define size six
#define next nex
const int N=2e5+5;
int n;
int size[N],son[N][2],f[N],cnt[N],val[N],tot,rt;
struct Splay{
inline void maintain(int x){
size[x]=size[son[x][0]]+size[son[x][1]]+cnt[x];
}
inline bool get(int x){
return x==son[f[x]][1];
}
inline void rotate(int x){
int y=f[x],z=f[y],k=get(x),w=son[x][k^1];
son[y][k]=w,f[w]=y;
son[z][get(y)]=x,f[x]=z;
son[x][k^1]=y,f[y]=x;
maintain(y),maintain(x);
}
inline void splay(int x,int pos){
while(f[x]!=pos){
int y=f[x],z=f[y];
if(z!=pos){
if(get(x)==get(y)) rotate(y);
else rotate(x);
}
rotate(x);
}
if(pos==0) rt=x;
}
inline void insert(int x){
int cur=rt,p=0;
for(;cur&&val[cur]!=x;p=cur,cur=son[cur][x>val[cur]]);
if(cur) ++cnt[cur];
else{
cur=++tot;
if(p) son[p][x>val[p]]=cur;
son[cur][0]=son[cur][1]=0;
f[cur]=p,val[cur]=x;
cnt[cur]=size[cur]=1;
}
splay(cur,0);
}
inline void find(int x){
int cur=rt;
for(;son[cur][x>val[cur]]&&x!=val[cur];cur=son[cur][x>val[cur]]);
splay(cur,0);
}
inline int xth(int x){
int cur=rt;
while(1){
if(son[cur][0]&&x<=size[son[cur][0]]) cur=son[cur][0];
else if(x>size[son[cur][0]]+cnt[cur]){
x-=size[son[cur][0]]+cnt[cur];
cur=son[cur][1];
}
else{
splay(cur,0);
return cur;
}
}
}
inline int pre(int x){
find(x);
if(val[rt]<x) return rt;
int cur=son[rt][0];
while(son[cur][1]) cur=son[cur][1];
return cur;
}
inline int next(int x){
find(x);
if(val[rt]>x) return rt;
int cur=son[rt][1];
while(son[cur][0]) cur=son[cur][0];
return cur;
}
inline void del(int x){
int pr=pre(x),suc=next(x);
splay(pr,0),splay(suc,pr);
int y=son[suc][0];
if(cnt[y]>1){
--cnt[y];
splay(y,0);
}
else son[suc][0]=0;
splay(suc,0);
}
}t;
signed main(){
n=read();
t.insert(100000000);
t.insert(-100000000);
while(n--){
int op=read(),x=read();
if(op==1) t.insert(x);
if(op==2) t.del(x);
if(op==3){
t.find(x);
printf("%d\n",size[son[rt][0]]);
}
if(op==4) printf("%d\n",val[t.xth(x+1)]);
if(op==5) printf("%d\n",val[t.pre(x)]);
if(op==6) printf("%d\n",val[t.next(x)]);
}
}
当然,之后可能会采用结构体的写法
Splay
是平衡树,它是通过 \(splay\) (伸展) 这一操作来维持平衡的
\(splay(x,pos)\) 表示通过不断 \(rotate\) 把 \(x\) 节点旋转到 \(pos\) 节点的儿子处
\(splay(x,0)\) 表示把 \(x\) 旋转到根
这样我们可以通过 \(splay(pre(val),0)\) \(splay(next(val),pre(val))\) 等一系列操作更方便的执行复杂的插入与修改
具体会在题目中阐述
Splay
有两点重要的地方:
- 它可以维护序列
- 它是
LCT(Link Cut Tree)
的基础
个人比较喜欢 Splay
的原因:好学,方便,比 Treap
运用的范围更广
当然 FHQ-Treap
貌似也能维护序列 (可我不会)
Splay
习题
P3391 文艺平衡树
题意:
给你一个长度为 \(n\) 的序列,序列的第 \(i\) 项初始为 \(i\)
会进行 \(m\) 次翻转序列中区间 \([l,r]\) 的操作
最后输出序列
满足 \(1\leq n,m\leq 10^5\)
思路:
将序列每个点的位置存入平衡树中 (这样它的中序遍历就是最后的序列)
上述操作可以 \(insert\) 也可以递归建树 (像线段树一样)
翻转区间 \([l,r]\) 时我们 \(splay(l-1,0)\) \(splay(r+1,l-1)\)
这样区间 \([l,r]\) 就在 \(r+1\) 的左子树中
然后将 \(r+1\) 的左子树中所有点的左右子树交换就行
但是一次全交换会 T
飞
所以考虑像线段树一样整一个翻转的 \(lazytag\)
同时在每次访问前 \(pushdown\) 更新
这样就可以保证复杂度
P2234 营业额统计
题意:
有 \(n\) 天,告诉你每天的营业额 \(a_i\)
我们定义,一天的最小波动值 = \(\min\{|\text{该天以前某一天的营业额}-\text{该天营业额}|\}\)
特别地,第一天的最小波动值为第一天的营业额
求最小波动值之和
思路:
没什么特别,Splay
P1486 郁闷的出纳员
题意:
给你 \(n\) 条命令,\(\min\) 的值 (工资下界)
-
I k
新建一个工资档案,初始工资为 \(k\)。如果某员工的初始工资低于工资下界,他将立刻离开公司。 -
A k
把每位员工的工资加上 \(k\) 。 -
S k
把每位员工的工资扣除 \(k\)。 -
F k
查询第 \(k\) 多的工资。
在初始时,可以认为公司里一个员工也没有。
- \(0 \leq n \leq 3 \times 10^5\),\(0 \leq \text{min} \leq 10^9\)
思路:
考虑到维护 A
操作比较复杂
搞一个增加值 \(delta\) 表示当前已经加过 \(delta\) 这么多工资
每次插入 \(x\) 的时候 \(insert(x-delta)\)
这样树内的每个值 \(val\) 实际上是 \(val+delta\)
由于需要删掉每个小于 \(min\) 的节点
每次删除操作时把大于等于 \(min-delta\) 的最小值 \(splay\) 到根,然后把根的左儿子变成 \(0\)
这样就完成了删除操作
注意这里调用 \(next(min-delta)\) 就要把 \(>\) 改成 \(\ge\),或者是调用 \(next(min-delta-1)\)
P2286 宠物收养场
题意:
凡凡开了一间宠物收养场。收养场提供两种服务:收养被主人遗弃的宠物和让新的主人领养这些宠物。
被遗弃的宠物过多时,假若到来一个领养者,这个领养者希望领养的宠物的特点值为 \(a\),那么它将会领养一只目前未被领养的宠物中特点值最接近 \(a\) 的一只宠物。
(任何两只宠物的特点值都不可能是相同的,任何两个领养者的希望领养宠物的特点值也不可能是一样的)
如果存在两只宠物他们的特点值分别为 \(a-b\) 和 \(a+b\),那么领养者将会领养特点值为 \(a-b\) 的那只宠物。
收养宠物的人过多,假若到来一只被收养的宠物,能够领养它的领养者,是那个希望被领养宠物的特点值最接近该宠物特点值的领养者。
如果该宠物的特点值为a,存在两个领养者他们希望领养宠物的特点值分别为 \(a-b\) 和 \(a+b\),那么特点值为 \(a-b\) 的那个领养者将成功领养该宠物。
一个领养者领养了一个特点值为 \(a\) 的宠物,而它本身希望领养的宠物的特点值为 \(b\),那么这个领养者的不满意程度为 \(|a-b|\)。
给你 \(n\) 个 领养者和被收养宠物到来收养所的情况,请你计算所有收养了宠物的领养者的不满意程度的总和。
初始时,收养所里面既没有宠物,也没有领养者。
\(0<n\leq80000\)
思路:
维护一棵 Splay
以及当前树内是宠物还是领养者
找前驱后继,若前驱后继都行则选前驱,然后删掉就行
P3850 书架
题意:
有一个书架上有 \(N\) 本书,给你 \(M\) 本书以及要插入的位置 \(x\),将其插进去
\(Q\) 次询问,每次问你第 \(k\) 本书的名字
\(1 \leqslant N \leqslant 200\), \(1 \leqslant M \leqslant 10^5\), \(1 \leqslant Q \leqslant 10^4\)
思路:
Splay
维护位置
每次插入到位置 \(x\) 就找到在树中排名为 \(x\) 的位置 \(a\) 并 \(splay(a,0)\)
找到在树中排名为 \(x-1\) 的位置 \(b\) 并 \(splay(b,a)\)
这样 \(b\) 的右儿子为空,我们让 \(x\) 为 \(b\) 的右儿子就完成了插入
每次查找直接问就行 (因为树中的排名就表示序列中的排名)
P3586 LOG
题意:
维护一个长度为 \(n\) 的序列,一开始都是 \(0\),支持以下两种操作:
U k a
将序列中第 \(k\) 个数修改为 \(a\)。Z c s
在这个序列上,每次选出 \(c\) 个正数,并将它们都减去 \(1\),询问能否进行 \(s\) 次操作。
每次询问独立,即每次询问不会对序列进行修改。
思路:
操作 \(1\) 很容易
对于操作 \(2\) 分为大于等于 \(s\) 的数和小于 \(s\) 的数,让它们总贡献大于等于 \(c\cdot s\) 就行
假设大于等于 \(s\) 的数有 \(x\) 个,那么它们的贡献就是 \(x\cdot s\) (每一个数的贡献至多为 \(s\) )
而小于 \(s\) 的数的贡献就是它们的和 \(Sum\)
所以能进行 \(s\) 次操作的条件是: \(x\cdot s+Sum\ge c\cdot s\)
也就是 \(Sum\ge (c-x)\cdot s\)
因此在 Splay
中 \(maintain\) 的时候多维护一个东西 \(sum\) (这个东西的维护方式就和 \(size\) 一样)
设 \(pre(s)=y\),每次查询的时候 \(splay(y,0)\),那么就可以计算 \(Sum\) 和 \(x\) 了
\(Sum=sum[p]-sum[son[p][1]]\)
\(x=size[son[p][1]]]\)
注意:代码中 \(x=size[son[p][1]]-1\) 因为最开始插入了一个极大值防止越界
标签:splay,宠物,cur,val,领养,son,Splay,平衡 来源: https://www.cnblogs.com/into-qwq/p/16437085.html