题解 P3369 【【模板】普通平衡树】
shenbear
2019-12-13 12:19:21
经过主席,duck,kh的指点,bear终于学会了fhq,就写了一篇题解,来纪念一下
首先,第一个问题:fhq是什么?
fhq是由fhq巨佬发明的treap,利用分裂和合并,可以做到无旋treap,及可持久化等多项功能
------------
## fhq的核心就是分裂与合并
### 首先是合并:
对于两颗树t1,t2,满足t1中的所有点均<=t2,我们可以按以下规则合并:
1.如果t1,t2中有任意一颗树是空的那么,直接接上即可
2.否则,为了保持平衡,我们rand值,按sz[t1]:sz[t2]的概率来决定是合并到t1,还是t2
3.递归合并,上传
**注意:因为t2的点大于t1,所以合并一定是t2合t1右子树,t1合t2左子树,还有在递归合并时也要注意t1,t2大小**
**(简单来说,你可以理解为让小的合并到大的的概率更大)**
![合并](https://cdn.luogu.com.cn/upload/image_hosting/y8d5252b.png?x-oss-process=image/resize,m_lfit,h_170,w_225)
```cpp
int hb(int t1,int t2) //合并操作,必须保证t1的点都<=t2
{
if(!t1||!t2) return t1+t2;
else if((rad()%(sz[t1]+sz[t2]))<sz[t1]) //然它期望均摊到两颗树中
// < t1,t2的期望:sz[t1]:sz[t2] > t1,t2的期望:sz[t2]:sz[t1]
//简单来说,就是尽可能让小的和到大的上面,来保证它良好的复杂度
{
r[t1]=hb(r[t1],t2); //将t2合并到t1右树(t2>=t1)
pup(t1);
return t1;
}
else
{
l[t2]=hb(t1,l[t2]); //同上
pup(t2);
return t2;
}
}
```
------------
### 然后是分裂
分裂有两种,按值分裂和按个数分裂
先讲分裂的总体思路:
如果你的树空了,那么分出来的也一定是空的
然后按判断标准,选择分裂到谁,递归,上传
![分裂](https://cdn.luogu.com.cn/upload/image_hosting/31udk9ph.png?x-oss-process=image/resize,m_lfit,h_170,w_225)
按值分裂:就是把所有<=w合到x树上,其他的合到y树上
```cpp
void splitv(int k,int w,int &x,int &y) //按值分裂,保证x<=y , 把所有<=w的值都给x,剩下的给y
{
if(!k) x=y=0;
else if(w>=v[k]) //我要的值比当前值大
{
x=k; //当前值给小的(x)
splitv(r[k],w,r[x],y); //向右子树继续分裂
pup(x);
}
else
{
y=k;
splitv(l[k],w,x,l[y]);
pup(y);
}
}
```
按个数分裂:就是把前w个分到x树上,剩下的分到y树上
```cpp
void splits(int k,int w,int &x,int &y) //按个数分裂,保证x<=y,(x就是那前w个)
{
if(!k) x=y=0;
else if(w<=sz[l[k]]) //如果当前个数多余
{
y=k; //比w多,一定是在y上的
splits(l[k],w,x,l[y]); //向左子树分裂
pup(y);
}
else
{
x=k;
splits(r[k],w-sz[l[k]]-1,r[x],y);
pup(x);
}
}
```
------------
有了这两个操作,那么剩下的也比较简单了
------------
插入:把所有<=x的分裂到一颗树a上,剩下的在另一颗树b上,把a,x,b合并即为所求
```cpp
void ins(int x) //插入
{
int t1,t2;
splitv(rt,x,t1,t2); //把一棵树分裂成按x分裂成两部分
rt=hb(hb(t1,xj(x)),t2); //再把左边与x合并,再与右边合并
}
```
------------
删除:把所有<x的值分裂到一颗树a上,剩下的在b上,再把b上分出一个第一小的数,即为要删的x,剩下的是d,只要把中间这个x忽略,直接合并a,d,就相当于删了一个x
```cpp
void del(int x) //删除
{
int a,b,c,d;
splitv(rt,x-1,a,b); //把它分成两部分(x是右边那部分的最小值)
splits(b,1,c,d); //把右边分裂出一个大小为1的点c(x)
rt=hb(a,d); //不管c,直接合并a,d(忽略了一个x)
}
```
------------
剩下的操作你可以按普通treap进行,也可以用分裂合并,读者自想不难,想不出的可以看代码,注释里说的很详细
------------
代码:
```cpp
#include<bits/stdc++.h>
#define il inline
using namespace std;
const int N = 1e5+5;
il int rad(){return 1ll*rand()*19491001%19260817;}
struct treep
{
int l[N],r[N],sz[N],v[N];
int tim,rt;
il void pup(int x){sz[x]=sz[l[x]]+sz[r[x]]+1;}
il int xj(int x)
{
tim++;
v[tim]=x;
sz[tim]=1;
return tim;
}
int hb(int t1,int t2) //合并操作,必须保证t1的点都<=t2
{
if(!t1||!t2) return t1+t2;
else if((rad()%(sz[t1]+sz[t2]))<sz[t1]) //然它期望均摊到两颗树中
// < t1,t2的期望:sz[t1]:sz[t2] > t1,t2的期望:sz[t2]:sz[t1]
//简单来说,就是尽可能让小的和到大的上面,来保证它良好的复杂度
{
r[t1]=hb(r[t1],t2); //将t2合并到t1右树(t2>=t1)
pup(t1);
return t1;
}
else
{
l[t2]=hb(t1,l[t2]); //同上
pup(t2);
return t2;
}
}
void splitv(int k,int w,int &x,int &y) //按值分裂,保证x<=y , 把所有<=w的值都给x,剩下的给y
{
if(!k) x=y=0;
else if(w>=v[k]) //我要的值比当前值大
{
x=k; //当前值给小的(x)
splitv(r[k],w,r[x],y); //向右子树继续分裂
pup(x);
}
else
{
y=k;
splitv(l[k],w,x,l[y]);
pup(y);
}
}
void splits(int k,int w,int &x,int &y) //按个数分裂,保证x<=y,(x就是那前w个)
{
if(!k) x=y=0;
else if(w<=sz[l[k]]) //如果当前个数多余
{
y=k; //比w多,一定是在y上的
splits(l[k],w,x,l[y]); //向左子树分裂
pup(y);
}
else
{
x=k;
splits(r[k],w-sz[l[k]]-1,r[x],y);
pup(x);
}
}
void ins(int x) //插入
{
int t1,t2;
splitv(rt,x,t1,t2); //把一棵树分裂成按x分裂成两部分
rt=hb(hb(t1,xj(x)),t2); //再把左边与x合并,再与右边合并
}
void del(int x) //删除
{
int a,b,c,d;
splitv(rt,x-1,a,b); //把它分成两部分(x是右边那部分的最小值)
splits(b,1,c,d); //把右边分裂出一个大小为1的点c(x)
rt=hb(a,d); //不管c,直接合并a,d(忽略了一个x)
}
int askpm(int x) //查x的排名
{
int a,b;
splitv(rt,x-1,a,b); //裂出所有比x小的数
int ans=sz[a]+1; //x排名为比它小的数的个数+1
rt=hb(a,b); //把树合并回去
return ans;
}
int askdx(int x) //查第x大的数
{
int a,b,c,d;
splits(rt,x-1,a,b); //把前x-1个数裂出来
splits(b,1,c,d); //那么没被裂出来的数中的第一个,一定就是第x大的
int ans=v[c];
rt=hb(a,hb(c,d)); //把所有裂开的都依次合回去
return ans;
}
int askq(int x) //查前驱
{
int a,b,c,d;
splitv(rt,x-1,a,b); //把比它小的数裂出来
splits(a,sz[a]-1,c,d); //在小的中裂出最大的一个
int ans=v[d];
rt=hb(hb(c,d),b); //合并
return ans;
}
int askh(int x) //查后继
{
int a,b,c,d;
splitv(rt,x,a,b); //把比它大的裂出来
splits(b,1,c,d); //在比它大的数中找最小的
int ans=v[c];
rt=hb(a,hb(c,d)); //合并
return ans;
}
}T;
int main()
{
int t;
cin>>t;
while(t--)
{
int opt,x;
scanf("%d%d",&opt,&x);
if(opt==1) T.ins(x);
if(opt==2) T.del(x);
if(opt==3) printf("%d\n",T.askpm(x));
if(opt==4) printf("%d\n",T.askdx(x));
if(opt==5) printf("%d\n",T.askq(x));
if(opt==6) printf("%d\n",T.askh(x));
}
}
```
写于2019.12.13,纪念学会fhq
同时纪念南京大屠杀