旋转treap
优点:代码比splay好打
缺点:很多操作(尤其是区间操作)不资磁
插入
插入方式和二叉查找树一致,新建节点后,要给节点rand一个pos值。
然后返回改节点的父亲,如果该节点pos值比父亲大,就要旋转,以保证父节点的pos值小于儿子的,满足堆的性质。使用随机数可以使得这棵树尽量平衡。
void ins(int &x,LL bh) {
if(!x) {x=++sss,pos[x]=rand(),num[x]=bh,v[x]=1;return;}
if(bh<num[x]) {ins(son[x][0],bh);if(pos[son[x][0]]<pos[x]) spin(x,0);}
else {ins(son[x][1],bh);if(pos[son[x][1]]<pos[x]) spin(x,1);}
}
旋转
treap的旋转操作比splay清爽多了,可以说是相当于splay的zig/zag操作。
void spin(int &x,int is) {
int t=son[x][is];
son[x][is]=son[t][!is],son[t][!is]=x,x=t;
}
查找前驱后继
例题:洛谷P2234/bzoj2234/codevs1296 营业额统计
LL pre(int x,LL num) {//前驱
if(!x) return -inf;
if(v[x]>num) return pre(son[x][0],num);
return max(pre(son[x][1],num),v[x]);
}
LL nxt(int x,LL num) {//后继
if(!x) return inf;
if(v[x]<num) return nxt(son[x][1],num);
return min(nxt(son[x][0],num),v[x]);
}
## 删除
找到要删除的节点x,看它的左右儿子,旋转pos值较小的那个儿子,直到x成为叶子节点,删除x。
void del(int &x,LL bh) {
if(!x) return;
if(num[x]==bh) {
if(son[x][0]*son[x][1]==0) {x=son[x][0]+son[x][1];return;}
if(pos[son[x][0]]<pos[son[x][1]]) spin(x,0),del(x,bh);
else spin(x,1),del(x,bh);
}
if(bh<num[x]) del(son[x][0],bh);
else del(son[x][1],bh);
}
无旋treap
优点:可以可持久化,资磁的操作多
缺点:比splay慢
例题:洛谷P2464/codevs1840
打这道题的时候,我真的是比小j还烦恼…这道题有一种解法是离散+对于每一种书建立一棵无旋treap,然后进行操作。
无旋treap是基于合并与分裂两个操作的一种treap。
合并
现在我们有两棵分别以a和b为根的treap,想要a在左b在右的合并。如何合并呢?
首先,比较a和b的pos值,较小的那个可以作为一个根,然后将其左/右子树和另一个合并。如果是a,那么让a的右子树和b合并。如果是b,那么让b的左子树和a合并。
int merge(int a,int b) {
if(!a) return b;
if(!b) return a;
if(pos[a]<pos[b]) {son[a][1]=merge(son[a][1],b),up(a);return a;}
else {son[b][0]=merge(a,son[b][0]),up(b);return b;}
}
分裂
现在我们想在以x为根的子树里,左边分裂出一个num大小的树。怎么办?
我们的函数返回值是一个pair,这样可以存两棵分裂出来的子树的根:左边的和右边的。
首先特判两种可以直接分裂的情况:
1.x的左子树大小为num,此时可以直接分裂为左子树和以x为根的子树
2.x的左子树大小为num-1,此时可以直接分裂为以x为根的子树和右子树
然后再比较x左子树和num的大小,进行递归分裂。(不懂看代码)
#define pr pair<int,int>
#define mkp make_pair
pr split(int x,int num) {//从将树左边分离出一个num大小的
if(!x) return mkp(0,0);
int ls=son[x][0],rs=son[x][1];
if(sz[son[x][0]]==num) {son[x][0]=0,up(x);return mkp(ls,x);}
if(sz[son[x][0]]+1==num) {son[x][1]=0,up(x);return mkp(x,rs);}
if(num<sz[son[x][0]]) {//递归分裂
pr tmp=split(son[x][0],num);
son[x][0]=tmp.second,up(x);
return mkp(tmp.first,x);
}
else {
pr tmp=split(son[x][1],num-sz[son[x][0]]-1);
son[x][1]=tmp.first,up(x);
return mkp(x,tmp.second);
}
}
插入
有了上面两种操作,插入操作就变得轻松了许多,即新建一个节点,找到原树中该节点应该排在第几,然后分裂原来的树,再依次合并(分裂出来的左边,新节点,分裂出来的右边)
插入一个区间也与之类似。
代码:见完整代码标记处。
删除
找到要删除的节点,先分裂出该节点左边一棵子树,再分裂出该节点右边一棵子树,然后把两棵子树合并,就不要那个节点了。
删除一个区间也与之类似。
代码:见完整代码标记处。
区间操作
用类似于删除的方法把整个区间取出来,打上区间操作标记即可。
然后在合并和删除操作的时候都要记得pushdown。
例题就是bzoj3223/洛谷P3391 文艺平衡树 啦,完整代码下面有
完整代码
小J的烦恼
#include<bits/stdc++.h>
using namespace std;
int read() {
int q=0;char ch=' ';
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
return q;
}
#define pr pair<int,int>
#define mkp make_pair
const int N=600005;
int n,m,cnt,sss;
map<int,int> mp;
int rt[N],bk[N],son[N][2],pos[N],wz[N],sz[N];
int newjd(int x) {if(!mp[x]) mp[x]=++cnt; return mp[x];}
void up(int x) {sz[x]=sz[son[x][0]]+sz[son[x][1]]+1;}
int merge(int a,int b) {
if(!a) return b;
if(!b) return a;
if(pos[a]<pos[b]) {son[a][1]=merge(son[a][1],b),up(a);return a;}
else {son[b][0]=merge(a,son[b][0]),up(b);return b;}
}
pr split(int x,int num) {//从将树左边分离出一个num大小的
if(!x) return mkp(0,0);
int ls=son[x][0],rs=son[x][1];
if(sz[son[x][0]]==num) {son[x][0]=0,up(x);return mkp(ls,x);}
if(sz[son[x][0]]+1==num) {son[x][1]=0,up(x);return mkp(x,rs);}
if(num<sz[son[x][0]]) {
pr tmp=split(son[x][0],num);
son[x][0]=tmp.second,up(x);
return mkp(tmp.first,x);
}
else {
pr tmp=split(son[x][1],num-sz[son[x][0]]-1);
son[x][1]=tmp.first,up(x);
return mkp(x,tmp.second);
}
}
int find(int x,int kth) {//wz小于kth的节点个数
if(!x) return 0;
if(wz[x]<=kth) return sz[son[x][0]]+1+find(son[x][1],kth);
return find(son[x][0],kth);
}
int main()
{
char ch[10];int x,y,z;
n=read(),m=read(),srand(m);
for(int i=1;i<=n;++i) {
x=read(),bk[i]=newjd(x);
pos[++sss]=rand(),sz[sss]=1,wz[sss]=i;
rt[bk[i]]=merge(rt[bk[i]],sss);//这是插入操作
}
while(m--) {
scanf("%s",ch),x=read(),y=read();
if(ch[0]=='Q') {
z=read(),z=newjd(z);
printf("%d\n",find(rt[z],y)-find(rt[z],x-1));
}
else {
pr t1,t2;
t1=split(rt[bk[x]],find(rt[bk[x]],x)-1);//这里是删除操作
t2=split(t1.second,1);
rt[bk[x]]=merge(t1.first,t2.second);
bk[x]=newjd(y);
t1=split(rt[bk[x]],find(rt[bk[x]],x));//以下是插入操作
pos[++sss]=rand(),sz[sss]=1,wz[sss]=x;
rt[bk[x]]=merge(t1.first,merge(sss,t1.second));
}
}
return 0;
}
文艺平衡树
#include<bits/stdc++.h>
using namespace std;
#define mkp make_pair
#define pr pair<int,int>
const int N=100005;
int n,m,rt;
int son[N][2],rev[N],pos[N],sz[N];
void up(int x) {sz[x]=sz[son[x][0]]+sz[son[x][1]]+1;}
void pd(int x) {
if(son[x][0]) rev[son[x][0]]^=1;
if(son[x][1]) rev[son[x][1]]^=1;
swap(son[x][0],son[x][1]),rev[x]=0;
}
int merge(int a,int b) {
if(!a) return b;
if(!b) return a;
if(rev[a]) pd(a); if(rev[b]) pd(b);//注意pushdown
if(pos[a]<pos[b]) {son[a][1]=merge(son[a][1],b),up(a);return a;}
else {son[b][0]=merge(a,son[b][0]),up(b);return b;}
}
pr split(int x,int num) {
if(!x) return mkp(0,0);
if(rev[x]) pd(x);//注意pushdown
int ls=son[x][0],rs=son[x][1];
if(sz[son[x][0]]==num) {son[x][0]=0,up(x);return mkp(ls,x);}
if(sz[son[x][0]]+1==num) {son[x][1]=0,up(x);return mkp(x,rs);}
if(num<sz[son[x][0]]) {
pr tmp=split(son[x][0],num);
son[x][0]=tmp.second,up(x);
return mkp(tmp.first,x);
}
else {
pr tmp=split(son[x][1],num-sz[son[x][0]]-1);
son[x][1]=tmp.first,up(x);
return mkp(x,tmp.second);
}
}
int build(int l,int r) {
if(l==r) {pos[l]=rand(),sz[l]=1;return l;}
int mid=(l+r)>>1;
return merge(build(l,mid),build(mid+1,r));
}
void print(int x) {
if(rev[x]) pd(x);
if(son[x][0]) print(son[x][0]);
printf("%d ",x);
if(son[x][1]) print(son[x][1]);
}
int main()
{
int x,y;srand(14233);
scanf("%d%d",&n,&m);rt=build(1,n);
while(m--) {
scanf("%d%d",&x,&y);
pr t2=split(rt,y);
pr t1=split(t2.first,x-1);
rev[t1.second]^=1;
rt=merge(t1.first,merge(t1.second,t2.second));
}
print(rt);
return 0;
}
维护数列
既然学了无旋treap,就不得不去做一做这道神题啦!
你会发现无旋treap虽然比splay慢,可是编码速度更快呢!
戳我看代码