非旋平衡树(fhq-treap)详解
主要参考:FHQ-Treap(非旋treap/平衡树)——从入门到入坟_hsez_yyh的博客-CSDN博客_fhq树
平衡树这玩意吗,首先他的是一棵树对吧,而且得是一颗二叉树
肯定还得是个二叉搜索树吧,
于是我们可以定义如下结构体:
struct node{ int l,r,key,val,si; }tr[200010];
其中si表示整个以此节点为根的子树的大小,val表示优先级。key就是此节点所带的键值
平衡树就是建立在键值与优先级的基础上的。
顾名思义,平衡树,肯定得让这棵树左右两边平衡而不是差距过大,来保持logn级的复杂度,优先级就是这个目的
这时我们需要引出一个重要的性质:
对于任意fhq-treap中的节点 i ,其左子树上的所有节点的key小于等于 i 节点的key值,i 节点所有右子树上所有节点的key 大于等于 i 节点的key值;对于任意节点 i ,其左、右儿子的val值大于等于 i 的val值
嗯,似曾相识。
没错,与小根堆一样,不然你以为treap这名字怎么来的?(treap = tree+heap)
但在这里还是提醒一句:千万别记错了,不然直接爆零(应该没人会比我傻吧)
fhq-treap相较于splay,有着一个不会退化成链好处,还有一个具有争议的部分,时间复杂度,究竟是o(nlogn)还是O(nlog^2(n))
我是不知道了,还请读者自行判断(偷懒了)个人偏向第一种(主要是我算不来啊)
接着讲第二点
分裂与合并操作,主要作用就是代替旋转:
分裂,一看就知道是将树扯成两半,只不过是按照一个参照来分裂
众所周知,二叉搜索树所有的左子树小于根而右子树大于根
根据这一点,我们即可以把树分为小于参照的与大于参照的
下面代码中的x即为参照,p为当前位置l和r即是指向左右子树
void split(int p,int x,int &l,int &r){ //p为当前所在节点,l,r,为分裂后的值,这里用指针是方便传递值(应该不会有人认为这值就不用了吧) if(!p){//判断此树是否为空 l = r = 0; return ; } if(tr[p].key<=x){//当参照大于等于当前节点键值,左子树全部小于当前节点键值,所以全部小于参照 l = p;//当前节点为其左子树 split(tr[l].r,x,tr[l].r,r);//继续往右子树 }else{//当参照小于当前节点键值,右子树值全部大于键值 r = p;//当前节点为右子树 split(tr[r].l,x,l,tr[r].l);//继续往左子树找 } pushup(p);//与线段树一个道理,向上更新 }
合并吗,就是把分出来的两半再合一起呗
当然,这个就没有参照了,反正就比个大小然后就并起来了
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); pushup(l);//别忘了更新 return l; }else{ //同理 tr[r].l = merge(l,tr[r].l); pushup(r); return r; } }
第三点:
插入与删除
插入吗,也就是建树,我们只需要给这个需要插入的元素找个合适的位置就行了
原来嘛自然是用旋转,但我们现在有了分裂与合并操作
插入自然是先分裂,给x找出位置后再合并
删除就是将树分裂成三部分,再将除了x的两部分合并起来,还需要记录一下根节点
void insert(int x){ split(rt,x-1,dl,dr); rt = merge(merge(dl,getrand(x)),dr); } void delet(int x){ split(rt,x-1,dl,dr); split(dr,x,tmp,dr); tmp = merge(tr[tmp].l,tr[tmp].r); rt = merge(dl,merge(tmp,dr)); }
第四点:
查询x的排名,这个应该不用多说
int getrk(int x){ split(rt,x-1,dl,dr); int rnk = tr[dl].si+1; rt = merge(dl,dr); return rnk; }
第五点:
查询前驱与后继,查询排名为x的数
此处思想就为二叉搜索树
若要查前驱,getnum(dl,tr[dl].si);因为前缀是<=x中排名最后的
若要查后继,getnum(dr,1);因为后缀即为>x中排名第一的
查询排名为x的数就是直接getnum(root,x);
读者可自己揣摩揣摩
int getnum(int p,int x){ int u = tr[tr[p].l].si+1; if(u==x) return tr[p].key; if(u>x) return getnum(tr[p].l,x); else return getnum(tr[p].r,x-u); }
于是乎,将这些结合在一起,我们就能A了这道板子题(题目链接)
最后说一句,多多手动模拟,会对其理解更深(不是我想整你们·,真不是)
此处为维护值域的平衡树,过几天再补上一个维护序列的
蒟蒻瞎扯完毕
2022-10-28 16:44:43
更新一手
fhq-treap维护区间修改
此时,split函数需要重新写,void split(int p,int x,int &l,int &r)的意思不再是分离<=k和>k的两棵树
而是变为了 将序列p的前x个元素分离出来形成一棵新fhq-treap的l子树,另外的一部分形成另一颗 fhq-treap的r 子树
所以,split你会发现写法像极了前面的普通平衡树的getnum函数
void split(int p,int x,int &l,int &r){ if(!p){ l = r = 0; return ; } pushdown(p); int u = tr[tr[p].l].si+1; if(u<=x){ l = p; split(tr[l].r,x-u,tr[l].r,r); pushup(l); }else{ r = p; split(tr[r].l,x,l,tr[r].l); pushup(r); } }
完整版例题+代码可以看这里
梦与现实间挣扎着,所求为何
你可以借走我的文章,但你借不走我的智慧 虽然我是傻逼本文来自博客园,作者:cztq,转载请注明原文链接:https://www.cnblogs.com/cztq/p/16835888.html