FHQ Treap:不用旋转的treap,还能维护区间!
FHQ(范浩强) Treap:利用了treap的结构(每个节点上的一个新值域整体上满足堆性质),却简化了很多操作(不用旋转),核心操作2个函数,助您深刻理解“函数式编程”的意义!
提前说明一下,为了方便,FHQtreap中每个节点只存一个值,即就算有若干相同的值,他们也要建若干节点存起来而不是只建一个节点存。
核心操作:
1、分裂(split):
分裂分两种:按值和按排名。
按值分裂:按值x将1个treap分成2个,其中一个所有节点的键值小于等于x,另一个则大于x的treap。这里分裂过程是在原treap上从上到下一部分一部分拆开合成两个treap的。
从原树根节点开始看当前节点,若当前节点的键值小于等于x,则可以把当前节点及其左子树归到“小树”里,再看它的右子树;若当前节点的键值大于x,则可以把当前节点及其右子树归到“大树”里,再看它的左子树。
void split(int now,int x,int &u,int &v) { if(!now) { u=v=0; return; } if(num[now]<=x) { u=now; split(tre[now][1],x,tre[now][1],v); } else { v=now; split(tre[now][0],x,u,tre[now][0]); } updata(now); }
按排名(个数)分裂:思路与上类似,注意看右子树是排名要减(左子树的大小+1)
void splith(int now,int k,int &u,int &v) { if(!now) { u=v=0; return; } if(siz[tre[now][0]]>=k) { v=now; splith(tre[now][0],k,u,tre[now][0]); } else { u=now; splith(tre[now][1],k-siz[tre[now][0]]-1,tre[now][1],v); } updata(now); }
2、合并(merge):
将两棵树合并,其中小树所有节点键值都小于大树(这是重点),从两个树的根节点看起当前两个节点,若小根的强化值小于等于大根的强化值,则小根及其左子树成为新树的一部分,新右子树为小根的右子树与剩下的大树merge后的结果;若小根的强化值大于大根的强化值,则大根及其右子树成为新树的一部分,新左子树为大根的左子树与剩下的小树merge后的结果。这样满足treap的二叉查找树和堆的性质。
int merge(int u,int v) { if(!u||!v) return u+v; if(dev[u]<=dev[v]) { tre[u][1]=merge(tre[u][1],v); updata(u); return u; } else { tre[v][0]=merge(u,tre[v][0]); updata(v); return v; } }
一定要注意merge()的第一个参数传的是当前小树的根,第二个参数传的是当前大树的根。
(3) 查找排名第k大的数(辅助函数)
与二叉搜索树无异。
int fin(int u,int k) { if(siz[tre[u][0]]>=k) return fin(tre[u][0],k); if(siz[tre[u][0]]+1==k) return num[u]; return fin(tre[u][1],k-siz[tre[u][0]]-1); }
实现操作:
几乎所有操作都能由上面三个函数配合完成,代码很直白
1、插入数x;2、删除数x;3、查询数x的排名;4、查找排名为x的数;5、求数x前驱;6、求数x后继。
opt=read(),x=read(); switch(opt) { case 1: split(root,x,u,v); v=merge(newnode(x),v); root=merge(u,v); break; case 2: split(root,x,u,v); split(u,x-1,u,w); w=merge(tre[w][0],tre[w][1]); u=merge(u,w); root=merge(u,v); break; case 3: split(root,x-1,u,v); printf("%d\n",siz[u]+1); root=merge(u,v); break; case 4: printf("%d\n",fin(root,x)); break; case 5: split(root,x-1,u,v); printf("%d\n",fin(u,siz[u])); root=merge(u,v); break; case 6: split(root,x,u,v); printf("%d\n",fin(v,1)); root=merge(u,v); break; }
注意一般情况下,修改/查询操作的最后要再将所有分裂的树合并起来。
7、区间操作:
此时节点的键值存的是区间下标,若要修改/查询区间[l,r]的元素,只需将原树按r分裂,再对小树按l-1分裂,这是分裂出的大树即为[l,r],此时再输出查询结果/打标记就行了。若一个点有标记,则在其子树增或减点时都要下传标记。
说一下用平衡树维护区间的注意点:
若用平衡树维护区间,分裂多是按排名(个数)分裂。因为所有操作都不会改变它二叉搜索树的性质,所以用排名代表下标就好。
插入:若要对一个完整序列建立平衡树,可顺序merge到树里。若要再第i个数后插入一个数x,可先按排名i分裂成两树,加上新节点,逐一merge起来就好。
其他操作类似。其实用平衡树维护区间的话,它用于满足二叉搜索树的性质的键值就是元素在序列中的下标。不过,因为所有操作都不会改变它二叉搜索树的性质,所以甚至连这个键值都不用花内存存,直接存要维护的元素的数据就好。
可联系一下splay的相关部分:Splay详解
遇到的问题(坑)
rand()范围: 0~32767(RAND_MAX)
const int &x作为函数形参,函数中不能对x直接赋值,但可以利用地址修改x的值。不要认为有了const int &x后x的值就一定不会被手残改掉。