FHQ-treap 学习笔记
FHQ-treap 类似带旋treap 每个点维护键值对 依靠随机维护的形态
但它不需要复杂的旋转操作 主要依靠暴力分裂和合并 代码极其好写
常数较小 仅次于带旋treap 但代码难度远小于带旋treap
int siz[N],ls[N],rs[N],rd[N],val[M],cnt;
首先维护如下信息:
- \(siz\) :子树大小
- \(ls/rs\) :当前节点左/右儿子
- \(val\) :节点值
- \(rd\) :随机键值
- \(cnt\) :节点数
pushup
inl void pushup(int k){siz[k]=siz[ls[k]]+siz[rs[k]]+1;}
很好理解 维护子树大小
split
FHQ-treap 经典操作
inl void split(int k,int x,int &l,int &r){
if(!k)return l=r=0,void();
if(val[k]<=x){
l=k;
split(rs[k],x,rs[k],r);
}else{
r=k;
split(ls[k],x,l,ls[k]);
}
pushup(k);
}
\(split(rt,x,l,r)\) 会把 \(rt\) 为根的子树拆成 \(\le x\) 和 \(>x\) 两部分
并返回他们的根 即为 \(l,r\)
如果 $x\ge $当前点值 那么左子树一定为 \(\le x\) 部分 把 \(l\) 设为 \(k\)
此时右子树可能还存在值 \(\le x\) 的点 所以向右子树递归 把剩余的部分接到当前点右儿子上
另一种情况同理
merge
inl int merge(int x,int y){
if(!x||!y)return x+y;
if(rd[x]<=rd[y]){
rs[x]=merge(rs[x],y);
return pushup(x),x;
}else{
ls[y]=merge(x,ls[y]);
return pushup(y),y;
}
}
\(merge(x,y)\) 会把以 \(x\) 和 \(y\) 为根的子树合并 并返回根节点的值
如果有一个是叶子节点 那么就并完了 返回另一个即可
然后这里注意 \(merge\) 函数要求每次调用都保证 \(y\) 子树内元素值 \(\ge\) \(x\) 子树内元素值
否则左右儿子值是乱的 无法保证左子树值 \(<\) 右子树值
或者你合并前特判 \(swap\) 一下也行)
但无论如何 一定把值更大的 \(y\) 并到 \(x\) 右儿子 或者 值小的 \(x\) 并到 \(y\) 的左儿子
求x数的排名等平衡树常规操作
int x=read();
int p,q;
treap.split(rt,x-1,p,q);
cout<<treap.siz[p]+1<<endl;
rt=treap.merge(p,q);
把 \(<x\) 的数拆出来 排名就是子树大小+1 然后在并回去
注意FHQ每次插入都会新开节点 不同于splay和带旋treap
所以朴素求排名还有如下写法:
inl int rank(int k,int x){
if(!k)return 1;
if(val[k]>=x)return rank(ls[k],x);
if(val[k]<x)return siz[ls[k]]+1+rank(rs[k],x);
}
其余操作相同
inl int pre(int k,int x){
if(!k)return -inf;
if(val[k]>=x)return pre(ls[k],x);
return max(val[k],pre(rs[k],x));
}
inl int suc(int k,int x){
if(!k)return inf;
if(val[k]<=x)return suc(rs[k],x);
return min(val[k],suc(ls[k],x));
}