首先,我们学习一个比较简单的东西: BST(Binary Search Tree),即二叉排序树。
BST
Description
Solution
二叉排序树是满足下面条件的一棵二叉树: 对于任何一个节点 r t rt rt,它的关键码大于所有在其左子树内的关键码,但是小于所有在其右子树内的关键码。这也被称为BST性质。
对于某些题目,我们需要在修改的同时维护这个数据结构,在查询的时候快速得到答案。我们先假设任何时候二叉排序树内不存在任何两个相同的节点使得它们的关键码相同。
下面为了方便叙述,假设第 i i i个节点的关键码为 v a l i val_i vali,左孩子为 l s o n i lson_i lsoni,右孩子为 r s o n i rson_i rsoni。
① B S T BST BST的建立。
初始时, B S T BST BST长这样:
即,根节点为负无穷大,其唯一的右孩子为正无穷大。
②搜索一个数
x
x
x。
从根节点向下递归,假设目前递归到了节点
r
t
rt
rt。
(1)如果
v
a
l
r
t
=
x
val_{rt}=x
valrt=x,那么搜索成功;
(2)如果
x
<
v
a
l
r
t
x<val_{rt}
x<valrt,向左递归;
(3)否则向右递归。
③插入一个数
x
x
x。
我们向下递归。假设目前递归到了节点
r
t
rt
rt:
(1)如果
r
t
rt
rt为
0
0
0,说明目前走到了一个空节点上。此时我们就在这个位置新建一个节点即可。
(2)如果
x
<
v
a
l
r
t
x<val_{rt}
x<valrt,就向左子树递归;
(3)如果
x
>
v
a
l
r
t
x>val_{rt}
x>valrt,就向右子树递归。
④查询
x
x
x树的排名。
我们对于每一个BST上的节点多维护一个量
s
i
z
e
size
size,表示以其为根的子树大小。这可以通过递归来算出,即这个节点的
s
i
z
e
size
size等于其左孩子(如果有的话)的
s
i
z
e
size
size加上右孩子(如果有的话)的
s
i
z
e
size
size再加上
1
1
1。特别地,对于一个新建的节点,其
s
i
z
e
size
size为
1
1
1。
我们向下递归。定义一个变量
p
p
p,表示目前比
x
x
x小的数有多少个。假设目前递归到了
r
t
rt
rt:
(1)如果
r
t
=
0
rt=0
rt=0,那么走到了一个空节点上;结束此轮计算;
(2)如果
x
<
v
a
l
r
t
x<val_{rt}
x<valrt,不改变
p
p
p,直接向左孩子递归;
(3)如果
x
>
v
a
l
r
t
x>val_{rt}
x>valrt,
p
p
p加上
s
i
z
e
l
s
o
n
(
x
)
+
1
size_{lson(x)}+1
sizelson(x)+1,然后向右子树递归。
这里解释一下 ( 3 ) (3) (3)的做法。由于 x > v a l r t x>val_{rt} x>valrt,那么rt的左子树与 r t rt rt本身的关键码肯定都是比 x x x小的。所以要加上 s i z e l s o n ( x ) + 1 size_{lson(x)}+1 sizelson(x)+1。
⑤查询排名为 x x x的树。
类似④,我们可以得到⑤的做法。
我们向下递归。在传参的同时多传一个变量
r
a
n
k
rank
rank,表示想要找到这个子树中排名为
r
a
n
k
rank
rank的关键码。假设目前搜到了
r
t
rt
rt这个节点。
(1)如果
r
t
=
0
rt=0
rt=0,直接
r
e
t
u
r
n
return
return;
(2)如果左孩子的大小不大于
r
a
n
k
−
2
rank-2
rank−2,那么向左递归;
(3)如果左孩子的大小恰为
r
a
n
k
−
1
rank-1
rank−1,返回
r
t
rt
rt的关键码;
(4)否则向右递归。
⑥求
x
x
x的前驱。
首先执行在二叉排序树中查找
x
x
x的操作。如果没有找到,那么答案就在经过的节点的关键码中,可以采用“实时更新”的方式来快速求出答案。如果找到了呢?
假设在节点 r t rt rt这里找到了 x x x。我们先将 r t rt rt变成它的左孩子 l s o n r t lson_{rt} lsonrt,然后不停地将当前的 r t rt rt变成它的右孩子即可。最终答案为搜到的唯一一个叶节点的关键码。如下图,查询 3 3 3的前驱得到了正确答案 2 2 2。
⑦求
x
x
x的后继
首先执行在二叉排序树中查找 x x x的操作。如果没有找到,那么答案就在经过的节点的关键码中,可以采用“实时更新”的方式来快速求出答案。如果找到了呢?
假设在节点 r t rt rt这里找到了 x x x。我们先将 r t rt rt变成它的右孩子 r s o n r t rson_{rt} rsonrt,然后不停地将当前的 r t rt rt变成它的左孩子即可。最终答案为搜到的唯一一个叶节点的关键码。如下图,查询 3 3 3的得到了正确答案 4 4 4。
至此,我们讲完了 B S T BST BST的所有基本操作(除了删除操作,因为 B S T BST BST中的删除操作较为复杂;而使用平衡树在删除方面甚至比BST的删除操作更为简单,所以暂不赘述)。
现在我们评估一下 B S T BST BST的复杂度。这棵树的期望高度是 log q \log q logq的,所以期望时间复杂度就是 O ( q log q ) O(q \log q) O(qlogq)的。
但是,这只是期望复杂度。 B S T BST BST很容易就会退化为一条链。比如,插入的是一个有序序列,那么树的高度最终会变成 q q q,而复杂度就退化为了 O ( q 2 ) O(q^2) O(q2)……
此时,隆重登场的是我们的 平衡树 \huge \color {red} \text {平衡树} 平衡树。
Treap
首先很容易发现,在随机数据下树高是 log n \log n logn级别的。我们要巧妙运用“随机”的性质,来对这棵树进行一些变动,使得这棵树的高度越来越小,趋于平衡。
变动有两种: 第一种为右旋,第二种为左旋。先说说右旋:
初始的树:
右旋后的树:
当然,左旋与右旋相反,从第二个图到第一个图就是左旋。
这个旋转有什么用呢?我们随便画一条链,然后您旋转几次看看,会发现:这棵树越来越趋于平衡!!!
但是,这么时候应该执行左旋与右旋呢?我们可以在新建节点的同时,给这个节点随机赋予一个堆码,如果在一次“插入”或“删除”操作之后,发现 r t rt rt的左孩子的堆码或右孩子的堆码竟然大于了 r t rt rt的堆码(即不满足了大根堆的性质),那么就执行一次左旋或右旋的操作。更为具体地说,将被旋转上来作为这个子树的根的,是 r t rt rt的两个孩子中堆码大的那一个。可以发现,若每次操作都按照上述方法做合理的旋转,那么每次操作之后这棵树依然满足堆性质。
同时,旋转操作不仅能让这棵树趋于平衡,也能维护删除节点的操作。假设当前想要删除的节点为 r t rt rt,我们就不停地将 r t rt rt向下旋转,旋转到叶节点的时候,直接删除即可。注意在删除方面,我们每次旋转都要保证这棵树的堆性质。
于是,我们通过随机的方法,合理地进行左旋与右旋,保证了这棵树的“BST性质”与“平衡性质”。同时,我们也通过左旋与右旋,完美实现了节点的删除操作。这种做法合并了“Tree(Binary Search Tree)”的思想与"Heap(堆)"的思想,被称为 T r e a p Treap Treap。
Treap的时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)的。
Tips
Treap的板子是它。
这题中可能会出现相同的关键码。我们可以对于每一个节点多维护一个值 c n t cnt cnt,表示其关键码出现的次数。同时,一些函数以及数组的定义需要做简单的修改,比如 s i z e size size与 p u s h u p pushup pushup(即通过递推求出此节点的 s i z e size size)函数。
Treap的实现细节非常多,建议在写之前仔细阅读别人的AC代码。这里只说其中最重要的几点注意事项:
①实时
p
u
s
h
u
p
pushup
pushup;
②搞清“排名”与“比这个数小的数的数量”的关系,不要搞混;
③对于所有修改操作,都要实时判断是否满足大根堆性质;如果不满足,执行一次旋转;
④根据数据范围,合理选择建树时“inf”的大小。
Code
#include <bits/stdc++.h>
#define inf 2000000007
using namespace std;
int read(){
int s=0,w=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') w=-w;ch=getchar();}
while (ch>='0'&&ch<='9'){s=(s<<1)+(s<<3)+(ch^'0');ch=getchar();}
return s*w;
}
int q,root,tot=0;
int tree[100005][2],dat[100005],siz[100005],cnt[100005],val[100005];
void pushup(int rt){siz[rt]=siz[tree[rt][0]]+siz[tree[rt][1]]+cnt[rt];}
int New(int num){tot++;val[tot]=num,dat[tot]=rand(),siz[tot]=1,cnt[tot]=1;return tot;}
void build(){root=New(-inf),tree[root][1]=New(inf);pushup(root);}
void Rotate(int &rt,int d){
int tmp=tree[rt][d^1];
tree[rt][d^1]=tree[tmp][d],tree[tmp][d]=rt;
rt=tmp;
pushup(rt);pushup(tree[rt][d]);
}
void insert(int &rt,int num){
if (!rt){
rt=New(num);
return;
}
if (num==val[rt]) cnt[rt]++;
else{
int d=(num<val[rt])?0:1;
insert(tree[rt][d],num);
if (dat[tree[rt][d]]>dat[rt]) Rotate(rt,d^1);
}
pushup(rt);
return;
}
void Remove(int &rt,int num){
if (val[rt]==num){
if (cnt[rt]>1){
cnt[rt]--;pushup(rt);
return;
}
if (tree[rt][0]||tree[rt][1]){
if (dat[tree[rt][0]]>dat[tree[rt][1]]||tree[rt][1]==0)
Rotate(rt,1),Remove(tree[rt][1],num);
else Rotate(rt,0),Remove(tree[rt][0],num);
pushup(rt);
}
else rt=0;
return;
}
if (num<val[rt]) Remove(tree[rt][0],num);
else Remove(tree[rt][1],num);
pushup(rt);
}
int get_rnk(int rt,int num){
if (num<val[rt]) return get_rnk(tree[rt][0],num);
else if (num==val[rt]) return siz[tree[rt][0]]+1;
else return siz[tree[rt][0]]+cnt[rt]+get_rnk(tree[rt][1],num);
}
int get_val(int rt,int rnk){
if (rnk<=siz[tree[rt][0]]) return get_val(tree[rt][0],rnk);
else if (rnk<=siz[tree[rt][0]]+cnt[rt]) return val[rt];
else return get_val(tree[rt][1],rnk-siz[tree[rt][0]]-cnt[rt]);
}
int get_pre(int num){
int rt=root,pre=-inf;
while (rt){
if (val[rt]<num) pre=max(pre,val[rt]),rt=tree[rt][1];
else rt=tree[rt][0];
}
return pre;
}
int get_nxt(int num){
int rt=root,nxt=inf;
while (rt){
if (val[rt]>num) nxt=min(nxt,val[rt]),rt=tree[rt][0];
else rt=tree[rt][1];
}
return nxt;
}
signed main(){
srand(time(0));
q=read();build();
while (q--){
int opt=read(),x=read();
if (opt==1) insert(root,x);
else if (opt==2) Remove(root,x);
else if (opt==3) printf("%d\n",get_rnk(root,x)-1);
else if (opt==4) printf("%d\n",get_val(root,x+1));
else if (opt==5) printf("%d\n",get_pre(x));
else if (opt==6) printf("%d\n",get_nxt(x));
}
return 0;
}