前言
介绍
- 平衡树有大量的写法,代表有 \(Treap\),\(Splay\),替罪羊树等,msjing学的是无旋 \(Treap\)
其实msjing还学了平板电视
- \(Treap\),平衡树的一种,依据 \(BST\) 和 \(Heap\) 实现,不像有旋 \(Treap\) 一样需要依据旋转维护平衡
- \(FHQ\)-\(Treap\),是一种无旋的 \(Treap\) 且是非常常用的,是一个单 \(\log\)
但是常数飞起来
实现及维护
- \(FHQ\)-\(Treap\) 的实现依据堆性质和 \(BST\),先说 \(BST\)
\(BST\)
- \(BST\),全称二叉搜索树,其左子树上所有点的附加权值均小于其根节点的值,右子树上所有点的附加权值均大于其根节点的值, 就是中序遍历是有序的
- 那么我们发现 \(BST\) 上可以做很多东西,但是由于 \(BST\) 不保证树高,即不保证平衡,因此所有的操作都是 \(O(H)\) 的,其中 \(H\) 为树高,那么我们发现,\(BST\) 可能退化成链,就太长了
- 所以我们利用堆来维护
- 如何利用堆来维护呢
堆
- 堆就不必多说了,
priority_queue 都用过吧,这就是堆了
- 那么我们如何利用堆让树平衡呢,我们可以直接 \(rand\) 一个值给每个节点,按堆和 \(BST\) 的要求搞一下,然后就行了
从oiwiki偷张图吧,woc偷不了

sbmqwm:图都能贺啊msjing你无敌了
- 黑色字的是 \(BST\) 的,蓝色的是堆的
然后经过一阵 \(nb\) 的复杂的证明,可以证得树高是 \(log_n\) 的,树高保证了,我们就可以搞许多东西了
\(Split\) & \(Merge\)
- \(Split\) 和 \(Merge\) 是 \(FHQ\) 重要的维护平衡的操作,也是前面说的与别的平衡树不一样的地方(由于树的结构是确定的),是全部操作的基础
\(Split\)
- \(Split\) 操作主要是依据 \(v\) 把树分裂成小于等于 \(v\) 和大于 \(v\) 两部分,具体怎么实现呢
- 首先我们先搞两个
滚木虚空节点,利用取地址符来直接对这两个点改动
- 然后依据 \(v\) 向下递归,遇到比 \(v\) 小或等的就直接把这个点和它的左子树全塞到左边的虚空节点上,左边的虚空节点改为这棵树的右儿子,并递归这个点的右儿子,遇到大的同理,这是依据 \(BST\) 的性质做的,很难理解,
再偷张图

- sbmqwm:如果觉得msjing讲的不清,请前往这里学一下,msjing也是学的这个,我去紫菜一下msjing
一段美好的时光,请脑补
- msjing:sbmqwm又欺负我555
- 回归正题,这样做就可以分开树了,实现是贺的\(Super\)_\(lollipop\)的,给一下吧
点击查看代码
void Split(int now,int v,int &x,int &y)
{
if (!now) return x=y=0,void();
if (val[now]<=v) x=now,Split(rs[now],v,rs[now],y);
else y=now,Split(ls[now],v,x,ls[now]);
Pushup(now);
}
\(Merge\)
- \(Merge\)操作是把分出两棵树合并,具体如何合并呢
- 由于我们分出的两棵树有:左子树小于右子树的性质且区间不交,故仅需搞一下我们随机出来的值就行,所以合并依据随机出来的 \(rnd\)
- 什么是区间不交,我们看上图,左子树表示区间 \([1,5]\),右子树则是 \([6,7]\),这就是区间不交
- 再粘一遍要不然看得费劲


- 我们看两张图上图(后图也是那篇文章的图),已经分裂好的两棵树(\(rnd\) 以下图为准),其根分别为 4 和 6,发现 \(rnd_4 < rnd_6\),所以 6 应该在 4 的下面,并向下递归其右子树到达 5,发现 \(rnd_5 > rnd_6\),所以 5 应该在 6 的下面,所以 5 成为 6 的儿子,6 成为 4 的儿子,就还原成上图了

- 至此两个关键操作就搞完了
点击查看代码
int Merge(int x,int y)
{
if (!x || !y) return x|y;
if (rnd[x]<rnd[y])
{
rs[x]=Merge(rs[x],y);
Pushup(x);
return x;
}
else
{
ls[y]=Merge(x,ls[y]);
Pushup(y);
return y;
}
}
其他操作
\(New\)
- 新建一个节点,好搞,记得再 \(rd\) 一个权值就行
点击查看代码
int New(int v)
{
tot++,val[tot]=v,rnd[tot]=rd(),siz[tot]=1;
return tot;
}
\(Pushup\)
点击查看代码
void Pushup(int x) {siz[x]=siz[ls[x]]+siz[rs[x]]+1;}
\(Insert\)
- 加入一个点,直接依据加的点把树裂开加然后和就行了
常数起飞了
点击查看代码
void Insert(int a)
{
int x,y;
Split(root,a,x,y);
root=Merge(Merge(x,New(a)),y);
}
\(Delete\)
点击查看代码
void Delete(int v)
{
int x,y,z;
Split(root,v,x,z);
Split(x,v-1,x,y);
y=Merge(ls[y],rs[y]);
root=Merge(Merge(x,y),z);
}
\(fid\) 全家桶
\(fid\)_\(kth\)
- 找 \(k\) 的排名,递归找,如果左儿子的个数比 \(k\) 小,让 \(k - siz_{ls_x} - 1\),然后递归进入右子树,否则递归进入左子树。当 \(k = siz_{ls_x}\) 的时候就找到这个值了
\(fid\)_\(siz\)
\(fid\)_ \(prev\) & \(fid\)_\(nxt\)
- 查前驱后继,暴力拆树,前驱按 \(<v\) 和 \(\ge v\) 拆,左子树的第 \(siz_x\) 大(最大的)就是了,后继同理,按 \(\le v\) 和 \(> v\) 拆,右子树第一小就是
点击查看代码
int fid_kth(int x,int k)
{
while (1)
{
if (k<=siz[ls[x]]) x=ls[x];
else if (k == siz[ls[x]]+1) return x;
else k-=(siz[ls[x]]+1),x=rs[x];
}
}
int fid_siz(int v)
{
int x,y;
Split(root,v-1,x,y);
int k=siz[x]+1;
root=Merge(x,y);
return k;
}
int fid_prev(int v)
{
int x,y;
Split(root,v-1,x,y);
int tmp=val[fid_kth(x,siz[x])];
root=Merge(x,y);
return tmp;
}
int fid_nxt(int v)
{
int x,y;
Split(root,v,x,y);
int tmp=val[fid_kth(y,1)];
root=Merge(x,y);
return tmp;
}
- \(kth\) 中这么做就是对的,我不会证,哎sbmqwm你别搞我
又是一段美好的时光
总代码
点击查看代码
int n,root;
namespace fhq
{
mt19937 rd(time(0));
int val[maxn],siz[maxn],rnd[maxn],tot,ls[maxn],rs[maxn];
void Pushup(int x) {siz[x]=siz[ls[x]]+siz[rs[x]]+1;}
void Split(int now,int v,int &x,int &y)
{
if (!now) return x=y=0,void();
if (val[now]<=v) x=now,Split(rs[now],v,rs[now],y);
else y=now,Split(ls[now],v,x,ls[now]);
Pushup(now);
}
int Merge(int x,int y)
{
if (!x || !y) return x|y;
if (rnd[x]<rnd[y])
{
rs[x]=Merge(rs[x],y);
Pushup(x);
return x;
}
else
{
ls[y]=Merge(x,ls[y]);
Pushup(y);
return y;
}
}
int New(int v)
{
tot++,val[tot]=v,rnd[tot]=rd(),siz[tot]=1;
return tot;
}
void Insert(int a)
{
int x,y;
Split(root,a,x,y);
root=Merge(Merge(x,New(a)),y);
}
int fid_kth(int x,int k)
{
while (1)
{
if (k<=siz[ls[x]]) x=ls[x];
else if (k == siz[ls[x]]+1) return x;
else k-=(siz[ls[x]]+1),x=rs[x];
}
}
int fid_siz(int v)
{
int x,y;
Split(root,v-1,x,y);
int k=siz[x]+1;
root=Merge(x,y);
return k;
}
int fid_prev(int v)
{
int x,y;
Split(root,v-1,x,y);
int tmp=val[fid_kth(x,siz[x])];
root=Merge(x,y);
return tmp;
}
int fid_nxt(int v)
{
int x,y;
Split(root,v,x,y);
int tmp=val[fid_kth(y,1)];
root=Merge(x,y);
return tmp;
}
void Delete(int v)
{
int x,y,z;
Split(root,v,x,z);
Split(x,v-1,x,y);
y=Merge(ls[y],rs[y]);
root=Merge(Merge(x,y),z);
}
};
后记
- msjing还有 \(Splay\) 要学,\(Splay\) 的学习笔记等学会了再写
- 以下是捏造在说话