平衡树(fhq-Treap)
远古文章,谨慎食用!
稍微修了一下但感觉还是很naive。。。
fhq-Trea,又名非旋Treap
思路简单,代码简短,功能强大,LCT会多个log,但是其他操作与Splay功能差不了多少,还支持可持久化。
每个节点一个随机权值,维持平衡性。内部权值是个BST(二叉搜索树),随机权值是个Heap(堆)。
2大基本函数(如果学过ODT的话就是跟Split一样重要的函数),可随题目随机应变。
1.merge。建树及操作都要用。
pushup是修改节点信息,和线段树很像。这里只有修改size的作用。
void pushup(int u) {
size[u]=size[ch[u][0]]+size[ch[u][1]]+1;
}
int merge(int x,int y) {
if(!x||!y)return x+y;
if(rnd[x]<rnd[y]) {
ch[x][1]=merge(ch[x][1],y);
pushup(x);
return x;
} else {
ch[y][0]=merge(x,ch[y][0]);
pushup(y);
return y;
}
}
2.split.按照某种权值分裂(如果把排名当做权值就是排名分裂了),把权值不大于k的分到左子树,大于k的分到右子树。
void split(int u,int k,int &x,int &y) {
if(!u) {
x=y=0;
return;
}
if(val[u]<=k)
x=u,split(ch[u][1],k,ch[u][1],y);
else
y=u,split(ch[u][0],k,x,ch[u][0]);
pushup(u);
}
其余操作就是在这2个函数的基础上乱搞。基本套路:split到想要的东西,操作,然后把整棵树merge回去。
权值分裂:
0.申请一个新节点。所有信息都记录一下。
int newnode(int x) {
++tot;
rnd[tot]=rand();
val[tot]=x;
size[tot]=1;
return tot;
}
1.插入.申请一个新节点,和根节点merge一下
void insert(int x) {
int a,b;
split(rt,x,a,b);
rt=merge(merge(a,newnode(x)),b);
}
2.删除。把这个权值通过split找到,把它的左右子树merge一下。
void del(int x) {
int a,b,c;
split(rt,x,a,b);
split(a,x-1,a,c);
c=merge(ch[c][0],ch[c][1]);
rt=merge(merge(a,c),b);
}
3.查询值为x的数的排名。把比它小的数放到左子树,然后左子树大小加一就是答案。
int rk(int x) {
int a,b;
split(rt,x-1,a,b);
int res=size[a]+1;
rt=merge(a,b);
return res;
}
4.第k小值。经典二叉搜索树操作,不多说了。
int kth(int u,int k) {
while(true) {
if(k<=size[ch[u][0]])u=ch[u][0];
else {
if(k==size[ch[u][0]]+1)return val[u];
else k-=size[ch[u][0]]+1,u=ch[u][1];
}
}
}
5.前驱。把不大于它的放到左子树,把左子树里比它小的放到左子树的左子树,然后在左子树的左子树里查询第size[左子树的左子树]小的值。
int lowerbound(int x) {
int a,b;
split(rt,x-1,a,b);
int res=kth(a,size[a]);
rt=merge(a,b);
return res;
}
6.后继。同理。
int upperbound(int x) {
int a,b;
split(rt,x,a,b);
int res=kth(b,1);
rt=merge(a,b);
return res;
}
整合一下就可以AC了。
#include<bits/stdc++.h>
using namespace std;
const int N=100005;
int n,rt,tot;
struct fhq {
int size[N],ch[N][2],val[N],rnd[N];
void pushup(int u) {
size[u]=size[ch[u][0]]+size[ch[u][1]]+1;
}
int newnode(int x) {
++tot;
rnd[tot]=rand();
val[tot]=x;
size[tot]=1;
return tot;
}
int merge(int x,int y) {
if(!x||!y)return x+y;
if(rnd[x]<rnd[y]) {
ch[x][1]=merge(ch[x][1],y);
pushup(x);
return x;
} else {
ch[y][0]=merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int u,int k,int &x,int &y) {
if(!u) {
x=y=0;
return;
}
if(val[u]<=k)
x=u,split(ch[u][1],k,ch[u][1],y);
else
y=u,split(ch[u][0],k,x,ch[u][0]);
pushup(u);
}
void insert(int x) {
int a,b;
split(rt,x,a,b);
rt=merge(merge(a,newnode(x)),b);
}
void del(int x) {
int a,b,c;
split(rt,x,a,b);
split(a,x-1,a,c);
c=merge(ch[c][0],ch[c][1]);
rt=merge(merge(a,c),b);
}
int rk(int x) {
int a,b;
split(rt,x-1,a,b);
int res=size[a]+1;
rt=merge(a,b);
return res;
}
int kth(int u,int k) {
while(true) {
if(k<=size[ch[u][0]])u=ch[u][0];
else {
if(k==size[ch[u][0]]+1)return val[u];
else k-=size[ch[u][0]]+1,u=ch[u][1];
}
}
}
int lowerbound(int x) {
int a,b;
split(rt,x-1,a,b);
int res=kth(a,size[a]);
rt=merge(a,b);
return res;
}
int upperbound(int x) {
int a,b;
split(rt,x,a,b);
int res=kth(b,1);
rt=merge(a,b);
return res;
}
} T;
int main() {
scanf("%d",&n);
for(int i=1; i<=n; i++) {
int opt,x;
scanf("%d%d",&opt,&x);
if(opt==1)T.insert(x);
if(opt==2)T.del(x);
if(opt==3)printf("%d\n",T.rk(x));
if(opt==4)printf("%d\n",T.kth(rt,x));
if(opt==5)printf("%d\n",T.lowerbound(x));
if(opt==6)printf("%d\n",T.upperbound(x));
}
return 0;
}
排名分裂的应用:
打个标记就好了,旋转的时候把标记下传给左右子树,然后把排名当权值。
#include<bits/stdc++.h>
using namespace std;
#define rint register int
const int N=100005;
int n,m,tot,rt;
struct FHQTREAP{
int ch[N][2],pos[N],val[N],size[N];
bool fl[N];
void pushup(int u)
{
size[u]=size[ch[u][0]]+size[ch[u][1]]+1;
}
int newnode(int x)
{
val[++tot]=x;
size[tot]=1;
pos[tot]=rand();
return tot;
}
void pushdown(int p)
{
if(!fl[p])return;
swap(ch[p][0],ch[p][1]);
if(ch[p][0])fl[ch[p][0]]^=1;
if(ch[p][1])fl[ch[p][1]]^=1;
fl[p]=0;
}
int merge(int x,int y)
{
if(!x||!y)return x+y;
if(pos[x]<pos[y])
{
pushdown(x);
ch[x][1]=merge(ch[x][1],y);
pushup(x);
return x;
}
else
{
pushdown(y);
ch[y][0]=merge(x,ch[y][0]);
pushup(y);
return y;
}
}
void split(int u,int k,int &x,int &y)
{
if(!u)
{
x=y=0;
return;
}
pushdown(u);
if(size[ch[u][0]]<k){x=u;split(ch[u][1],k-size[ch[u][0]]-1,ch[u][1],y);}
else{y=u;split(ch[u][0],k,x,ch[u][0]);}
pushup(u);
}
void print(int u)
{
if(!u)return;
pushdown(u);
print(ch[u][0]);
printf("%d ",val[u]);
print(ch[u][1]);
}
}T;
int main()
{
srand(13);
scanf("%d%d",&n,&m);
for(rint i=1;i<=n;i++)
rt=T.merge(rt,T.newnode(i));
for(int i=1;i<=m;i++)
{
int l,r,a,b,c;
scanf("%d%d",&l,&r);
T.split(rt,l-1,a,b);
T.split(b,r-l+1,b,c);
T.fl[b]^=1;
rt=T.merge(a,T.merge(b,c));
}
T.print(rt);
return 0;
}
然后一堆类似P2343 宝石管理系统P2286 [HNOI2004]宠物收养场的题就可以随便切了,把板子打熟。
P3960 列队 ,著名的树状数组+二分的题,但是如果思维没那么强,就可以用平衡树按照题意模拟。
一个比较显然的做法是对于最后一列以及每一行(最后一列除外)开一棵平衡树,然后就可以分裂出节点,把它从一棵树里删掉,再插入另一颗树。
这题发现开不下 \(n*m\) 个节点,所以每个节点存的是区间。接着我们发现需要一个节点就没法搞了。讲讲如何把一个节点从区间内分离出来。
把一个节点裂成两个区间,然后把不需要的区间放到右子树,就好了。
这种需要改split的题目难度会高一些,我想了好久才会……
void changesize(int u,int k) {
if(k>=r[u]-l[u]+1)return;
int v=l[u]+k-1;
int nn=newnode(v+1,r[u]);
r[u]=v;
ch[u][1]=merge(nn,ch[u][1]);
upd(u);
}
void split(int u,int k,int &x,int &y) {
if(!u) {x=y=0;return;}
if(size[ch[u][0]]>=k) {
y=u;
split(ch[u][0],k,x,ch[u][0]);
} else {
changesize(u,k-size[ch[u][0]]);
x=u;
split(ch[u][1],k-size[ch[u][0]]-(r[u]-l[u]+1),ch[u][1],y);
}
upd(u);
}
完整的,注意主函数一堆乱七八糟的不要写错
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=6100005;
int n,m,q,root[N],ch[N][2],rnd[N],size[N],r[N],l[N],tot;
int newnode(int ll,int rr) {
++tot;
l[tot]=ll;
r[tot]=rr;
size[tot]=rr-ll+1;
rnd[tot]=rand()*rand();
return tot;
}
void upd(int u) {
size[u]=size[ch[u][0]]+r[u]-l[u]+1+size[ch[u][1]];
}
int merge(int x,int y) {
if(!x||!y)return x+y;
if(rnd[x]<rnd[y]) {
ch[x][1]=merge(ch[x][1],y);
upd(x);
return x;
} else {
ch[y][0]=merge(x,ch[y][0]);
upd(y);
return y;
}
}
void changesize(int u,int k) {
if(k>=r[u]-l[u]+1)return;
int v=l[u]+k-1;
int nn=newnode(v+1,r[u]);
r[u]=v;
ch[u][1]=merge(nn,ch[u][1]);
upd(u);
}
void split(int u,int k,int &x,int &y) {
if(!u) {x=y=0;return;}
if(size[ch[u][0]]>=k) {
y=u;
split(ch[u][0],k,x,ch[u][0]);
} else {
changesize(u,k-size[ch[u][0]]);
x=u;
split(ch[u][1],k-size[ch[u][0]]-(r[u]-l[u]+1),ch[u][1],y);
}
upd(u);
}
signed main() {
srand(time(0));
scanf("%lld%lld%lld",&n,&m,&q);
for(int i=1; i<=n; ++i)
root[i]=newnode((i-1)*m+1,i*m-1);
for(int i=1; i<=n; ++i)
root[n+1]=merge(root[n+1],newnode(i*m,i*m));
while(q--) {
int x,y;
scanf("%lld%lld",&x,&y);
if(y!=m) {
int a,b,c;
split(root[x],y,a,b);
split(a,y-1,a,c);
printf("%lld\n",l[c]);
int a1,b1,c1;
split(root[n+1],x,a1,b1);
split(a1,x-1,a1,c1);
root[x]=merge(a,merge(b,c1));
root[n+1]=merge(a1,merge(b1,c));
} else {
int a,b,c;
split(root[n+1],x,a,b);
split(a,x-1,a,c);
printf("%lld\n",l[c]);
root[n+1]=merge(a,merge(b,c));
}
}
return 0;
}
几个月之后我再来补充一下我的感想:
fhq的常数比较大,在这方面与splay齐名
但是有个优秀的性质:树高严格 \(\log\) ,这个性质比较重要,有些操作可以基于这个保证复杂度
它可以很方便的分裂区间,这也是为什么理解起来简单,写起来简单,常数大的原因
大部分时候建议写treap或替罪羊,除非有什么操作比较复杂,那么首选fhq。
为了卡常可以把treap和fhq的操作混用,因为本质是同一个东西。
平衡树往往只是作为工具使用,重要的还是思维,板子打熟就好。