非旋转Treap:用运行时间换调试时间的有效手段
Hello大家好,我们今天来聊一聊非旋转Treap。
相信各位或多或少都做过些序列上的问题。如果水题我们考虑暴力;不强制在线我们可能用过莫队和待修改莫队;不更改序列上的时间戳信息的我们使用线段树或者树状数组,也有可能请出主席树。那如果更大幅度的操作,我们要用到splay或者块链。但是!我们今天介绍一种特殊的平衡树,用时间换取时间的数据结构。
啥意思?如果写过splay和块链,前者的调试时间和后者的敲版子时间让我们头痛欲裂,但是这个平衡树,好写好调,省去了时间。但是常数相对较大,这便是我们的代价(好一个时间换时间)。
Treap就是Tree+heap,一个基于堆的二叉搜索树,但是它的维护过程是仰仗于旋转的。而我们今天讲的非旋转Treap,顾名思义,不需要旋转的哦。
那应该如何维护平衡呢?用两个基本操作即可:撕裂&合并。
撕裂??考虑区间最本质的:如果我们可以将要修改的区间成为一棵单独的树,我们是不是想干什么就干什么!整体操作就打标记,特殊操作就特殊处理。我们也想要这样怎么办?引入一个split操作,就是断开原来BST上的一条边,整个BST变成了两棵树,返回一个pair,pair里存的是两棵新BST的根。如果想要提取一个区间,就split两次,中间的子树就是我们要的区间子树。 附上版子(为了方便理解,我每个语句都是一行):
// split函数的pair存的是两个新BST的两个根,时间戳小的在first,时间戳大的在second
pair split(int pos,int num) //表示在以pos为根节点的BST中,取出前num个作为新的BST,剩下的作为另一棵BST
{
if(num==0)//递归退出条件,如果在一个pos的BST取出前0个,我们就返回一个(0,pos)的pair。
{
return make_pair(0,pos);
}
int lson=a[pos].ls;
int rson=a[pos].rs;
// 这里我们分类讨论
if(num==a[lson].size) // 如果恰好是左子树的大小,我们将左子树断开。
{
a[pos].ls=0;//将左子树和根节点之间的路径断开
pushup(pos);//更新当前根节点信息。
return make_pair(lson,pos);//返回两个新子树的pair,这里返回的是lson而不是a[pos].ls是因为后者已经被更新成0.
}
// 后面的同理
else if(num==a[lson].size+1)
{
a[pos].rs=0;
pushup(pos);
return make_pair(pos,rson);
}
else if(num<a[lson].size)
{
Pair t=split(lson,num);
a[pos].ls=t.second;
pushup(pos);
return make_pair(t.first,pos);
}
else
{
// 这里我们需要注意:如果num>a[lson].size+1,说明我们分开的两棵子树中有一棵包含了pos的左子树和根节点。
Pair t=split(rson,num-a[lson].size-1); // 所以这里我们直接将右子树分成num-a[lson].size-1的两个子树。
a[pos].rs=t.first; // 然后将num-a[lson].size-1大小的子树和前面的树加在一起,这样就得到了大小为num的子树。
pushup(pos);
return make_pair(pos,t.second);
}
}
合并??如果我们将一棵单独的表示区间的BST处理完了,我们需要将两棵子树合并到一起啊!所以就有了这个merge操作。因为我们本质上维护平衡的办法就是利用随机数的权值来维护一个依据权值的堆,撕裂操作显然不会打破这个局势,但是如果我们瞎合并,就无法保证平衡,时间复杂度也就没有办法保证。所以我们想一个能维护两个子树的方法,即能保证堆的性质,也保留BST应该有的中序遍历时间戳递增。我们请出可并堆!显然,如果我们采用可并堆的合并方式,是可以达到我们预期的效果的。
可并堆是如何合并的呢?其实也非常简单。这样:因为我们是想维护基于随机权值val的堆!所以对于两棵BST,分别以x和y为根。其中,x的整体时间戳小于y。那么,如果val[x]大于val[y],那么x一定是他们两棵BST合并之后的根。因为val[x]是x所在子树里最大的,y是y所在子树里最大的,x还比y大,所以x是根。又因为,我们要维护Tree,所以我们递归地将x的右儿子和y合并即可。这样合并时候的新BST就满足我们的要求。 附上板子!
// merge 操作返回的是两棵BST合并后的新BST的根。
int merge(int x,int y)
{
if(x==0||y==0)
{
if(x==0) return y;
else return x;
}
// 如果x的权值大于y的权值,显然x是根。我们只需要让x的右儿子和y合并即可。
if(a[x].val_heap>a[y].val_heap)
{
a[x].rs=merge(a[x].rs,y);
pushup(x);
return x;
}
// 反之,如果y的权值大于x,显然y是根。所以我们让x和y的左儿子合并即可。
else
{
a[y].ls=merge(x,a[y].ls);
pushup(y);
return y;
}
}
说完了非旋转Treap的基本操作,相信各位对于这个神奇的平衡树有了一定的认识。它的其他操作有了split和merge后都变得非常简单,自己手玩即可。
例题时间
Bzoj 3223 文艺平衡树 (所有平衡树的入门题)
这道题非常经典,我们记录一下标记,每次merge和split之前pushdown一下即可。 趁着这个题看一下其他的操作哦。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 100100
#define mp make_pair
using namespace std;
typedef pair<int,int> par;
int ls[N],rs[N],size[N],key[N],val[N];
int n,m,root,tot;
bool lazy[N];
inline void update(int x)
{
size[x]=1;
if(ls[x])size[x]+=size[ls[x]];
if(rs[x])size[x]+=size[rs[x]];
}
inline void pushdown(int x)
{
if(!x||!lazy[x])return;
swap(ls[x],rs[x]);lazy[x]=0;
if(ls[x])lazy[ls[x]]^=1;
if(rs[x])lazy[rs[x]]^=1;
}
int lson,rson;
par split(int x,int k)
{
pushdown(x);
if(!k) return mp(0,x);
lson=ls[x],rson=rs[x];
par t;
if(k==size[ls[x]])
{
ls[x]=0;update(x);
return mp(lson,x);
}
else if(k==size[ls[x]]+1)
{
rs[x]=0;update(x);
return mp(x,rson);
}
else if(k<size[ls[x]])
{
t=split(lson,k);
ls[x]=t.second;update(x);
return mp(t.first,x);
}
t=split(rson,k-size[ls[x]]-1);
rs[x]=t.first;update(x);
return mp(x,t.second);
}
int merge(int x,int y)
{
if(!x||!y) return x|y;
pushdown(x);pushdown(y);
if(key[x]<key[y])
{
ls[y]=merge(x,ls[y]);update(y);
return y;
}
rs[x]=merge(rs[x],y);update(x);
return x;
}
void output(int x,int flag)
{
pushdown(x);
if(ls[x]) output(ls[x],0);
if(!rs[x]&&flag) printf("%d",val[x]);
else printf("%d ",val[x]);
if(rs[x])output(rs[x],flag);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
key[++tot]=rand();
val[tot]=i;
size[tot]=1;
root=merge(root,tot);
}
par t1,t2;
for(int x,y,i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
t2=split(root,y);t1=split(t2.first,x-1);
lazy[t1.second]^=1;
root=merge(merge(t1.first,t1.second),t2.second);
}
output(root,1);
return 0;
}
Bzoj 1861 书架
多了两个get_rank和get_id的操作。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 80010
#define mp make_pair
using namespace std;
typedef pair<int,int> par;
int n,m,root;
int ls[N],rs[N],size[N],key[N],fa[N];
inline void pushup(int x)
{
size[x]=size[ls[x]]+size[rs[x]]+1;
}
int merge(int x,int y)
{
if(!x||!y) return x|y;
if(key[x]>key[y])
{
rs[x]=merge(rs[x],y);fa[rs[x]]=x;
pushup(x); return x;
}
ls[y]=merge(x,ls[y]);fa[ls[y]]=y;
pushup(y); return y;
}
par split(int x,int k)
{
if(!k) return mp(0,x);
int lson=ls[x],rson=rs[x];
if(k==size[lson])
{
ls[x]=0;pushup(x);
fa[lson]=0;
return mp(lson,x);
}
if(k==size[lson]+1)
{
rs[x]=0;pushup(x);
fa[rson]=0;
return mp(x,rson);
}
if(k<size[lson])
{
par t=split(lson,k);
ls[x]=t.second;fa[ls[x]]=x;pushup(x);
return mp(t.first,x);
}
par t=split(rson,k-size[ls[x]]-1);
rs[x]=t.first;fa[rs[x]]=x;pushup(x);
return mp(x,t.second);
}
int getid(int x)
{
int flag=1,t=x,ans=0;
while(t)
{
if(flag)ans+=size[ls[t]]+1;
flag=(t==rs[fa[t]]);
t=fa[t];
}
return ans-1;
}
int find(int x)
{
x--;int t=root;
while(1)
{
if(x==size[ls[t]]) return t;
if(x<size[ls[t]]) t=ls[t];
else x-=size[ls[t]]+1,t=rs[t];
}
}
int main()
{
cin >> n >> m ;
for(int x,i=1;i<=n;i++)
{
scanf("%d",&x);
size[x]=1,key[x]=rand()*rand();
root=merge(root,x);
}
char flag[10];
for(int x,y,i=1;i<=m;i++)
{
scanf("%s",flag);
if(flag[0]=='T')
{
scanf("%d",&x);
int id=getid(x);
par t2=split(root,id+1),t1=split(t2.first,id);
root=merge(t1.second,merge(t1.first,t2.second));
}
else if(flag[0]=='B')
{
scanf("%d",&x);
int id=getid(x);
par t2=split(root,id+1),t1=split(t2.first,id);
root=merge(merge(t1.first,t2.second),t1.second);
}
else if(flag[0]=='I')
{
scanf("%d%d",&x,&y);
if(!y)continue;
if(y==-1)
{
int id=getid(x);
par t3=split(root,id+1),t2=split(t3.first,id),t1=split(t2.first,id-1);
root=merge(merge(merge(t1.first,t2.second),t1.second),t3.second);
}
else
{
int id=getid(x);
par t3=split(root,id+2),t2=split(t3.first,id+1),t1=split(t2.first,id);
root=merge(merge(merge(t1.first,t2.second),t1.second),t3.second);
}
}
else if(flag[0]=='A')
{
scanf("%d",&x);
printf("%d
",getid(x));
}
else
{
scanf("%d",&x);
printf("%d
",find(x));
}
}
return 0;
}
小结:好啦,非旋转Treap到这里,基本就说完了。这个平衡树好写,不用好调。常数虽然相较其他平衡树(除了BST)较大,但是时间复杂度是没有问题的。相信用这个fhq大神发明的神奇平衡树能让各位感受到序列的友好吧! 有问题直接评论哦!