平衡树基础
这么久了发现平衡树还不会背所以来复习一下。
首先你得知道什么是二叉搜索树(BST)。它是一棵二叉树,且对于任意节点都满足左儿子权值小于该节点,右儿子权值大于该节点。
我们发现普通的二叉搜索树单次查询的最坏复杂度是 \(O(n)\) 的(比如下面这张盗的图):
所以我们有几种平衡树来解决这个问题,把查询的复杂度变成 \(\Theta(\log n)\) 。
Treap(有旋)
Treap,是Tree和Heap拼起来的一个词。所以它既满足BST的性质又满足堆的性质。具体地,它的点权满足上面的BST性质,还使得每个节点的所有子节点权值都比该节点小。
在随机情况下,BST的树高是 \(\log n\) 级别的。所以我们随机满足堆性质的点权,就可以使它比较平衡。
我们把几个核心操作分开。
节点
struct tree{
int son[2],val,data,size,cnt;
//son左右儿子 val权值 data堆的权值 size子树大小 cnt相同权值的节点个数
}tree[1000010];
int New(int x){//新建节点
tree[++t].val=x;
tree[t].data=rand();
tree[t].size=1;tree[t].cnt=1;
return t;
}
void pushup(int x){
tree[x].size=tree[tree[x].son[0]].size+tree[tree[x].son[1]].size+tree[x].cnt;//计算子树大小
}
初始化
初始化的时候我们插入两个节点, \(\infty,-\infty\) ,来防止访问到不存在的地址导致RE。
void build(){
rt=New(-1000000000);
tree[rt].son[1]=New(1000000000);
pushup(rt);
}
旋转
旋转是在不影响树的性质的前提下,调整节点相互关系的一个操作,分左旋和右旋。左旋一个节点就是把这个节点旋转成左儿子,然后把右儿子提上来。右旋就是反过来。盗个图。(oiwiki的图画的真好)
通过这个我们发现,旋转(以左旋为例)有如下几个操作:
- 将该节点的右儿子变成原先右儿子的左儿子(2的右儿子变成4)
- 将该节点右儿子的左儿子变成该节点(3的右儿子变成2,即旋转上来)
- 将原来该节点地址的位置变成右儿子
如果是右旋那就左右翻转一下就行了。于是我们可以将两个旋转的代码放到一起写:(反正我觉得不好背,理解万岁)
void rotate(int &x,int d){//0左旋1右旋
int tmp=tree[x].son[d^1];
tree[x].son[d^1]=tree[tmp].son[d];
tree[tmp].son[d]=x;
x=tmp;
pushup(tree[x].son[d]);pushup(x);
}
插入
和普通的BST插入没什么区别,加一个旋转就行。
void ins(int &x,int val){
if(!x){
x=New(val);return;//没有查到节点就新建一个
}
if(val==tree[x].val)tree[x].cnt++;//有就cnt++
else{
int d=0;
if(val>=tree[x].val)d=1;//找到是在哪一棵子树
ins(tree[x].son[d],val);//递归插入
if(tree[x].data<tree[tree[x].son[d]].data)rotate(x,d^1);
//插入的那一棵子树的权值会发生变化 如果不满足堆性质就把它转过来(左儿子右旋,右儿子左旋)
}
pushup(x);//更新其他信息
}
删除
同样的,我们找到这个节点之后把它旋转到最底下删掉。注意旋转的时候仍然要保证堆的性质。
void del(int &x,int val){
if(!x)return;
if(val==tree[x].val){//找到
if(tree[x].cnt>1){
tree[x].cnt--;pushup(x);
return;
}
if(tree[x].son[0]||tree[x].son[1]){//如果有儿子就把它转下去
if(!tree[x].son[1]||tree[tree[x].son[0]].data>tree[tree[x].son[1]].data){
//我们让堆权值更大的一个儿子转上来然后递归到另一边删除
rotate(x,1);del(tree[x].son[1],val);
}
else rotate(x,0);del(tree[x].son[0],val);
pushup(x);
}
else x=0;//没有儿子直接删掉
return;
}
if(val<tree[x].val)del(tree[x].son[0],val);//查找在哪棵子树
else del(tree[x].son[1],val);
pushup(x);
}
查询某个数的排名(定义为比当前数小的个数+1)
这个也可以直接在二叉树上二分递归找就行。
int getrank(int x,int val){//x的子树内val的排名
if(!x)return 0;//没有直接返回
if(val==tree[x].val)return tree[tree[x].son[0]].size+1;//找到返回
else if(val<tree[x].val)return getrank(tree[x].son[0],val);//查左儿子
else return tree[tree[x].son[0]].size+tree[x].cnt+getrank(tree[x].son[1],val);
//查右儿子 因为所有左儿子和当前节点都比右儿子小所以加上它们的大小
}
getrank(rt,x)-1;//我们一开始插入了-inf所以要减掉
查询排名为x的数
这个也是类似的。
int getval(int x,int k){
if(!x)return 0;//直接返回
if(k<=tree[tree[x].son[0]].size)return getval(tree[x].son[0],k);//找左儿子
else if(k<=tree[tree[x].son[0]].size+tree[x].cnt)return tree[x].val;//找到了
else return getval(tree[x].son[1],k-tree[tree[x].son[0]].size-tree[x].cnt);//找右儿子 注意减掉左儿子和该节点的大小
}
getval(rt,x+1);//同理 因为我们一开始插入了-inf所以要找多一名
查询前驱/后继
int pre(int val){
int x=rt,pre;
while(x){
if(tree[x].val<val){//比要求的值小 找右儿子 更新答案为该节点
pre=tree[x].val;x=tree[x].son[1];
}
else x=tree[x].son[0];//比要求的值大 不能更新前驱 找左儿子
}
return pre;
}
int nxt(int val){
int x=rt,nxt;
while(x){
if(val<tree[x].val){
nxt=tree[x].val;x=tree[x].son[0];
}
else x=tree[x].son[1];
}
return nxt;
}
于是我们就可以切掉普通平衡树了。(就只写主函数了,不想把战线拉那么长)
int main(){
build();
int n;scanf("%d",&n);
while(n--){
int od,x;scanf("%d%d",&od,&x);
if(od==1)ins(rt,x);
else if(od==2)del(rt,x);
else if(od==3)printf("%d\n",getrank(rt,x)-1);
else if(od==4)printf("%d\n",getval(rt,x+1));
else if(od==5)printf("%d\n",pre(x));
else if(od==6)printf("%d\n",nxt(x));
}
return 0;
}
Treap(无旋)
实际上有旋Treap能干的事情线段树都能干(但是常数差不少)。所以为了整点有旋Treap不能干的东西,我们有了一些其他的平衡树,比如无旋Treap。 而且码量相当小。
无旋Treap的核心操作有两个:分裂和合并。而且貌似不需要一开始插进去 \(\infty,-\infty\) 。
节点
struct node{
int son[2],val,size,data;
}tree[100010];//没怎么变 但是cnt没有了 因为无旋Treap处理重复是直接新加一个节点进去
void pushup(int x){
tree[x].size=tree[tree[x].son[0]].size+tree[tree[x].son[1]].size+1;
}
int New(int x){
tree[++t].size=1;tree[t].val=x;
tree[t].data=rand();
return t;
}
分裂
分裂操作将一棵Treap分成两部分,一部分所有节点值 \(\le val\) ,另一部分 \(> val\) 。还是盗图:
其实很简单,每次判断当前节点的值和 \(val\) 的大小关系决定划入哪一棵子树。如果小于等于,将根节点扔进第一棵子树,由于左子树全部小于等于该节点所以只需要递归右子树分裂。大于同理。
void split(int now,int val,int &x,int &y){//
if(!now){
x=0;y=0;return;//没有就返回
}
if(tree[now].val<=val){
x=now;split(tree[now].son[1],val,tree[now].son[1],y);//递归分裂右子树
}
else{
y=now;split(tree[now].son[0],val,x,tree[now].son[0]);//递归分裂左子树
}
pushup(now);
}
合并
合并操作将两棵Treap合并起来,其中一棵的所有节点权值小于等于另一棵。
我们可以顺序从根节点开始扫描两棵树,每次往新的树上合并一个节点,使其满足堆的性质(我写的小根堆,不要问我为什么有旋写的是大根堆)。与此同时,因为一棵的所有节点权值都小于另一棵,所以我们可以轻松地调整左右儿子,使其满足BST性质。
int merge(int x,int y){
if(!x)return y;
if(!y)return x;
if(tree[x].data<tree[y].data){
tree[x].son[1]=merge(tree[x].son[1],y);
pushup(x);//x<y y应是x的右儿子
return x;
}
else{
tree[y].son[0]=merge(x,tree[y].son[0]);
pushup(y);//x<y x应是y的左儿子
return y;
}
}
有了分裂和合并这两种操作,我们就可以用它们进行平衡树的一系列操作(当然你也可以用上面提到的那一套来搞)。
插入
假如我们要插入 \(x\) ,那么我们直接按照 \(x\) 分成两半,然后新建一个节点,合并三棵树就行了。
void ins(int val){
split(rt,val,x,y);
rt=merge(merge(x,New(val)),y);
}
删除
同样的按照 \(x\) 分成三棵子树然后把剩下两个合并起来。
void del(int val){
split(rt,val,x,z);
split(x,val-1,x,y);
y=merge(tree[y].son[0],tree[y].son[1]);
//可能有若干个相同的值 我们只去掉一个 所以合并左右子树 只去掉根
rt=merge(merge(x,y),z);
}
查询x的排名
直接根据 \(x-1\) 分裂两棵树,第一棵树的节点数就是比 \(x\) 小的节点数。
int getrank(int val){
split(rt,val-1,x,y);
int ans=tree[x].size+1;
rt=merge(x,y);
return ans;
}
查询排名为x的数
我懒了直接按照普通写法写的。当然你可以按照排名分裂成三棵树然后找到中间一个的值。
int getval(int x,int k){
if(!x)return 0;
if(k<=tree[tree[x].son[0]].size)return getval(tree[x].son[0],k);
else if(k==tree[tree[x].son[0]].size+1)return tree[x].val;
else return getval(tree[x].son[1],k-tree[tree[x].son[0]].size-1);
}
查询前驱/后继
仍然分裂,前驱就按 \(x-1\) 分开然后找最大,后继就按 \(x\) 分开然后找最小。
int getpre(int val){
split(rt,val-1,x,y);
int ans=getval(x,tree[x].size);
merge(x,y);
return ans;
}
int getnxt(int val){
split(rt,val,x,y);
int ans=getval(y,1);
merge(x,y);
return ans;
}
于是我们又多了一份普通平衡树的代码。
好了说了这么半天它除了有旋Treap能干的以外它能干什么?它可以维护区间信息,一会再说。
Splay
Splay也是一种平衡树,它通过将某个节点不断旋转到根节点来保证平衡。
仍然一个一个来。
节点
int rt,t;
struct node{
int son[2],fa,val,cnt,size;
//多存一个父亲
}tree[100010];
bool get(int x){
return x==tree[tree[x].fa].son[1];
}//判断是哪个儿子
void clear(int x){
tree[x].son[0]=tree[x].son[1]=tree[x].fa=tree[x].val=tree[x].size=tree[x].cnt=0;
}
void pushup(int x){
tree[x].size=tree[tree[x].son[0]].size+tree[tree[x].son[1]].size+tree[x].cnt;
}
旋转
这个旋转和有旋Treap的旋转是一样的,类比即可。然后这里的旋转是把一个节点旋转上去不是旋转下来,所以写法有一点点小区别。(有一说一把图背住手模几遍比什么都强)
void rotate(int x){
int y=tree[x].fa,z=tree[y].fa;
int tmp=get(x);
tree[y].son[tmp]=tree[x].son[tmp^1];
tree[tree[x].son[tmp^1]].fa=y;
tree[x].son[tmp^1]=y;
tree[y].fa=x;tree[x].fa=z;
if(z)tree[z].son[y==tree[z].son[1]]=x;
pushup(y);pushup(x);
}
Splay
在Splay中,规定每访问一个节点都要把它旋转到根节点。这样对于要旋转的节点 \(x\) 就有六种情况讨论:
- \(x\) 的父亲是根,直接旋转 \(x\) 即可。
- \(x\) 的父亲和 \(x\) 的儿子类型相同(都是左/右儿子),则先转父亲后转儿子,可以保证这三个节点仍然是一条链的形态。
- \(x\) 的父亲和 \(x\) 的儿子类型不同,直接转就行。
实际上就只是三点共线的时候先转父亲再转儿子,别的时候转就完了。
void splay(int x){
for(int i=tree[x].fa;i=tree[x].fa,i;rotate(x)){
if(tree[i].fa){
if(get(x)==get(i))rotate(i);
else rotate(x);
}
rt=x;
}
}
插入
由于Splay要维护父亲,所以递归写起来不是很方便,于是写了非递归的版本。
void ins(int val){
if(!rt){
tree[++t].val=val;
tree[t].cnt++;
rt=t;
pushup(rt);
return;
}//节点为空 直接插入
int tmp=rt,fa=0;
while(1){
if(tree[tmp].val==val){//找到直接插入
tree[tmp].cnt++;
pushup(tmp);pushup(fa);
splay(tmp);//别忘了
break;
}
fa=tmp;tmp=tree[tmp].son[tree[tmp].val<val];
if(!tmp){//新建一个
tree[++t].val=val;tree[t].cnt++;
tree[t].fa=fa;
tree[fa].son[tree[fa].val<val]=t;
pushup(t);pushup(fa);
splay(t);//别忘了
break;
}
}
}
查询x的排名
这个其实也可以按照别的一样写。
int getrank(int val){
int x=0,tmp=rt;
while(1){
if(val<tree[tmp].val)tmp=tree[tmp].son[0];
else{
x+=tree[tree[tmp].son[0]].size;
if(val==tree[tmp].val)return x+1;
x+=tree[tmp].cnt;
tmp=tree[tmp].son[1];
}
}
}
查询排名为x的数
同理。
int getval(int k){
int tmp=rt;
while(1){
if(tree[tmp].son[0]&&k<=tree[tree[tmp].son[0]].size)tmp=tree[tmp].son[0];
else{
k-=tree[tmp].cnt+tree[tree[tmp].son[0]].size;
if(k<=0)return tree[tmp].val;
tmp=tree[tmp].son[1];
}
}
}
查询前驱/后继
我们可以插入 \(x\) ,之后由于Splay操作,前驱就变成了左子树里最大的节点,后继是右子树里最小的节点。
int pre(){
int tmp=tree[rt].son[0];
if(!tmp)return tmp;
while(tree[tmp].son[1])tmp=tree[tmp].son[1];
splay(tmp);
return tmp;
}
int nxt(){
int tmp=tree[rt].son[1];
if(!tmp)return tmp;
while(tree[tmp].son[0])tmp=tree[tmp].son[0];
splay(tmp);
return tmp;
}
int getpre(int x){
ins(x);
int ans=tree[pre()].val;
del(x);
return ans;
}
int getnxt(int x){
ins(x);
int ans=tree[nxt()].val;
del(x);
return ans;
}
删除
首先将要删除的节点Splay到根。然后如果个数不为 \(1\) 则减掉,否则直接合并两棵子树。
如何合并?我们现在要合并两棵Splay,其中一棵所有节点权值小于另一棵。则我们可以不断将权值小的那一棵的最大值Splay到根,然后把它的右子树设置为另一棵树并更新节点信息。
void del(int val){
getrank(val);
if(tree[rt].cnt>1){
tree[rt].cnt--;pushup(rt);return;
}
if(!tree[rt].son[0]&&!tree[rt].son[1]){
clear(rt);rt=0;return;
}
if(!tree[rt].son[0]){
int tmp=rt;
rt=tree[rt].son[1];
tree[rt].fa=0;
clear(tmp);return;
}
if(!tree[rt].son[1]){
int tmp=rt;rt=tree[rt].son[0];
tree[rt].fa=0;clear(tmp);
return;
}
int tmp=rt;int x=pre();
tree[tree[tmp].son[1]].fa=x;
tree[x].son[1]=tree[tmp].son[1];
clear(tmp);
pushup(rt);
}
替罪羊树
讲解到时候再说,先上个代码。
#include <cstdio>
#include <algorithm>
#include <iostream>
#define lson tree[x].son[0]
#define rson tree[x].son[1]
using namespace std;
const double alpha=0.8;
struct node{
int val,son[2],cnt,s,size,sd;
}tree[100010];
int cnt,rt,a[100010];
void pushup(int x){
tree[x].s=tree[lson].s+tree[rson].s+1;
tree[x].size=tree[lson].size+tree[rson].size+tree[x].cnt;
tree[x].sd=tree[lson].sd+tree[rson].sd+(tree[x].cnt!=0);
}
bool judge(int x){
return tree[x].cnt&&
(alpha*tree[x].s<=(double)max(tree[lson].s,tree[rson].s)
||(double)tree[x].sd<=alpha*tree[x].s);
}
void flat(int x,int &num){
if(!x)return;
flat(lson,num);
if(tree[x].cnt)a[num++]=x;
flat(rson,num);
}
int build(int l,int r){
if(l>=r)return 0;
int mid=(l+r)>>1;
tree[a[mid]].son[0]=build(l,mid);
tree[a[mid]].son[1]=build(mid+1,r);
pushup(a[mid]);
return a[mid];
}
void found(int &x){
int num=0;
flat(x,num);
x=build(0,num);
}
void ins(int &x,int val){
if(!x){
x=++cnt;
if(!rt)rt=1;
tree[x].val=val;lson=rson=0;
tree[x].cnt=tree[x].s=tree[x].size=tree[x].sd=1;
return;
}
if(tree[x].val==val)tree[x].cnt++;
else if(tree[x].val<val)ins(rson,val);
else ins(lson,val);
pushup(x);
if(judge(x))found(x);
}
void del(int &x,int val){
if(!x)return;
if(tree[x].val==val){
if(tree[x].cnt)tree[x].cnt--;
}
else{
if(tree[x].val<val)del(rson,val);
else del(lson,val);
}
pushup(x);
if(judge(x))found(x);
}
int getrank(int x,int val){
if(!x)return 0;
else if(tree[x].val==val&&tree[x].cnt)return tree[lson].size;
else if(val<tree[x].val)return getrank(lson,val);
else return tree[lson].size+tree[x].cnt+getrank(rson,val);
}
int getrank2(int x,int val){
if(!x)return 1;
else if(tree[x].val==val&&tree[x].cnt)return tree[lson].size+tree[x].cnt+1;
else if(val<tree[x].val)return getrank2(lson,val);
else return tree[lson].size+tree[x].cnt+getrank2(rson,val);
}
int getval(int x,int k){
if(!x)return 0;
else if(tree[lson].size<k&&k<=tree[lson].size+tree[x].cnt)return tree[x].val;
else if(k<=tree[lson].size)return getval(lson,k);
else return getval(rson,k-tree[lson].size-tree[x].cnt);
}
int pre(int val){
return getval(rt,getrank(rt,val));
}
int nxt(int val){
return getval(rt,getrank2(rt,val));
}
int main(){
int n;scanf("%d",&n);
while(n--){
int od,x;scanf("%d%d",&od,&x);
if(od==1)ins(rt,x);
else if(od==2)del(rt,x);
else if(od==3)printf("%d\n",getrank(rt,x)+1);
else if(od==4)printf("%d\n",getval(rt,x));
else if(od==5)printf("%d\n",pre(x));
else printf("%d\n",nxt(x));
}
return 0;
}
就完了。别的到时候再补。