FHQ平衡树
FHQ平衡树
大致是这样的题目:
您需要动态地维护一个可重集合 M M M,并且提供以下操作:
- 向 M M M 中插入一个数 x x x。
- 从 M M M 中删除一个数 x x x(若有多个相同的数,应只删除一个)。
- 查询 M M M 中有多少个数比 x x x 小,并且将得到的答案加一。
- 查询如果将 M M M 从小到大排列后,排名位于第 x x x 位的数。
- 查询 M M M 中 x x x 的前驱(前驱定义为小于 x x x,且最大的数)。
- 查询 M M M 中 x x x 的后继(后继定义为大于 x x x,且最小的数)。
对于操作 3,5,6,不保证当前可重集中存在数 x x x。
对于一颗二叉搜索树来说,确实支持上述所有操作,但是如果构造一组递增或者递减序列,那么二叉搜索树就会退化成一条链,那这样时间复杂度也就退化成了 O ( n ) O(n) O(n) ,这显然不是我们想要的。
所以考虑平衡树,平衡树是指左右儿子大小相差不超过一的二叉搜索树。但是传统的平衡树的旋转很麻烦,所以这里采用一种无旋平衡树的实现方式。
FHQ平衡树,在每个节点不仅维护它的权值的时候,还维护一个 k e y key key 值,然后我们规定除了权值需要满足 左儿子<根节点<右儿子 外,仍需满足 k e y key key 值 :根节点>左儿子 且 根节点 >右儿子。
也就是说对于权值,我们使这棵树满足二叉搜索树的性质,对于 k e y key key 值,我们使这棵树满足大根堆的性质。可以证明的是,如果 k e y key key 值足够随机,那么这颗树的形态就是唯一的。所以我们只需要构造 k e y key key 值使得其满足随机性即可。
接下来需要掌握的前置知识是:
-
对于一颗FHQ平衡树,如何将其分裂为 ≤ v \leq v ≤v 和 > v \gt v >v 的两部分。
有点类似动态开点的思想,分裂的时候 如果当前根节点的权值 ≤ v \leq v ≤v ,那么根节点及其左子树均可以分到左边的部分,然后递归访问其右儿子继续分即可。反之可以将根节点及其右子树分给右边的部分,然后递归访问左儿子继续分。注意:在此过程中,需要动态的修改某些节点的左右儿子,所以用传地址的方式,具体详见代码。
-
对于两颗FHQ平衡树,如何将其合并。
考虑到两颗树,一颗为A树(左),一颗为B树(右),必定有一颗树的根节点是总树的根节点,并且由于二叉搜索树的性质,两棵树的相对位置必定是不变的。那么如果A树的根节点的 k e y key key 值 大于 B树的根节点的 k e y key key 值,那么A树根节点及其左儿子是不用动的,只需要将B树放到A的右儿子上即可,但是A树的右儿子怎么办呢,我们可以让A树的右子树和B树再进行合并,实质上就是一次递归了。反之亦然。搞定!
接下来我们搞定分裂和合并了,那么插入删除以及其他操作不就解决了吗!(bushi)
-
考虑插入权值为 v v v 的节点:
只需要将原树分裂成 ≤ v \leq v ≤v 和 > v \gt v >v 的两部分,然后新建节点,三部分按次序合并即可。
-
删除权值为 v v v 的节点:
只需要分裂为 < v 、 = v 、 > v \lt v 、 =v 、 \gt v <v、=v、>v 的三部分即可,如果删除权值为 v v v 的所有节点,那么只需合并 < v 、 > v \lt v 、 \gt v <v、>v 的部分即可,如果删除单个,那么只需要删除 = v =v =v 树的根节点,即需要合并这个部分的左右子树(不合并根节点)不就相当于删除单个节点了吗。
-
找出多少个数比 x x x 小:
即分裂为 ≤ x \leq x ≤x 的部分的树的 s i z e size size
其余的操作同理,过于简单就不说了。
FHQ实现文艺平衡树
您需要写一种数据结构(可参考题目标题),来维护一个有序数列。
其中需要提供以下操作:翻转一个区间,例如原有序序列是 5 4 3 2 1 5\ 4\ 3\ 2\ 1 5 4 3 2 1,翻转区间是 [ 2 , 4 ] [2,4] [2,4] 的话,结果是 5 2 3 4 1 5\ 2\ 3\ 4\ 1 5 2 3 4 1。
这个问题,实质上就不能按权值进行分裂了,我们可以按照树的 s i z e size size 进行分裂,这样是不会改变元素相对位置的。
对于一颗FHQ平衡树来说,维护有序区间只需要按次序插入即可,按 s i z e size size 分裂也可以随时分裂出这个序列的任何一个子区间,那么这个题目就简单了。
对于翻转 [ l , r ] [l,r] [l,r] 这段区间来说,只需要先分裂出这段区间,然后由于中序遍历是可以得出这段序列的,那翻转就是将每个节点的左右子树交换即可,但是这样的时间复杂度太高,我们可以像线段树一样在每个节点上维护一个懒标记,需要的时候下传即可。
需要注意的是,每次 s p l i t split split 以及 m e r g e merge merge 可能会改变节点的左右儿子,所以我们需要在 s p l i t split split 和 m e r g e merge merge 前进行标记的下传。
代码:
mt19937 rnd(time(NULL));
struct node{int sz,v,key,l,r,f;node(){this->sz=this->v=this->key=this->l=this->r=this->f=0;this->key=rnd();}node(int v){this->sz=this->v=this->key=this->l=this->r=this->f=0;this->sz=1;this->key=rnd();this->v=v;}
};
struct fhq{node tr[100020];int tot=0,root=0;void pushup(int root){tr[root].sz=tr[tr[root].l].sz+tr[tr[root].r].sz+1;}void pushdown(int root){if(!tr[root].f)return ;swap(tr[root].l,tr[root].r);tr[root].f^=1;tr[tr[root].l].f^=1;tr[tr[root].r].f^=1;}void split(int root,int v,int &x,int &y){//v分裂if(!root){x=y=0;return ;}pushdown(root);if(tr[root].v<=v){x=root;split(tr[root].r,v,tr[root].r,y);}else {y=root;split(tr[root].l,v,x,tr[root].l);}pushup(root);}void split_sz(int root,int v,int &x,int &y){//sz分裂if(!root){x=y=0;return ;}pushdown(root);if(tr[tr[root].l].sz+1<=v){x=root;split(tr[root].r,v-(tr[tr[root].l].sz+1),tr[root].r,y);}else {y=root;split(tr[root].l,v,x,tr[root].l);}pushup(root);}int merge(int x,int y){if(!x||!y)return x|y;if(tr[x].key<tr[y].key){pushdown(x);tr[x].r=merge(tr[x].r,y);pushup(x);return x;}// else {pushdown(y);tr[y].l=merge(x,tr[y].l);pushup(y);return y;// }}void insert(int v){int x,y;split(this->root,v-1,x,y);tr[++tot]=node(v);this->root=merge(merge(x,tot),y);}void del(int v){int x,y,z;split(this->root,v-1,x,y);// x [1,v-1]split(y,v,y,z);// y [v,v] z [v+1,n]// 删除单个vthis->root=merge(x,merge(tr[y].l,tr[y].r));this->root=merge(this->root,z);}int find_min(int v){//查询<v的个数int x,y;split(this->root,v-1,x,y);int ans=tr[x].sz;this->root=merge(x,y);return ans;}int find_xth(int x){//查询排名第x位的数int now=this->root;while(now){if(tr[tr[now].l].sz+1==x)return tr[now].v;if(tr[tr[now].l].sz+1>x){now=tr[now].l;}else {x-=tr[tr[now].l].sz+1;now=tr[now].r;}}return -1; // 无}int find_pre(int v){// 找小于v最大的int x,y;split(this->root,v-1,x,y);int now=x;while(tr[now].r)now=tr[now].r;this->root=merge(x,y);return tr[now].v;}int find_suf(int v){// 找大于v最小的int x,y;split(this->root,v,x,y);int now=y;while(tr[now].l)now=tr[now].l;this->root=merge(x,y);return tr[now].v;}void reverse(int l,int r){int x,y,z;split(this->root,l-1,x,y);// x [1,l-1]split(y,r-l+1,y,z);// y [l,r] z [r+1,n]tr[y].f^=1;this->root=merge(merge(x,y),z);}void print(int root){if(!root)return ;pushdown(root);print(tr[root].l);cout<<tr[root].v<<' ';print(tr[root].r);}
}T;