FHQ 平衡树 算法笔记
使用搜索树的目的之一是缩短插入、删除、修改和查找(插入、删除、修改都包括查找操作)节点的时间。关于查找效率,如果一棵树的高度为h,那么我们平衡树的复杂度最坏情况下就是O(h) 然而我们理想的二叉搜索树是可以把复杂度降至为 logn 一次的,这就是我们平衡树存在的意义和作用。 二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树。二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:1.左子树所有权值小于根 2.右子树所有权值大于根首先
FHQ 平衡树有两个关键函函数——split,merge
也就是说我们舍弃了平衡树的旋转,转而用另一种方向去解决平衡树的操作。
FHQ 平衡树性质跟其他平衡树一样,能解决单点修改/查询 log(n) ,区间查找 log(n) ,以及等等平衡树能做到的操作
实现过程也是比较简单。我们舍去传统平衡树的旋转,转而选择分裂这颗平衡树,我们即希望它能有着二叉搜索树的性质,所以我们就运用一个随机权值来稳定它的二叉搜索树的性质和形态(这个随机数据期望深度是 log(n) 的,所以能保证复杂度)然后我们进行的查询操作分为两种——子树和权值,顾名思义,这两类维护的东西也有所不同,根据我当前理解而言,子树一般是用来维护序列的性质(例如反转,下传标记之类的),而权值则一般是用来维护一些跟序列数有关的内容(类似于查询前驱后继之类的)
Split
split操作主要是根据我们在平衡树中维护的值亦或是子树大小来进行分裂操作。因为我们的高度是期望是 logn 的,所以时间复杂度大致也是O(logn)
权值版
void split(int now,int k,int &x,int &y) {
if(!now) x=y=0;//如果搜不到了直接清零
else {
if(val[now]<=k) {
x=now;
split(ch[now][1],k,ch[now][1],y);
} else {
y=now;
split(ch[now][0],k,x,ch[now][0]);
}
update(now);
}
}
这个代码的主要原理就是根据二叉搜索树的性质,我们左儿子应该是要比根节点小,而右儿子是比根节点要大的,那么我们如果发现我们的根如果小于等于我们需要分裂的要求权值的话,就直接去分裂右儿子,反之亦然。这里运用了递归函数的调用,x,y的值直接更改,其中now表示我们目前到了那颗子树,x,y分别表示我们左儿子和右儿子要往上传递的根,也就是我们目前更改状态下新的根。
解释第一个的情况。当我们发现根节点是小于等于k时,我们就进入右儿子,因为只有右儿子才有可能分裂,所以我们要分裂的方向就是朝右儿子去分裂,自然我们目前所划分的序列未知的是x所以我们右儿子写道右儿子的根上进行修改。
子树版
子树操作跟权值版几乎一样,就是我们每次的k可能需要减
void split(int now,int k,int &x,int &y) {
if(!now) {
x=y=0;
return ;
}
if(siz[ch[now][0]]<k) {
x=now;
split(ch[now][1],k-siz[ch[now][0]]-1,ch[now][1],y);
} else {
y=now;
split(ch[now][0],k,x,ch[now][0]);
}
update(now);
}
Merge
merge操作主要是根据我们随机的key值,把整个数按照key值维护成一个小根堆,使这个平衡树维持平衡的形态,也去保证复杂度。
如果key值变成一条链的概率跟核弹直接创你脸上的概率一样——lcrh
所以说key值是可以保证复杂度的,但是在某些题里面我们需要维护的不是key值,这会在后面的题目中说明
int merge(int x,int y) {
if(!x || !y) return x+y;
if(key[x]<key[y]) {
ch[x][1]=merge(ch[x][1],y);
update(x);
return x;
} else {
ch[y][0]=merge(x,ch[y][0]);
update(y);
return y;
}
}
具体思路其实很简单,因为我们上面按照权值分了两颗子树,那么也就是说第一颗子树内的值一定是比第二棵要小的,而且因为我们的key值关系,整个树的结构其实是稳定的,就算是分裂也无法改变我们的树的具体形态。这就是fhq的巧妙之处。
前驱&后继&排名
前驱:我们再次之前先把v-1分裂成两颗子树在x里面寻找最大值,就是把siz减完
后继:跟前驱差不多,找第一个
排名:直接找就行
int kth(int now,int k) {
while(1) {
if(k<=siz[ch[now][0]]) now=ch[now][0];//先看前面有没有小的
else if(k==siz[ch[now][0]]+1) return now;
else k-=siz[ch[now][0]]+1,now=ch[now][1];
//根据二叉搜索树的性质,左儿子的所有值小于根,右儿子一定大于根(建树时就确定的)
}
}
tree.split(root,a-1,x,y);
printf("%d\n",val[tree.kth(x,siz[x])]);//前驱
root=tree.merge(x,y);
tree.split(root,a,x,y);
printf("%d\n",val[tree.kth(y,1)]);//后继
root=tree.merge(x,y);
好了,基础部分到此为止,后面是实践
P3391 【模板】文艺平衡树
这道题跟普通的平衡树相比就是要旋转,而且连修改都没有,根本不用担心树的基本位置会发生变化。所以说直接在修改的时候记录要旋转子树的根,打上标记(就是^)等到后面求值得时候直接把左右子交换然后继续递归就可以了。
关键代码
其实跟之前我们说的求排名的函数没有太大区别,主要是要交换左右子
void down(int x) {
swap(ch[x][0],ch[x][1]);//相当于一个标记下传,不实际改变位置而是改变搜索左右子顺序就可以了
if(ch[x][0]) fl[ch[x][0]]^=1;
if(ch[x][1]) fl[ch[x][1]]^=1;
fl[x]=0;
}
void solve(int i){ //先左再中再右
if(!i) return;
if(fl[i]) down(i);
solve(ch[i][0]);
printf("%d ",val[i]);
solve(ch[i][1]);
}
P3960 [NOIP2017 提高组]
列队这道题就是开n+1个平衡树,维护每一行的线段,然后每次查找就把他分离出来,把一段线段分成3段(原+原+新)因为每次我们都需要从后面加进来一个,然后我们再最后一列维护一个平衡树,维护每个进入新的数。因为每次修改只会增加2个点,而我们只会询问 3\times1e5 次所以空间是不会爆的
关键代码
void split_new(int now,int k) {
if(k>=r[now]-l[now]+1) return ;
int want=l[now]+k-1;
int neww=new_node(want+1,r[now]);
r[now]=want;
ch[now][1]=merge(neww,ch[now][1]);
update(now);
}
//每次往后分裂的时候就把线段分成4个
P2611 [ZJOI2012]小蓝的好友
首先我们发现这道题的标签是平衡树
我们发现这个东西的纵深值其实可以用平衡树维护,我们确定一个加值的模式,为了避免算重,我们可以用一个平衡树记录每一个纵坐标上的最深值,也就是类似下面的图。
平衡树示意图
题意示意图
我们可以用这样子的方式来统计答案,利用扫描线的思想,计算矩形底在当前计算的点的纵坐标(y)上,也就是方案的计算数就是
y * (x+1) * (C-x+1)
用FHQ_treap维护,如果在同一横排的平衡树就不会记入之前算过的子树。这样子我们就不用算重了。只需要改成:
y * ({size}_l+1) *({size}_r+1)
注:如果当前横排没有新的点的话直接把上一横排的答案记录,相当于y+1
CODE+注释
