作者:hsez_yyh
链接: FHQ-Treap——从入门到入坟_hsez_yyh的博客-CSDN博客
来源:湖北省黄石二中信息竞赛组
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
平衡树有很多种,其中fhq-treap 可以说是最强的一种平衡树之一,它可以维护值域,也可以维护下标,还能维护区间修改,更难能可贵的是,它可以完成splay都完成不了的可持久化操作。其唯一弱于splay的就是在LCT上,splay维护LCT的复杂度是O(nlog(n)),而fhq-treap的复杂度为O(nlog^2(n)),稍微大了一点,但是比splay好写多了。
其实FHQ-treap的实现非常简单,本人认为,数据结构其实都不难实现,只是难于在概念上抽象的理解,所以,对于学习数据结构的最好方法就是手动模拟,在开始我们愉快的FHQ-treap之旅之前,在此先瞻仰一下FHQ-treap的最先引入者范浩强大佬。
现在我们正式开始(其实迫于教练压力才写的这篇文章)
首先fhq-treap是一个二叉搜索树(BST),它的每个节点有两个主要信息:key和val,key是我们fhq-treap要维护的键值,而val是随机生成的(rand()),key信息主要用于我们对于题目信息的处理,而val则是用于维持fhq-treap在结构上满足期望高度为logn的,val保证了fhq-treap拥有稳定的logn的高度,不会像splay一样退化。fhq-treap 除了要满足关于key的BST性质之外,还需满足关于val的小根堆性质。就是说,对于任意fhq-treap中的节点 i ,其左子树上的所有节点的key小于等于 i 节点的key值,i 节点所有右子树上所有节点的key 大于等于 i 节点的key值;对于任意节点 i ,其左、右儿子的val值大于等于 i 的val值(满足小根堆的性质)。要牢牢记住这两点的区别,否则会像我一样搞混,然后写代码的时候狂 WA,(手动悲剧)。下面给出一颗fhq-treap:
上图就是一颗fhq-treap,其中每个节点中的数字为val,每个节点下方的数字为当前节点的key 可以发现一颗fhq-treap中序遍历后得到的序列是单调递增(如果维护方式不同其也可以是单调递减的)的。 非常好的性质——神犇ljz说。 这里,我们要引入fhq-treap的两个基本操作:split和merge, 其中split是分离操作,merge是合并操作,下面我们来一一阐明:
1. split 通常情况下,split用于分离一颗fhq-treap,对于一个元素 x ,我们会将这棵fhq-treap分裂为左右两棵树,左树上每个节点的key值都小于等于(<=)x , 而右树上的所有节点的key值都大于 x ;下面给出代码:
void split(int pos,int xx,int &l,int &r)// pos是当前fhq-treap的根
{ // &l 和 &r分别是fhq-treap分裂后得到的两个fhq-treap的根,这里用指针(引用)的方式,便于传递值
if(!pos)//如果该节点为空树,则分裂成的左右两个树也应为空
{
l=r=0;
return ;
}
if(tr[pos].key<=xx)// 如果当前节点的key值小于等于xx,则证明该节点的左子树上所有的key值也小于等于xx
{
l=pos;//让该节点成为左树的节点
split(tr[l].r,xx,tr[l].r,r);//继续递归分离当前节点的右子树
}
else//同上
{
r=pos;
split(tr[r].l,xx,l,tr[r].l);//继续递归分离当前节点的左子树
}
push_up(pos);
//一定要记得push_up,因为pos点的左右子树大小发生了改变,否则会导致信息不对
}
//以上便是递归分离左右子树的方法
以上代码如果学过线段树的话会好理解的多,本蒟蒻也建议先去熟练掌握线段树再来学习fhq-treap,这样会轻松很多。下面我们来模拟这个过程——本蒟蒻就是开始没有自己去模拟,导致一直不是很理解fhq-treap的实现方式。
对于上图,假如我们要将其分离为小于等于18和大于18两个部分:
在此之后,我们会继续递归,分裂K节点的左子树,大家可以自己手动模拟下(主要是画图太麻烦了,QAQ),由于I节点的权值20>18,所以我们将 I 节点归入右fhq-treap,接下来我们会碰到空节点,递归将会跳出,这时,我们就能得到左右两棵fhq-treap了:
以上就是fhq-treap的基本操作之一split操作的全部过程了 。
2. merge 操作 这是一个合并操作,但它也不能随便合并,如果要合并两棵fhq-treap,那么它们可以进行合并当且仅当左fhq-treap上所有节点的key值都小于等于右fhq-treap中的最小key值(也就是左树小于等于右树)或两棵树中有空树。 这个性质很重要,为根据val合并两棵树做好的前提,保证了BST结构不会因合并而被破坏。 合并时,我们会根据左右两棵树的根节点的val值大小进行合并,大家先看一下代码:
int merge(int l,int r)//传入左右子树的根节点
{
if(!l||!r) //如果有空树
return l|r;//直接返回非空树(本蒟蒻这样的写法等同于l+r,是用来装杯的)
if(tr[l].val<=tr[r].val)
{
//如果左树上当前节点的val值小于右树上当前节点的val值
//将l变为合并后的新fhq-treap的根节点
tr[l].r=merge(tr[l].r,r);//继续递归确定l的右子树该接谁
push_up(l);//一定记得上传
return l;//返回新得到的根节点
}
else// 同上
{
tr[r].l=merge(l,tr[r].l);
push_up(r);
return r;
}
}
还是一样,画图好理解:
以上是两棵要合并的fhq-treap
以上就是fhq-treap合并的全部过程了。
到现在,fhq-treap的最基本也是精华所在你已经完全掌握了,下面我们来看一道题目:
【模板】普通平衡树 - 洛谷 这道题就是平衡树的板子题,用fhq-treap可以轻松切掉,代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,idx;
int dl,dr,temp,rt;
struct node
{
int l,r;
int key,val;
int si;//以当前节点为根的树的节点数
}tr[N*2];
int get_rand(int xx)
{
tr[++idx].key=xx;
tr[idx].val=rand();
tr[idx].si=1;
return idx;
} //建立一个新节点
void push_up(int pos)
{
tr[pos].si=tr[tr[pos].l].si+tr[tr[pos].r].si+1;
// 注意这里,这里跟线段树的push_up不一样,要记得加上自己的大小
}
void split(int pos,int xx,int &l,int &r)//分离操作
{
if(!pos)
{
l=r=0;
return ;
}
if(tr[pos].key<=xx)
{
l=pos;
split(tr[l].r,xx,tr[l].r,r);
}
else
{
r=pos;
split(tr[r].l,xx,l,tr[r].l);
}
push_up(pos);
}
int merge(int l,int r) // 合并操作
{
if(!l||!r)
return l|r;
if(tr[l].val<=tr[r].val)
{
tr[l].r=merge(tr[l].r,r);
push_up(l);
return l;
}
else
{
tr[r].l=merge(l,tr[r].l);
push_up(r);
return r;
}
}
void insert(int xx)
{
// 对于合并操作,先将原树分裂成<=xx-1和 >xx-1(>=xx)两个部分
// 将xx与<=xx-1的部分合并,再和>xx-1的部分合并
split(rt,xx-1,dl,dr);
rt=merge(merge(dl,get_rand(xx)),dr);
}
void delet(int xx)
{
split(rt,xx-1,dl,dr);
split(dr,xx,temp,dr);
//连续两次分裂,temp得到了一棵所有key值==xx的fhq-treap
temp=merge(tr[temp].l,tr[temp].r);
//由于只能删一个xx,而原树中可能有很多xx,那么我们直接合并temp的左右子树即可
rt=merge(dl,merge(temp,dr));
// 将原树恢复(已删去xx)
}
int get_rk(int xx)
{
//非常简单,直接分离出<=xx-1的一棵树,也就是<xx的一棵树
//然后xx的排名就是<xx的树的大小+1即可
split(rt,xx-1,dl,dr);
int rnk=tr[dl].si+1;
rt=merge(dl,dr);
return rnk;
}
int get_num(int pos,int xx)
{
//这里采用递归搜索,思想见二分搜索树
int u=tr[tr[pos].l].si+1;
if(u==xx)
return tr[pos].key;
if(u>xx) return get_num(tr[pos].l,xx);
else return get_num(tr[pos].r,xx-u);
}
int main()
{
scanf("%d",&n);
int op,xx;
while(n--)
{
scanf("%d %d",&op,&xx);
if(op==1) insert(xx);
else if(op==2) delet(xx);
else if(op==3) printf("%d\n",get_rk(xx));
else if(op==4) printf("%d\n",get_num(rt,xx));
else if(op==5)
{
split(rt,xx-1,dl,dr);
printf("%d\n",get_num(dl,tr[dl].si));
// 前驱就是<=xx-1的这棵树中排名最后的那个数
rt=merge(dl,dr);
}
else
{
split(rt,xx,dl,dr);
printf("%d\n",get_num(dr,1));
//后继就是>xx的这棵树中排名第一的树
rt=merge(dl,dr);
}
}
return 0;
}
好了,目前为止,平衡树维护值域的方法你就已经掌握了,下面我们再来看一个fhq-treap维护序列的题目:
【模板】文艺平衡树 - 洛谷 这道题就是典型的fhq-treap维护序列,操作也非常简单,仅仅是加上了一个翻转的懒标记而已——可见学线段树的必要。
值得一说的是,fhq-treap在维护序列时,void split(int pos,int k,int &l,int &r)的意思不再是分离<=k和>k的两棵树,而是变为了 将序列pos 的前k个元素分离出来形成一棵新fhq-treap l,另外的一部分形成另一颗 fhq-treap r .
下面展示代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m;
struct node
{
int l,r;
int key,val;
int si;
bool tag;
}tr[N*2];
int dl,dr,temp,rt,idx;
int get_rand(int xx)
{
tr[++idx].key=xx;
tr[idx].val=rand();
tr[idx].si=1;
return idx;
}
void push_up(int pos)
{
tr[pos].si=tr[tr[pos].l].si+tr[tr[pos].r].si+1;
}
void push_down(int pos)
{
//下传懒标记,如果一直下传下去,将会实现左右区间翻转
if(tr[pos].tag)
{
tr[tr[pos].l].tag^=1;
tr[tr[pos].r].tag^=1;
swap(tr[pos].l,tr[pos].r);
tr[pos].tag=0;
}
}
void split(int pos,int k,int &l,int &r)
{
if(!pos)
{
l=r=0;
return ;
}
//只要对某个节点的子节点做操作,就一定要将该节点的标记下传
push_down(pos);
int u=tr[tr[pos].l].si+1;
if(u<=k)
{
l=pos;
split(tr[pos].r,k-u,tr[pos].r,r);
push_up(l);
}
else
{
r=pos;
split(tr[pos].l,k,l,tr[pos].l);
push_up(r);
}
}
int merge(int l,int r)
{
if(!l||!r) return l|r;
push_down(l);
push_down(r);
if(tr[l].val<=tr[r].val)
{
tr[l].r=merge(tr[l].r,r);
push_up(l);
return l;
}
else
{
tr[r].l=merge(l,tr[r].l);
push_up(r);
return r;
}
}
void reverse(int l,int r)
{
int p1=0,p2=0,p3=0,p4=0;
split(rt,l-1,p1,p2);
split(p2,(r-l+1),p3,p4);
// 将要翻转的一段分离出来,打上标记,再合并回去
tr[p3].tag^=1;
p2=merge(p3,p4);
rt=merge(p1,p2);
}
void print_res(int pos)
{
push_down(pos);
if(tr[pos].l) print_res(tr[pos].l);
printf("%d ",tr[pos].key);
if(tr[pos].r) print_res(tr[pos].r);
// 迭代输出,没什么好说的
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
{
split(rt,i-1,dl,dr);
rt=merge(merge(dl,get_rand(i)),dr);
}
int xx,yy;
while(m--)
{
scanf("%d %d",&xx,&yy);
reverse(xx,yy);
}
print_res(rt);
return 0;
}
到此,平衡树最大的两个功能都能实现了 Orz ! ! !
但既然要入坟,那么在此推荐两道题目:
266. 超级备忘录 - AcWing题库
[NOI2005] 维护数列 - 洛谷 祝大家WA的开心,本蒟蒻虽然过了,但是懒得贴代码了——都是泪
你以为就这样结束了??? 不,还远远不止,既然要入坟,那么就要坚持到底:
fhq-treap之所以是现在OI界地位逐渐超过splay的结构,自然有splay做不到的地方。众所周知 ,splay依靠二次旋转来保持logn的复杂度的,然而,这样做会经常改变树的结构,自然,splay对于处理可持久化的信息是一点办法都没有——就是嘛,转来转去的一看就不好 。但是嘛,splay可以做的fhq-treap可以做,splay不能做的fhq-treap还可以做(就算lct复杂度高一点,但是也是可以做的嘛,手动滑稽) 。
口胡完毕,进入正题: 细心的你可以发现,在fhq-treap的 split 操作和 merge 操作时,由于fhq-treap的期望高度是 logn,并且可以发现 split 和 merge 时都是自上而下的,那么,对于每次 split 和 merge 操作,真正有所变动的点只有 logn 个 ,所以我们只需要在操作过程中记录下沿途经过的 logn 个节点,就能对某次历史版本有所记录,其记录方式与主席树类似——又是线段树 。
如果再仔细思考,我们可以发现,每次 merge 操作都是合并先由 split 操作分离的两棵树,所以,对于真正结果有影响的操作只有 split ,所以,我们只需要在每次 split 的时候记录沿途的节点即可。
例题: 【模板】可持久化平衡树 - 洛谷
需要注意的是,我们每次操作产生的新版本,都要创建一个新根来指向次版本。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=5e5+10;
int n,dl,dr,temp;
struct node
{
int l,r;
LL key,val;
LL si;
}tr[N*60];
int rt[N*60],idx;
void push_up(int pos)
{
tr[pos].si=tr[tr[pos].l].si+tr[tr[pos].r].si+1;
}
int get_rand(int xx)
{
tr[++idx].key=xx;
tr[idx].val=rand();
tr[idx].si=1;
return idx;
}
void split(int pos,int xx,int &l,int &r)
{
if(!pos)
{
l=r=0;
return ;
}
if(tr[pos].key<=xx)
{
l=++idx;
tr[l]=tr[pos];
split(tr[l].r,xx,tr[l].r,r);
push_up(l);
}
else
{
r=++idx;
tr[r]=tr[pos];
split(tr[pos].l,xx,l,tr[r].l);
push_up(r);
}
}
int merge(int l,int r)
{
if(!l||!r) return l|r;
int pos=++idx;
if(tr[l].val<=tr[r].val)
{
tr[pos]=tr[l];
tr[pos].r=merge(tr[pos].r,r);
}
else
{
tr[pos]=tr[r];
tr[pos].l=merge(l,tr[pos].l);
}
push_up(pos);
return pos;
}
void insert(int &pos,int xx)
{
split(pos,xx-1,dl,dr);
pos=merge(merge(dl,get_rand(xx)),dr);
}
void delet(int &pos,int xx)
{
split(pos,xx-1,dl,dr);
split(dr,xx,temp,dr);
temp=merge(tr[temp].l,tr[temp].r);
pos=merge(dl,merge(temp,dr));
}
int get_rk(int &pos,int xx)
{
split(pos,xx-1,dl,dr);
int rk=tr[dl].si+1;
pos=merge(dl,dr);
return rk;
}
int get_num(int pos,int xx)
{
int u=tr[tr[pos].l].si+1;
if(u==xx) return tr[pos].key;
if(u>xx) return get_num(tr[pos].l,xx);
else return get_num(tr[pos].r,xx-u);
}
int main()
{
scanf("%d",&n);
int op,xx,t;
int cnt=0;
while(n--)
{
cnt++;
scanf("%d %d %d",&t,&op,&xx);
rt[cnt]=rt[t];
if(op==1) insert(rt[cnt],xx);
else if(op==2) delet(rt[cnt],xx);
else if(op==3) printf("%d\n",get_rk(rt[cnt],xx));
else if(op==4) printf("%d\n",get_num(rt[cnt],xx));
else if(op==5)
{
split(rt[cnt],xx-1,dl,dr);
printf("%d\n",get_num(dl,tr[dl].si));
rt[cnt]=merge(dl,dr);
}
else
{
split(rt[cnt],xx,dl,dr);
printf("%d\n",get_num(dr,1));
rt[cnt]=merge(dl,dr);
}
}
return 0;
}
既然有了这道题,那么你肯定能想到另外一道:【模板】可持久化文艺平衡树 - 洛谷
其实和普通的文艺平衡树一样,只需要多加一个可持久化操作即可,代码如下(也就写了一小时):
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+10;
int n,idx;
int dl,dr,temp;
struct node
{
int l,r;
int key,val;
int si;
bool rev;
LL sum;
}tr[N*100];
int rt[N];
void push_up(int pos)
{
tr[pos].si=tr[tr[pos].l].si+tr[tr[pos].r].si+1;
tr[pos].sum=tr[tr[pos].l].sum+tr[tr[pos].r].sum+tr[pos].key;
}
int get_rand(int xx)
{
tr[++idx].sum=xx;
tr[idx].key=xx;
tr[idx].si=1;
tr[idx].val=rand();
return idx;
}
void push_down(int &pos)
{
if(!tr[pos].rev) return ;
if(tr[pos].l)
{
tr[++idx]=tr[tr[pos].l];
tr[pos].l=idx;
tr[tr[pos].l].rev^=1;
}
if(tr[pos].r)
{
tr[++idx]=tr[tr[pos].r];
tr[pos].r=idx;
tr[tr[pos].r].rev^=1;
}
tr[pos].rev=0;
swap(tr[pos].l,tr[pos].r);
}
void split(int pos,int k,int &l,int &r)
{
if(!pos)
{
l=r=0;
return ;
}
push_down(pos);
int u=tr[tr[pos].l].si+1;
if(u<=k)
{
l=++idx;
tr[l]=tr[pos];
split(tr[pos].r,k-u,tr[l].r,r);
push_up(l);
}
else
{
r=++idx;
tr[r]=tr[pos];
split(tr[pos].l,k,l,tr[r].l);
push_up(r);
}
}
int merge(int l,int r)
{
if(!l||!r) return l|r;
int pos=++idx;
if(tr[l].val<=tr[r].val)
{
tr[pos]=tr[l];
push_down(pos);
tr[pos].r=merge(tr[pos].r,r);
push_up(pos);
return pos;
}
else
{
tr[pos]=tr[r];
push_down(pos);
tr[pos].l=merge(l,tr[pos].l);
push_up(pos);
return pos;
}
}
void insert(int &pos,int xx,int yy)
{
split(pos,xx,dl,dr);
dl=merge(dl,get_rand(yy));
pos=merge(dl,dr);
}
void delet(int &pos,int xx)
{
split(pos,xx-1,dl,dr);
split(dr,1,temp,dr);
pos=merge(dl,dr);
}
void reverse(int &pos,int l,int r)
{
split(pos,l-1,dl,dr);
split(dr,r-l+1,temp,dr);
tr[temp].rev^=1;
dr=merge(temp,dr);
pos=merge(dl,dr);
}
LL query(int &pos,int l,int r)
{
split(pos,l-1,dl,dr);
split(dr,r-l+1,temp,dr);
LL ans=tr[temp].sum;
dr=merge(temp,dr);
pos=merge(dl,dr);
return ans;
}
int main()
{
scanf("%d",&n);
int op,v,xx,yy;
LL lan=0;
for(int t=1;t<=n;t++)
{
scanf("%d %d",&v,&op);
rt[t]=rt[v];
if(op==2)
{
scanf("%d",&xx);
xx^=lan;
delet(rt[t],xx);
}
else
{
scanf("%d %d",&xx,&yy);
if(op==1)
{
xx^=lan,yy^=lan;
insert(rt[t],xx,yy);
}
else if(op==3)
{
xx^=lan,yy^=lan;
reverse(rt[t],xx,yy);
}
else
{
xx^=lan,yy^=lan;
lan=query(rt[t],xx,yy);
printf("%lld\n",lan);
}
}
}
return 0;
}
一定要注意的是,每次操作,不管是合并、分离还是push_down,只要影响到树形和树中节点的信息,就必须新建节点来储存,所以fhq-treap的可持久化格外的费空间,不过现在空间都给的足,所以一般都能过。用fhq-Treap支持区间翻转和可持久化,需要注意的是和区间修改主席树一样,在push_down翻转rev的时候需要复制左右儿子结点,把rev推到新复制的结点上。
多体会下,就能很容易理解啦,在本蒟蒻看来,其实平衡树比线段树简单多了 qwq
好了,现在已经快入坟了,就差轻轻一推了,那么它来啦!!! 作为平衡树五题的压轴题目,也是平衡树的另一大考点:【模板】二逼平衡树(树套树) - 洛谷 相信有人已经看不下去了,本蒟蒻也是到这里才体会到了平衡树的快乐 ,当本蒟蒻听到树套树之后,直接兴奋的半个小时交了一发,然后快乐的只过了一个点。在听完课后,虽然本蒟蒻会写了,但是,但是!,又交了一发,只过了90分(关键是就我第一次提交过的那个点没过——离离原上谱) ,虽然后来我看题解用树状数组套平衡树过了,However,这和我线段树套平衡树的初衷不相符,所以这里就不贴代码,就稍微解释一下吧(尬)。
对于此题,我们可以发现,如果单纯的求前驱和后继,排名,数值,用一棵fhq-treap来维护绰绰有余,然而,此题异常毒瘤,它让我们在一个区间中进行上述操作。区间? 这不又是线段树嘛,欸,猜对了。又题目的提示可以知道:我们要在一棵维护区间的线段树上做平衡树,也就是下标线段树套值域平衡树,线段树的每个节点都是一段区间,所以我们要在线段树的每个节点上开一个平衡树,这样就可以轻松地维护信息啦——但是,这样也会有问题,我们在查询排名为k的数时,由于答案需要多棵不可合并的平衡树一起提供,所以我们要在线段树二分区间(logn)的前提下在平衡树中二分答案(logn*logn),那么复杂度就会到达 log^3(n),这样又会被出题人卡死。怎么办呢,为了降低复杂度,我们可以利用线段树的二分性,把树套树改成 下标平衡树套值域线段树,这样复杂度就会将下来,具体实现推荐去看肖然大佬的blog 【树套树】 - 肖然 的博客 - 洛谷博客文章来源:https://www.toymoban.com/news/detail-604978.html
本蒟蒻就水到这了,反正我是已经入坟躺平了,希望各位神犇大佬AC顺利 Orz文章来源地址https://www.toymoban.com/news/detail-604978.html
到了这里,关于FHQ-Treap(非旋treap/平衡树)——从入门到入坟的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!