平衡树
前言
Updating...(咕咕咕)
两年后……博主实在懒得更了 qwq
这篇博客不会再更了,但是 TOC 还留下,读者可以查找相关资料 .
因为两年前写的东西,这里也是比较入门的平衡树,更深入的理解还是多靠积累吧 .
UOJ 群里大佬多哦,甚至还有人说任何平衡树都可以持久化???
好吧,前言没了,看看文章吧 .
前置芝士
- 二叉搜索树(Binary Search Tree,BST)
- 堆(Heap)(Treap 需要)
BST 是最好的平衡树!只要数据随机吊打一切其他平衡树!
下面大部分平衡树都是不把相同元素合在一起的,为了防止毒瘤出题人出毒瘤数据卡可以自行随机合并(特别麻烦)或者合点(容易)
普通平衡树
模板:
普通平衡树的基本操作可以参考上方模板
1. 替罪羊树
众所周知 每种平衡树都有一种操作来维护平衡。
替罪羊树是暴力维护平衡的(
替罪羊树要存 左右儿子,节点值,子树实际大小(前面的一般平衡树都要用),子树不实际大小(?)还有删除标记(后面解释)。
struct Node
{
int l,r,val;
int size,fact; // 实际大小:fact
bool exist; // exist : 是否存在
}tzy[N];
int cnt,root; // cnt : 节点数量 root : 根
// 创建新节点
inline void newnode(int &now,int val){now=++cnt; tzy[now].val=val; tzy[now].size=tzy[now].fact=1; tzy[now].exist=true;}
1.1. 插入
只要按 BST 插入方法插入即可,但是为了平衡还得在后面加一句判平衡。
void ins(int &now,int val)
{
if (!now){newnode(now,val); check(root,now); return ;} // check : 判平衡
++tzy[now].size; ++tzy[now].fact;
if (val<tzy[now].val) ins(tzy[now].l,val);
else ins(tzy[now].r,val);
}
1.2. 删除
还记得前面节点里有维护「删除标记」吗?
对,删除就是把节点打上标记。
而「子树不实际大小」就是树中子树的大小,
「子树实际大小」就是树中子树去掉打了标记的大小。
同样,为了平衡还得在后面加一句判平衡。
void del(int now,int val)
{
if (tzy[now].exist&&tzy[now].val==val){tzy[now].exist=false; tzy[now].fact--; check(root,now); return ;}
tzy[now].fact--;
if (val<tzy[now].val) del(tzy[now].l,val);
else del(tzy[now].r,val);
}
!.1 判断树是否平衡并调整树
插入和删除都提到了「判平衡」,但是「判平衡」怎么写呢?
我们从根向要操作的节点找,如果找到了需要重构的节点,就暴力重构它的子树。
判断是否平衡的条件是:
当前节点的左子树或右子树的大小大于当前节点的大小 ,其中 是平衡因子,或
当前节点为根的子树内被删除的节点数量 树大小(非实际)的 了 .
必须取 ,一般取 ,最平常的是取 .
inline bool imbalence(int now){return (max(tzy[tzy[now].l].size,tzy[tzy[now].r].size)>tzy[now].size*alpha)||
(tzy[now].size-tzy[now].fact>tzy[now].size*0.3); }
所以总体的 check
大概是这样的:
void check(int &now,int end)
{
if (now==end) return;
if (imbalence(now)){rebuild(now); update(root,now); return ;}
if (tzy[end].val<tzy[now].val) check(tzy[now].l,end);
else check(tzy[now].r,end);
}
这里 rebuild
是重构,update
是更新数据(size
)。
update
非常的好写:
void update(int now,int end)
{
if (!now) return;
if (tzy[end].val<tzy[now].val) update(tzy[now].l,end);
else update(tzy[now].r,end);
tzy[now].size=tzy[tzy[now].l].size+tzy[tzy[now].r].size+1;
}
前面说过了,替罪羊树的维护平衡(重构)是暴力,替罪羊树的重构就是拉开(中序遍历)再拎起来。
vector<int> v;
void ldr(int now) // 拉开
{
if (!now) return;
ldr(tzy[now].l);
if (tzy[now].exist) v.push_back(now);
ldr(tzy[now].r);
}
void lift(int l,int r,int &now) // 拎起来
{
if (l==r){now=v[l]; tzy[now].l=tzy[now].r=0; tzy[now].size=tzy[now].fact=1; return ;}
int m=(l+r)>>1;
while ((l<m)&&(tzy[v[m]].val==tzy[v[m-1]].val)) --m;
now=v[m];
if (l<m) lift(l,m-1,tzy[now].l);
else tzy[now].l=0;
lift(m+1,r,tzy[now].r);
tzy[now].size=tzy[tzy[now].l].size+tzy[tzy[now].r].size+1; // (
tzy[now].fact=tzy[tzy[now].l].fact+tzy[tzy[now].r].fact+1;
}
void rebuild(int &now)
{
v.clear(); ldr(now);
if (v.empty()){now=0; return ;}
lift(0,v.size()-1,now);
}
1.3. & 1.4. 查询值的排名 / 查询排名的值
这些就按照普通 BST 写就行啦
int getrank(int val)
{
int now=root,rank=1;
while (now)
{
if (val<=tzy[now].val) now=tzy[now].l;
else {rank+=tzy[now].exist+tzy[tzy[now].l].fact; now=tzy[now].r;}
} return rank;
}
int getval(int rank)
{
int now=root;
while (now)
{
if (tzy[now].exist&&tzy[tzy[now].l].fact+tzy[now].exist==rank) break;
else if (tzy[tzy[now].l].fact>=rank) now=tzy[now].l;
else {rank-=tzy[tzy[now].l].fact+tzy[now].exist; now=tzy[now].r;} // 这个 rank-=... 有主席树内味了
} return tzy[now].val;
}
1.5. & 1.6. 求前趋 / 求后继
前趋和后继有个小技巧:
前趋:getval(getrank(x)-1);
后继:getval(getrank(x+1));
inline int pre(int x){return getval(getrank(x)-1);}
inline int nxt(int x){return getval(getrank(x+1));}
完整代码
struct tzy
{
private:
static constexpr double alpha=0.75;
struct Node
{
int l,r,val;
int size,fact;
bool exist;
}tr[N];
int cnt,root;
inline void newnode(int &now,int val){now=++cnt; tr[now].val=val; tr[now].size=tr[now].fact=1; tr[now].exist=true;}
inline bool imbalence(int now){return (max(tr[tr[now].l].size,tr[tr[now].r].size)>tr[now].size*alpha)||
(tr[now].size-tr[now].fact>tr[now].size*0.3); }
vector<int> v;
void ldr(int now)
{
if (!now) return;
ldr(tr[now].l);
if (tr[now].exist) v.push_back(now);
ldr(tr[now].r);
}
void lift(int l,int r,int &now)
{
if (l==r){now=v[l]; tr[now].l=tr[now].r=0; tr[now].size=tr[now].fact=1; return ;}
int m=(l+r)>>1;
while ((l<m)&&(tr[v[m]].val==tr[v[m-1]].val)) --m;
now=v[m];
if (l<m) lift(l,m-1,tr[now].l);
else tr[now].l=0;
lift(m+1,r,tr[now].r);
tr[now].size=tr[tr[now].l].size+tr[tr[now].r].size+1;
tr[now].fact=tr[tr[now].l].fact+tr[tr[now].r].fact+1;
}
void rebuild(int &now)
{
v.clear(); ldr(now);
if (v.empty()){now=0; return ;}
lift(0,v.size()-1,now);
}
void update(int now,int end)
{
if (!now) return;
if (tr[end].val<tr[now].val) update(tr[now].l,end);
else update(tr[now].r,end);
tr[now].size=tr[tr[now].l].size+tr[tr[now].r].size+1;
}
void check(int &now,int end)
{
if (now==end) return;
if (imbalence(now)){rebuild(now); update(root,now); return ;}
if (tr[end].val<tr[now].val) check(tr[now].l,end);
else check(tr[now].r,end);
}
void ins(int &now,int val)
{
if (!now){newnode(now,val); check(root,now); return ;}
++tr[now].size; ++tr[now].fact;
if (val<tr[now].val) ins(tr[now].l,val);
else ins(tr[now].r,val);
}
void del(int now,int val)
{
if (tr[now].exist&&tr[now].val==val){tr[now].exist=false; tr[now].fact--; check(root,now); return ;}
tr[now].fact--;
if (val<tr[now].val) del(tr[now].l,val);
else del(tr[now].r,val);
}
public:
inline void ins(int val){ins(root,val);}
inline void del(int val){del(root,val);}
int getrank(int val)
{
int now=root,rank=1;
while (now)
{
if (val<=tr[now].val) now=tr[now].l;
else {rank+=tr[now].exist+tr[tr[now].l].fact; now=tr[now].r;}
} return rank;
}
int getval(int rank)
{
int now=root;
while (now)
{
if (tr[now].exist&&tr[tr[now].l].fact+tr[now].exist==rank) break;
else if (tr[tr[now].l].fact>=rank) now=tr[now].l;
else {rank-=tr[tr[now].l].fact+tr[now].exist; now=tr[now].r;}
} return tr[now].val;
}
inline int pre(int x){return getval(getrank(x)-1);}
inline int nxt(int x){return getval(getrank(x+1));}
}BST;
2(1). fhq_Treap
发明人:范浩强神犇(%%%%%%%%%%%%%%%%%%%%%%)
要学习 fhq Treap,你得先简单了解一下普通 Treap 是咋个回事。
普通 Treap 其实是一棵特殊的笛卡尔树。
Treap=Tree+Heap,这是一棵拥有双重性质的树形数据结构,既满足 BST(二叉搜索树)的性质也满足 Heap(堆)的性质。
它让平衡树上的每一个结点存放两个信息:值和一个随机的索引 。其中值满足二叉搜索树的性质,索引满足堆的性质,结合二叉搜索树和二叉堆的性质来使树平衡。这也是 Treap 的性质。
Treap 为什么可以平衡?我们知道如果对一颗二叉搜索树进行插入的值按次序是有序的,那么二叉搜索树就会退化成一个链表。那么我们可以别让数值按次序插入,一个很好的方法就是把插入次序随机化。比如本来的插入次序是 ,结果随机之后变成了 ,是不是就好多了!
Treap 用二叉堆来维护随机索引 ,其实就是相当于把插入次序随机化。插入一值数后你必然要让 满足二叉堆的特性,但又因为索引是随机的,那就会导致插入的数不知道搞到了哪里去,相当于插入次序随机了。
当然你也可以选择不理解那么多,你其实只需要知道:
普通 Treap 维护平衡的操作是旋转(rorate),而 fhq_Treap 用来维护平衡的操作只有分裂(split)和合并(merge)。
fhq Treap 要存 左右子树编号,值,索引(),子树大小
!.1 分裂
分裂有两种:按值分裂和按大小分裂
写普通平衡树一般用按值分裂,而在写文艺平衡树的时候一般用按大小分裂(后面有文艺平衡树)
按值分裂:把树拆成两棵树,拆出来的一棵树的值全部小于等于给定的值,另外一部分的值全部大于给定的值。
那么如何拆呢?这就需要利用 fhq_Treap 的性质及本质了。
从一个根节点开始遍历就可以推出整棵树,所以一个 fhq_Treap 的根节点代表的是这棵 fhq_Treap 所有的点,你可以将其理解为这个 fhq_Treap 所代表的是一个 的区间,中序遍历后的 序列是单调不减的。
那么比如说一个 序列为 ,你想要将 小于等于 的所有节点和大于 的所有节点分开,就相当于将 的区间 $(l,r) 拆成两个区间 和 ,就等同于将一棵大 fhq_Treap 按照 分成两棵小 fhq_Treap .
我们将 称为「左区间树」,将 称为「右区间树」。
当 时,分裂的过程如图,图中的蓝色是已经归入左区间树的点,橙色是已经归入右区间树的点,左右区间树中的实圆是已经确定的节点,虚圆是尚未确定的节点。
我们首先建立两个虚拟节点,就是图中的虚圆,如果当前访问的节点 的 ,那么 及 的左子树,都应归在左区间树里,就是将左区间树上一个虚拟节点赋为 ,此节点便由虚变实,然后对 建立虚拟的右儿子。否则, 及 的右子树应该归在右区间树里,就是将上一个右区间树的虚拟节点赋为 ,此节点便由虚变实,然后对 建立虚拟的左儿子。如果 是空节点,则将左右区间树的虚拟节点赋为 .
虚拟节点可以用引用存。
因为每访问到一个非空的 节点时,递归分裂子树进行的操作会改变左右儿子,所以在维护一些值(如子树大小)的时候需要用同线段树类似的方法进行上传操作。
Code:
inline void update(int now){fhq[now].size=fhq[fhq[now].l].size+fhq[fhq[now].r].size+1;}
void split(int now,int val,int& x,int& y)
{
if (!now) x=y=0;
else
{
if (fhq[now].val<=val){x=now; split(fhq[now].r,val,fhq[now].r,y);}
else{y=now; split(fhq[now].l,val,x,fhq[now].l);}
update(now);
}
}
!.2 合并
合并就是反向分裂。
Code:
int merge(int x,int y)
{
if (!x||!y) return x+y;
if (fhq[x].key>fhq[y].key){fhq[x].r=merge(fhq[x].r,y); update(x); return x;}
else{fhq[y].l=merge(x,fhq[y].l); update(y); return y;}
return 0; // 为了不报 Warning 写的
}
2(1).1 插入
设插入的值为 ,那么我们只需要按 分裂再把节点插在中间(合并)即可。
#define def1 static int x,y;
#define def2 static int x,y,z;
inline void ins(int val){def1; split(root,val,x,y); root=merge(merge(x,newnode(val)),y);}
2(1).2 删除
设删除的值是 ,那么先按 分裂,再按 分裂,此时就分出了一棵全是 元素的子树,然后把那个子树的根删掉(合并左右子树),最后把分裂开的合并回去即可
inline void del(int val)
{
def2;
split(root,val,x,z); split(x,val-1,x,y);
y=merge(fhq[y].l,fhq[y].r); root=merge(merge(x,y),z);
}
2(1).3 & 2(1).4 查询值的排名 / 查询排名的值
查询值的排名就先按 分裂,然后看一下子树大小即可;查询排名的值按替罪羊树的写即可。
inline int getrank(int val){def1; split(root,val-1,x,y); int ans=fhq[x].size+1; root=merge(x,y); return ans;}
inline int getval(int rank)
{
int now=root;
while (now)
{
if (fhq[fhq[now].l].size+1==rank) break;
else if (fhq[fhq[now].l].size>=rank) now=fhq[now].l;
else{rank-=fhq[fhq[now].l].size+1; now=fhq[now].r;}
} return fhq[now].val;
}
2(1).5 & 2(1).6 求前趋 / 求后继
前驱:按值 分裂,则左子树里面最右边的数的前驱;
后继:按值 分裂,则右子树里面最左的数就是后继。
inline int pre(int val)
{
def1;
split(root,val-1,x,y); int now=x;
while (fhq[now].r) now=fhq[now].r;
int ans=fhq[now].val; root=merge(x,y); return ans;
}
inline int nxt(int val)
{
def1;
split(root,val,x,y); int now=y;
while (fhq[now].l) now=fhq[now].l;
int ans=fhq[now].val; root=merge(x,y); return ans;
}
完整代码
#include<random>
#include<iostream>
#include<ctime>
#include<queue>
#include<stack>
#include<cmath>
#include<iterator>
#include<cctype>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<bitset>
using namespace std;
const int N=1e5+15;
static mt19937 rnd(19260817); // C++11
struct fhq_treap
{
private:
struct Node{int l,r,val,size,key;}fhq[N];
int cnt,root;
inline int newnode(int val){fhq[++cnt].val=val; fhq[cnt].key=rnd(); fhq[cnt].size=1; return cnt;}
inline void update(int now){fhq[now].size=fhq[fhq[now].l].size+fhq[fhq[now].r].size+1;}
void split(int now,int val,int& x,int& y)
{
if (!now) x=y=0;
else
{
if (fhq[now].val<=val){x=now; split(fhq[now].r,val,fhq[now].r,y);}
else{y=now; split(fhq[now].l,val,x,fhq[now].l);}
update(now);
}
}
int merge(int x,int y)
{
if (!x||!y) return x+y;
if (fhq[x].key>fhq[y].key){fhq[x].r=merge(fhq[x].r,y); update(x); return x;}
else{fhq[y].l=merge(x,fhq[y].l); update(y); return y;}
return 0;
}
#define def1 static int x,y;
#define def2 static int x,y,z;
public:
inline void ins(int val){def1; split(root,val,x,y); root=merge(merge(x,newnode(val)),y);}
inline void del(int val)
{
def2;
split(root,val,x,z); split(x,val-1,x,y);
y=merge(fhq[y].l,fhq[y].r); root=merge(merge(x,y),z);
}
inline int getrank(int val){def1; split(root,val-1,x,y); int ans=fhq[x].size+1; root=merge(x,y); return ans;}
inline int getval(int rank)
{
int now=root;
while (now)
{
if (fhq[fhq[now].l].size+1==rank) break;
else if (fhq[fhq[now].l].size>=rank) now=fhq[now].l;
else{rank-=fhq[fhq[now].l].size+1; now=fhq[now].r;}
} return fhq[now].val;
}
inline int pre(int val)
{
def1;
split(root,val-1,x,y); int now=x;
while (fhq[now].r) now=fhq[now].r;
int ans=fhq[now].val; root=merge(x,y); return ans;
}
inline int nxt(int val)
{
def1;
split(root,val,x,y); int now=y;
while (fhq[now].l) now=fhq[now].l;
int ans=fhq[now].val; root=merge(x,y); return ans;
}
#undef def1
#undef def2
}BST;
int main()
{
int T; scanf("%d",&T); int opt,val;
while (T--)
{
scanf("%d%d",&opt,&val);
if (opt==1) BST.ins(val);
else if (opt==2) BST.del(val);
else if (opt==3) printf("%d\n",BST.getrank(val));
else if (opt==4) printf("%d\n",BST.getval(val));
else if (opt==5) printf("%d\n",BST.pre(val));
else if (opt==6) printf("%d\n",BST.nxt(val));
// else puts("ERROR");
} return 0;
}
2(2). Treap
Treap 维护平衡的操作是旋转,在插入的时候维护即可。
因为这是 Treap,所以别的操作按 BST 写即可。
!.1 旋转
注意删除的时候要旋到下面再删,一定要上传。
旋转的具体写法可以参考后面 AVL 树部分。
fhq_Treap 的一些 Trick
1. 随机合并
在普通 Treap 中只有插入的时候用到了索引 的比较,在 fhq_Treap 中只有合并时用力索引比较。
既然索引只是用于随机合并一下,不如在 if
语句里直接写玄学随机罢了。
写个 rand()&1
可能会被卡,具体可以看 洛谷讨论 123169
2. 垃圾回收
将删除掉的无用的节点在下次新建节点的时候优先使用。具体实现方法为在删除节点的时候将删除的节点一个个放入队列里,在新建节点的时候判断队列是否为空,如果不为空就从队头取一个节点清空原有的所有信息并新建,否则就按正常情况新建节点。此种操作可以节省空间。
3. 建树
具体可以参考 笛卡尔树 - OI Wiki
4. 定期重构
当有些题目中对空间有一定限制,且不要求查询历史版本,我们可以与定期重构,当所用空间超过一个给定值的时候,我们就中序遍历整棵树,存入一个数组里,再线性建树。
5. 启发式合并
对于两个有交集的 fhq_Treap,很暴力的方式就是选择其中的一棵 Treap,后序遍历整棵树,将每个点左右儿子清空,拆完后作为单点与另一棵树暴力插入。然而这样的时间复杂度是不优秀的,我们考虑每次将较小的树合并到较大的树上,这样每个点最多只会合并 次。
3. AVL 树(平衡树始祖)
AVL 和 Treap 都是依赖旋转来维护平衡的,我们称这种平衡树为有旋平衡树,反之则成为无旋平衡树。
有旋平衡树要比无旋平衡树相对快一些(最快的平衡树好像是 RBT(Red and Blue Red Black Tree,红黑树)?)
有旋平衡树当然要先说旋转了 qwq
!.1 旋转
先放一张维基上的动图(可能动的有亿点点慢,请耐心等待)
可以发现旋转过后中序遍历不变,所以二叉搜索树不会被破坏。
Code:
inline void lrotate(int& now)
{
int r=avl[now].r; avl[now].r=avl[r].l; avl[r].l=now; now=r;
update(avl[now].l); update(now); // 更新信息,可以不管
}
inline void rrotate(int& now)
{
int l=avl[now].l; avl[now].l=avl[l].r; avl[l].r=now; now=l;
update(avl[now].r); update(now); // 更新信息,可以不管
}
?. 正题
好,有了旋转就可以开始正题了。
AVL 树是由 G.M. Adelson-Velsky 和 Evgenii Landis 于 年发明的,也因此得名。而且,它是最早被发明的平衡树!
然而随着时代的进步,AVL 树已经成了时代的眼泪。
影响二叉搜索树操作最坏时间复杂度的因素是什么?
是树高,所以 AVL 保证任一结点的左右子树的最大高度差为 ,所以也叫 高度平衡树。
于此同时也保证了具有 个结点的 AVL 树高度一定是 ,也就是最坏时间复杂度为
AVL 树借 平衡因子 来维持树的平衡
一个结点的平衡因子 = 左子树高度 - 右子树高度
根据「任一结点的左右子树的最大高度差为 」可知:弟弟树所有结点的 都只能等于 ,如果 不等于这三个数,那证明不平衡,需要调整。
调整当然是按替罪羊树写啦
AVL 树用 旋转 来调整不平衡的结点,不平衡分为四种情况,每种情况都有不同的旋转方法
这四种情况用 和 字母的组合来表示:
- 的意思是:当前结点的左子树太高了,且左子树的左子树比较高;
- 的意思是:当前结点的左子树太高了,且左子树的右子树比较高;
- 的意思是:当前结点的右子树太高了,且右子树的左子树比较高;
- 的意思是:当前结点的右子树太高了,且右子树的右子树比较高。
左子树太高了指 ,比较高指 ;右子树太高了指 ,比较高指 .
处理方法:
- :右旋
- :左旋
- :左旋左子树再右旋
- :右旋右子树再左旋
Code:
inline void check(int& now)
{
int nf=BF(now);
if (nf>1)
{
int lf=BF(avl[now].l);
if (lf>0) rrotate(now);
else lrotate(avl[now].l),rrotate(now);
} else if (nf<-1)
{
int rf=BF(avl[now].r);
if(rf<0) lrotate(now);
else rrotate(avl[now].r),lrotate(now);
} else if (now) update(now);
}
3.1 & 3.2 & 3.3 & 3.4 & 3.5 & 3.6 插入 / 删除 / 查询值的排名 / 查询排名的值 / 求前驱 / 求后继
按普通 BST 写即可。
完整代码
#include<iostream>
#include<ctime>
#include<queue>
#include<stack>
#include<cmath>
#include<iterator>
#include<cctype>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<bitset>
using namespace std;
const int N=1e5+15;
struct AVLTree
{
private:
struct Node{int l,r,val,height,size;}avl[N];
int cnt,root;
inline void newnode(int &now,int val){avl[now=++cnt].val=val; avl[cnt].size=1;}
inline void update(int now)
{
avl[now].size=avl[avl[now].l].size+avl[avl[now].r].size+1;
avl[now].height=std::max(avl[avl[now].l].height,avl[avl[now].r].height)+1;
}
inline int BF(int now){return avl[avl[now].l].height-avl[avl[now].r].height;}
inline void lrotate(int& now)
{
int r=avl[now].r; avl[now].r=avl[r].l; avl[r].l=now; now=r;
update(avl[now].l); update(now);
}
inline void rrotate(int& now)
{
int l=avl[now].l; avl[now].l=avl[l].r; avl[l].r=now; now=l;
update(avl[now].r); update(now);
}
inline void check(int& now)
{
int nf=BF(now);
if (nf>1)
{
int lf=BF(avl[now].l);
if (lf>0) rrotate(now);
else lrotate(avl[now].l),rrotate(now);
} else if (nf<-1)
{
int rf=BF(avl[now].r);
if(rf<0) lrotate(now);
else rrotate(avl[now].r),lrotate(now);
} else if (now) update(now);
}
void ins(int& now,int val)
{
if (!now) newnode(now,val);
else if(val<avl[now].val) ins(avl[now].l,val);
else ins(avl[now].r,val);
check(now);
}
int find(int& now,int fa)
{
int ret;
if (!avl[now].l){ret=now; avl[fa].l=avl[now].r;}
else{ret=find(avl[now].l,now); check(now);}
return ret;
}
void del(int& now,int val)
{
if (val==avl[now].val)
{
int l=avl[now].l,r=avl[now].r;
if (!l||!r) now=l+r;
else
{
now=find(r,r);
if (now!=r) avl[now].r=r;
avl[now].l=l;
}
}
else if (val<avl[now].val) del(avl[now].l,val);
else del(avl[now].r,val);
check(now);
}
public:
inline void ins(int val){ins(root,val);}
inline void del(int val){del(root,val);}
int getrank(int val)
{
int now=root,rank=1;
while (now)
{
if (val<=avl[now].val) now=avl[now].l;
else{rank+=avl[avl[now].l].size+1; now=avl[now].r;}
} return rank;
}
int getval(int rank)
{
int now=root;
while (now)
{
if (avl[avl[now].l].size+1==rank) break;
else if (avl[avl[now].l].size>=rank) now=avl[now].l;
else{rank-=avl[avl[now].l].size+1; now=avl[now].r;}
} return avl[now].val;
}
inline int pre(int x){return getval(getrank(x)-1);}
inline int nxt(int x){return getval(getrank(x+1));}
}BST;
int main()
{
int T; scanf("%d",&T); int opt,val;
while (T--)
{
scanf("%d%d",&opt,&val);
if (opt==1) BST.ins(val);
else if (opt==2) BST.del(val);
else if (opt==3) printf("%d\n",BST.getrank(val));
else if (opt==4) printf("%d\n",BST.getval(val));
else if (opt==5) printf("%d\n",BST.pre(val));
else if (opt==6) printf("%d\n",BST.nxt(val));
// else puts("ERROR");
} return 0;
}
4. Zig-Zag Splay(Splay 的复杂代码版)
Splay 是一种自组织的数据结构,也就是说我们在使用这个数据结构的时候,我们可以通过一些情况来调整这个数据结构。例如使用频率。
做法就是对于查找频率较高的节点,使其处于离根节点相对较近的节点,但这个玩意儿确实不好统计,但是你可以认为每次被查找的点查找频率相对较高,所以每次只需要把查询到的点搬到根上就行了。
像 Cache 一样,对于经常用的数据,越用,访问得越快。
顺带一提,发明者里有 tarjan 神仙(tarjan 太强了)
!.1 伸展(Splay)
Splay 翻译过来叫伸展树,所以核心操作肯定是伸展(Splay)操作啦。
伸展即把一个结点通过旋转调整到某个结点处(一般都是伸展到根结点)
旋转就是 AVL 树那里说的左旋和右旋,不过貌似有了更高级的名字:左旋(Zag)和右旋(Zig)
Spaly 的单旋
Spaly 对于 Splay 操作的写法是:
一步一步旋转上去。
这种旋法叫做 单旋。看起来很对,是吗?
请看一个例子:
Splay 的双旋
一图胜千言,Splay 有四种双旋,分别是 Zig-Zig , Zig-Zag , Zag-Zig , Zag-Zag:
伸展!
采用递归 Splay 写法,可以不用维护 father,但常数会大点 .
void Splay(int x,int& y)
{
if (x==y) return ;
int& l=spl[y].l,&r=spl[y].r;
if (x==l) zig(y);
else if(x==r) zag(y);
else
{
if (spl[x].val<spl[y].val)
{
if (spl[x].val<spl[l].val) Splay(x,spl[l].l),zig(y),zig(y);
else Splay(x,spl[l].r),zag(l),zig(y);
}
else
{
if (spl[x].val>spl[r].val) Splay(x,spl[r].r),zag(y),zag(y);
else Splay(x,spl[r].l),zig(r),zag(y);
}
}
}
就是按双旋写的 .
3.1 & 3.2 & 3.3 & 3.4 & 3.5 & 3.6 插入 / 删除 / 查询值的排名 / 查询排名的值 / 求前驱 / 求后继
按普通 BST 写即可,注意要随时 Splay。
完整代码
struct ZigZag_SplayTree
{
private:
struct Node{int l,r,val,size,cnt;}spl[N];
int cnt,root;
inline void newnode(int& now,int& val){spl[now=++cnt].val=val; ++spl[cnt].size; ++spl[cnt].cnt;}
inline void update(int now){spl[now].size=spl[spl[now].l].size+spl[spl[now].r].size+spl[now].cnt;}
inline void zig(int& now)
{
int l=spl[now].l; spl[now].l=spl[l].r; spl[l].r=now; now=l;
update(spl[now].r); update(now);
}
inline void zag(int& now)
{
int r=spl[now].r; spl[now].r=spl[r].l; spl[r].l=now; now=r;
update(spl[now].l); update(now);
}
void Splay(int x,int& y)
{
if (x==y) return ;
int& l=spl[y].l,&r=spl[y].r;
if (x==l) zig(y);
else if(x==r) zag(y);
else
{
if (spl[x].val<spl[y].val)
{
if (spl[x].val<spl[l].val) Splay(x,spl[l].l),zig(y),zig(y);
else Splay(x,spl[l].r),zag(l),zig(y);
}
else
{
if (spl[x].val>spl[r].val) Splay(x,spl[r].r),zag(y),zag(y);
else Splay(x,spl[r].l),zig(r),zag(y);
}
}
}
inline void delnode(int now)
{
Splay(now,root);
if (spl[now].cnt>1) spl[now].size--,spl[now].cnt--;
else if (spl[root].r)
{
int p=spl[root].r;
while (spl[p].l) p=spl[p].l;
Splay(p,spl[root].r); spl[spl[root].r].l=spl[root].l; root=spl[root].r; update(root);
}
else root=spl[root].l;
}
void ins(int& now,int& val)
{
if (!now) newnode(now,val),Splay(now,root);
else if (val<spl[now].val) ins(spl[now].l,val);
else if (val>spl[now].val) ins(spl[now].r,val);
else{++spl[now].size; ++spl[now].cnt; Splay(now,root);}
}
void del(int now,int& val)
{
if (spl[now].val==val) delnode(now);
else if (val<spl[now].val) del(spl[now].l,val);
else del(spl[now].r,val);
}
public:
inline void ins(int val){ins(root,val);}
inline void del(int val){del(root,val);}
int getrank(int val)
{
int now=root,rank=1;
while (now)
{
if (spl[now].val==val){rank+=spl[spl[now].l].size; Splay(now,root); break;}
if (val<=spl[now].val) now=spl[now].l;
else{rank+=spl[spl[now].l].size+spl[now].cnt; now=spl[now].r;}
} return rank;
}
int getval(int rank)
{
int now=root;
while (now)
{
int lsize=spl[spl[now].l].size;
if (lsize+1<=rank&&rank<=lsize+spl[now].cnt){Splay(now,root); break ;}
else if (lsize>=rank) now=spl[now].l;
else{rank-=lsize+spl[now].cnt; now=spl[now].r;}
} return spl[now].val;
}
inline int pre(int x){return getval(getrank(x)-1);}
inline int nxt(int x){return getval(getrank(x+1));}
}BST;
半伸展
最后简单说一下半伸展。
我们在使用输入法的时候,大多数情况并不是用了一下这个词条后这个词条瞬间就到了首位了,而是随着我们对它的使用,慢慢地往首位移动,这是怎么实现的呢?
其实是对一字形旋转进行了一些改变:
5. Splay(Splay 的精简代码版)
6. SBT(节点大小平衡树)
7. WBLT
8. RBT & LLRBT
普通平衡树其他做法
1. vector
2. 权值线段树
3. 树状数组
4. 01Trie
5. pb_ds
6. multiset
文艺平衡树
1. fhq_Treap
2. Splay
实际时空比较
普通平衡树(用的是咕咕里的普通平衡树普通版):
平衡树 | 代码长度 | 时间 | 空间 | 来源 |
---|---|---|---|---|
替罪羊树 | 3.15KB | 352ms | 1.89MB | 文中 |
替罪羊树 | 3.81KB | 528ms | 18.07MB | Misaka_Azusa 的题解 |
fhq Treap | 2.47KB | 348ms | 1.50MB | 文中 |
fhq Treap | 2.49KB | 390ms | 1.64MB | 一个小屁孩 的题解 |
Treap | 4.65KB | 250ms | 10.98MB | 天上一颗蛋 的题解 |
Treap | 3.15KB | 293ms | 1.62MB | wasa855 的题解 |
AVL | 3.08KB | 278ms | 1.50MB | 文中 |
vector | 758B | 467ms | 1.11MB | 一个小屁孩 的题解 |
Reference
以下是博客签名,正文无关
本文来自博客园,作者:yspm,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/14110905.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】