平衡树

平衡树

平衡树

本文章中的部分代码和思路来自于 Treap - OI WikiSplay 树 - OI Wiki

平衡树是二叉查找树的一种,二叉查找树有一个很强的性质:中序遍历的结果是单调的。

例如下面这棵树:

它的中序遍历就是 $1,2,3,4,5,6,7$,并且树高是 $\log n$,这样的话我们在树上就算暴力跳边,时间复杂度也是 $\log$ 级别的。

但是如果我们是动态插入点的话,就可能会出现一条链的情况,这样的话操作就会退化成 $O(n)$ 级别,如下图所示:

虽然高度是这么大,但是依然满足二叉查找树的性质,于是就有了一个数据结构叫做平衡树,来强制让树高在 $\log$ 级别,此处主要叙述其中一种功能强大,常数小的 Treap。(Splay 请左转 LCT 区)

Treap

平衡树的核心操作就是旋转,因为我们发现同一个中序遍历可以有很多种树的形态,就像上面两张图展示的一样,我们先介绍有旋转的 Treap,再细说一下无旋 Treap(fhq-Treap)。

首先,我们定义左旋为把右儿子节点变到根节点;右旋为把左儿子节点变到根节点。如图所示(节点上的数字是编号,不是权值):

根据平衡树的定义,我们发现这样旋转完全不会影响中序遍历的结果。

于是就有了旋转代码:

1
2
3
4
5
6
7
8
9
10
11
12
void zig(ll &p){
ll q = tr[p].l;
tr[p].l = tr[q].r,tr[q].r = p;
p = q;
Update(tr[p].r),Update(p);
}
void zag(ll &p){
ll q = tr[p].r;
tr[p].r = tr[q].l,tr[q].l = p;
p = q;
Update(tr[p].l),Update(p);
}

其中 zig 是左旋,zag 是右旋。

那什么时候旋转呢?考虑我们学习过的笛卡尔树,它每个节点有两个值,其中一个的中序遍历单调,另外一个则满足堆性质,如果我们给 Treap 上的每个节点给一个堆的键值,如果随机赋值,那么期望高度就是 $O(\log)$ 级别的,这也是 Treap(Tree+heap)名字的由来。

考虑用小根堆的维护,如果父亲节点的堆的键值大于当前节点堆的键值,就往上旋转。(左旋或者右旋视当前节点是父亲的左/右儿子而定)

最后的结果就是这样的:

(第一个值是中序遍历到的值,第二个值是堆的键值)

到现在,平衡树就构建完了,那么我们考虑一下一些基本操作。为了方便起见,可以在树中插入 $-\text{inf}$ 和 $\text{inf}$ 两个值,避免处理边界情况。

插入

直接根据中序遍历的性质查找第一个能够插入的位置就可以了,同时记得更新节点存储的信息和堆的键值,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Insert(ll &p,ll val){
if(p==0){
p = New(val);
return ;
}
if(val==tr[p].val){
tr[p].cnt++,Update(p);
return;
}
if(val<tr[p].val){
Insert(tr[p].l,val);
if(tr[p].dat<tr[tr[p].l].dat) zig(p);
}
else{
Insert(tr[p].r,val);
if(tr[p].dat<tr[tr[p].r].dat) zag(p);
}
Update(p);
}

删除

跟插入一样,只不过找到节点之后删除需要合并它们的左右子树,分类讨论谁来当父节点就可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Remove(ll &p,ll val){
if(p==0) return ;
if(val==tr[p].val){
if(tr[p].cnt>1){
tr[p].cnt--;
Update(p);
return ;
}
if(tr[p].l||tr[p].r){
if(tr[p].r==0||tr[tr[p].l].dat>tr[tr[p].r].dat){
zig(p),Remove(tr[p].r,val);
}
else zag(p),Remove(tr[p].l,val);
Update(p);
}
else p=0;
return ;
}
val<tr[p].val?Remove(tr[p].l,val):Remove(tr[p].r,val);
Update(p);
}

前驱/后继

首先找到当前节点,然后跳左子树即可,如果没有左子树就往父亲节点走,后继同理,不再赘述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ll gp(ll val){
ll ans = 1,p = root;
while(p){
if(val==tr[p].val){
if(tr[p].l>0){
p = tr[p].l;
while(tr[p].r>0) p=tr[p].r;
ans = p;
}
break;
}
if(tr[p].val<val&&tr[p].val>tr[ans].val) ans=p;
p = val<tr[p].val?tr[p].l:tr[p].r;
}
return tr[ans].val;
}
ll gn(ll val){
ll ans = 2,p = root;
while(p){
if(val==tr[p].val){
if(tr[p].r>0){
p = tr[p].r;
while(tr[p].l>0) p=tr[p].l;
ans = p;
}
break;
}
if(tr[p].val>val&&tr[p].val<tr[ans].val) ans=p;
p = val<tr[p].val?tr[p].l:tr[p].r;
}
return tr[ans].val;
}

根据值查排名

找到那个值,途中累加经过的点的数量和左子树的数量即可,不难维护:

1
2
3
4
5
6
ll grbv(ll p,ll val){
if(p==0) return 0;
if(val==tr[p].val) return tr[tr[p].l].size+1;
if(val<tr[p].val) return grbv(tr[p].l,val);
return grbv(tr[p].r,val)+tr[tr[p].l].size+tr[p].cnt;
}

根据排名查值

类似于线段树上二分,如果排名在左子树里面,就跑到左子树里面,否则检查是否等于当前节点,最后跑到右子树里面即可。

1
2
3
4
5
6
ll gvbr(ll p,ll val){
if(p==0) return LLONG_MAX;
if(tr[tr[p].l].size>=val) return gvbr(tr[p].l,val);
if(tr[tr[p].l].size+tr[p].cnt>=val) return tr[p].val;
return gvbr(tr[p].r,val-tr[tr[p].l].size-tr[p].cnt);
}

综上所述,旋转 Treap 的功能大概就这样,接下来我们介绍一种更强大的平衡树:fhq-treap,它可以维护更多区间的信息,并且更好写。

非旋 Treap

非旋 Treap 的操作只有三个,但是我们可以通过这三个操作实现很多其他的技巧。

分裂-split

这是非旋 Treap 的核心操作,可以看一下下面那幅图,依然来自于 OI-wiki

这是按照权值分裂成左右两棵树,当然我们也可以按照树的大小来分裂,后者更为常用,我们可以控制前 $k$ 个元素分裂出来,并且分裂的时间复杂度也是 $O(\log)$ 的。

代码很简单,分类讨论一下根节点是要分裂到左边子树还是右边子树即可:

1
2
3
4
5
inline void split(ll x,ll k,ll &p1,ll &p2){
if(!x){p1=p2=0;return;}
if(k<=tr[tr[x].l].siz) split(tr[x].l,k,p1,tr[x].l),upd(x),p2=x;
else split(tr[x].r,k-tr[tr[x].l].siz-1,tr[x].r,p2),upd(x),p1=x;
}

合并-merge

按照堆的键值合并两棵子树,像线段树那样合并即可:

1
2
3
4
5
inline ll merge(ll p1,ll p2){
if(!p1||!p2) return p1+p2;
if(tr[p1].key<tr[p2].key) return tr[p1].r=merge(tr[p1].r,p2),upd(p1),p1;
else return tr[p2].l=merge(p1,tr[p2].l),upd(p2),p2;
}

注意:上面的合并生效当且仅当树上中序遍历不交,即一棵子树中序遍历出来是 $[l,r]$,另外一棵子树中序遍历出来是 $[s,t]$,满足 $r<s$ 才能用上面的函数合并。

那么我们如何处理中序遍历有交的 Treap 合并?于是我们考虑启发式合并的思想,利用如下方法完成合并:

设要合并的两棵树的根节点为 $p_1,p_2$,如果 $siz_{p_1}<siz_{p_2}$,则交换。

然后设 $p_1$ 的树值为 $v_1$,那么把 $p_2$ 分裂成值域为 $[1,v_1-1],[v_1,v_1],[v_1+1,n]$ 三段,然后中间那一段一定只有一个节点,将它与 $p_1$ 这个点合并即可(更新节点的 $cnt$,一个点可能有多个相同权值的点),然后对于剩下两段分别递归求解即可。

边界情况:$p_1=0$ 或者 $p_2=0$ 返回不等于 $0$ 的那一个。

最后这样做的时间复杂度是 $O(\log n)$(merge 函数外没有用 split 函数)或者 $O(\log^2 n)$(merge 函数外使用 split 函数)。下面给出这么做的参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
inline ll merge(ll p1,ll p2){
if(tr[p1].tag) pushdown(p1);
if(tr[p2].tag) pushdown(p2);
if(!p1||!p2) return p1+p2;
if(tr[p1].siz<tr[p2].siz) p1^=p2^=p1^=p2; //启发式合并
ll a1,a2,temp,a3;
split(p2,tr[p1].val,a1,a2); //这里的 split 是按权值分裂,不是按排名
split(a1,tr[p1].val-1,a3,temp); //这里的 split 是按权值分裂,不是按排名
tr[p1].cnt += tr[temp].cnt;
tr[p1].l = merge(tr[p1].l,a3),tr[p1].r = merge(tr[p1].r,a2);
upd(p1);
return p1;
}

求某个值的排名

像旋转 Treap 那样累加左子树的大小和经过节点的数量即可:

1
2
3
4
5
inline ll rankk(ll p,ll k){
if(!p) return 0;
if(k<tr[p].p) return rankk(tr[p].l,k);
else return tr[tr[p].l].siz+1+rankk(tr[p].r,k);
}

这三个操作就是非旋转 Treap 的基本操作。

接下来我们考虑用这三个操作处理另外的信息:

  • 插入一个值:找到这个值的排名(小于等于这个值的数量),然后按照排名分裂成两个子树,最后把这个值放到中间合并即可:

    1
    2
    3
    4
    5
    6
    inline void insert(ll k){
    tr[++tot] = (node){0,0,1,k,rnd()};
    ll rank = rankk(root,k),r1,r2;
    split(root,rank,r1,r2);
    root = merge(merge(r1,tot),r2);
    }
  • 删除一个值:找到这个值的排名(小于等于这个值的数量),然后按照排名分裂成三个子树,最后删去中间的子树,合并其余两棵子树即可:

    1
    2
    3
    4
    inline void erase(ll k){
    ll rank = rankk(root,k),r1,r2,r3,tmp;
    split(root,rank,r1,r2),split(r1,rank-1,r3,tmp),root=merge(r3,r2);
    }
  • 查找前驱/后继,找到这个值的排名,然后根据排名找值就可以了,实现很简单:

    1
    2
    3
    4
    5
    inline ll find(ll k){             //根据排名找值的函数
    ll r1,r2,r3,tmp;
    split(root,k,r1,r2),split(r1,k-1,r3,tmp),root=merge(merge(r3,tmp),r2);
    return tr[tmp].p;
    }

注:无旋 Treap 甚至不需要讨论边界情况,当 $root=0$ 的时候就是边界情况,程序会自动处理。

区间操作

除此之外,非旋 Treap 还支持各种各样的区间操作,下面我们列举几个:

区间翻转

首先,因为非旋 Treap 只有分裂合并操作,没有旋转,区间的 pushdownpushup 比较好实现,就有了区间翻转这个操作。

如果我们想要整体翻转一个二叉树的中序遍历,对于每个节点都翻转左右节点就可以了。

假如说我们要翻转 $[l,r]$ 这个区间,就通过分裂操作分裂成 $[1,l-1]$,$[l,r]$,$[r+1,n]$ 三个区间对应的子树,对于中间那个区间的子树的根打上区间翻转的 tag 即可,同时记得翻转左右子树。

特别注意:splitmergerank 查询的时候一定要先下放标记,再执行操作,下面附上 P3391 【模板】文艺平衡树 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<bits/stdc++.h>
#define ll long long
#define N 2000005
using namespace std;
struct node{ll l,r,siz,p,key,tag;}tr[N];
mt19937 rnd(time(0));
ll n,m,i,x,y,tot,root,ans,ANS;
inline void upd(ll x){tr[x].siz = tr[tr[x].l].siz+tr[tr[x].r].siz+1;}
inline void pushtag(ll p){if(p) tr[p].tag^=1,swap(tr[p].l,tr[p].r);}
inline void pushdown(ll p){if(tr[p].tag) pushtag(tr[p].l),pushtag(tr[p].r),tr[p].tag=0;}
inline void split(ll x,ll k,ll &p1,ll &p2){
if(!x){p1=p2=0;return;}
pushdown(x); //记得pushdown
if(k<=tr[tr[x].l].siz) split(tr[x].l,k,p1,p2),tr[x].l=p2,upd(x),p2=x;
else split(tr[x].r,k-tr[tr[x].l].siz-1,p1,p2),tr[x].r=p1,upd(x),p1=x;
}
inline ll merge(ll p1,ll p2){
if(!p1||!p2) return p1+p2;
pushdown(p1),pushdown(p2); //记得pushdown
if(tr[p1].key<tr[p2].key) return tr[p1].r=merge(tr[p1].r,p2),upd(p1),p1;
else return tr[p2].l=merge(p1,tr[p2].l),upd(p2),p2;
}
inline ll rankk(ll p,ll k){
if(!p) return 0;
pushdown(p); //记得pushdown
if(k<tr[p].p) return rankk(tr[p].l,k);
else return tr[tr[p].l].siz+1+rankk(tr[p].r,k);
}
inline void insert(ll k){
tr[++tot] = (node){0,0,1,k,rnd()};
ll rank = rankk(root,k),r1,r2;
split(root,rank,r1,r2);
root = merge(merge(r1,tot),r2);
}
inline void solve(ll x,ll y){
ll r1,r2,r3,tmp;
split(root,y,r1,r2),split(r1,x-1,r3,tmp),pushtag(tmp),root=merge(merge(r3,tmp),r2);
}
inline void dfs(ll x){
if(!x) return ;
pushdown(x),dfs(tr[x].l),cout<<tr[x].p<<" ",dfs(tr[x].r);
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
for(i=1;i<=n;i++) insert(i);
while(m--){
cin>>x>>y;
solve(x,y);
}
dfs(root);
return 0;
}

区间加/赋值

很简单,不再赘述,套路如线段树下放 tag 那样:

1
2
3
4
5
6
7
inline void pushtag(ll p,ll c,ll c2){
if(!p) return ;
if(c!=inf){
tr[p].val=c,tr[p].s=c*tr[p].siz,tr[p].tag=c;
}
}
inline void pushdown(ll p){pushtag(tr[p].l,tr[p].tag),pushtag(tr[p].r,tr[p].tag),tr[p].tag=inf;}

区间维护最大子段和

只需要记住 pushup 的时候更新即可,如果有其它的操作,务必确保操作的先后顺序和操作的合并,下面是有区间赋值/翻转/最大子段和的 pushdownupdpushtag 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
inline void upd(ll x){
tr[x].siz = tr[tr[x].l].siz+tr[tr[x].r].siz+1;
tr[x].s = tr[tr[x].l].s + tr[tr[x].r].s + tr[x].val;
tr[x].ms = max({tr[tr[x].l].ms,tr[tr[x].r].ms,max(tr[tr[x].l].rs,0)+max(tr[tr[x].r].ls,0)+tr[x].val});
tr[x].ls = max({tr[tr[x].l].ls,tr[tr[x].l].s+tr[x].val,tr[tr[x].l].s+tr[x].val+tr[tr[x].r].ls});
tr[x].rs = max({tr[tr[x].r].rs,tr[tr[x].r].s+tr[x].val,tr[tr[x].r].s+tr[x].val+tr[tr[x].l].rs});
}
inline void pushtag(ll p,ll c,ll c2){
if(!p) return ;
if(c2!=0){
tr[p].tag2^=1;
swap(tr[p].l,tr[p].r),swap(tr[p].ls,tr[p].rs);
}
if(c!=inf){
tr[p].val=c,tr[p].s=c*tr[p].siz,tr[p].tag=c;
if(c>=0) tr[p].ms=tr[p].ls=tr[p].rs=tr[p].s;
else tr[p].ms=tr[p].ls=tr[p].rs=c;
}
}
inline void pushdown(ll p){pushtag(tr[p].l,tr[p].tag,tr[p].tag2),pushtag(tr[p].r,tr[p].tag,tr[p].tag2),tr[p].tag=inf,tr[p].tag2=0;}

Splay

Splay 树一般是学习 Link-Cut Tree 的基础,因为 Splay 的时间复杂度分析需要用到势能分析法,详见 OI-wiki,此处不做展开。

Splay 节点维护的信息约等于 Treap,但是每个节点需要记录当前节点所代表的值有多少个,即 cnt 变量,否则时间复杂度会不对。

基本操作

pushup(x):更新 $x$ 节点以及其子树中的所有信息。

clear(x):销毁 $x$ 节点。

get(x):获取 $x$ 是其父亲的左儿子节点还是有儿子节点。

前两个我们很熟悉,就不讨论了,最后一个是为了后面代码的方便使用,我们规定,如果是左儿子节点返回 $0$,否则返回 $1$,并且使用此函数的时候 $x$ 一定不是 Splay 的根。

扩展操作

旋转

旋转是 Splay 的重点,同 Treap,下面是 Splay 的旋转图:

如果 $x$ 要旋转并且 $x$ 是 $fa_x$ 的左儿子,那么把 $x$ 旋转到根的过程称为右旋,否则称为左旋。

这样旋转不会影响中序遍历的结果,因此我们以右旋为例来看一下如何在代码中实现:

假设需要旋转的节点是 $x$,其父亲为 $y$,在上图中,我们从右往左看,$x=2,y=3$。

  • 首先令 $y$ 的左儿子指向 $x$ 的右儿子,如果 $x$ 有右儿子,那么让 $x$ 的右儿子的父亲指向 $y$。
  • 然后将 $x$ 的右儿子指向 $y$,并更新 $y$ 的父亲为 $x$,清空 $x$ 的父亲。
  • 如果 $y$ 原来有父亲 $z$,如果 $y$ 是 $z$ 的左儿子,那么更新 $z$ 的左儿子为 $x$,$x$ 的父亲为 $z$,反之同理。

左旋是一样的道理,使用位运算就不需要区分左旋还是右旋,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
void rotate(int x) {
int y = fa[x], z = fa[y], chk = get(x);
ch[y][chk] = ch[x][chk ^ 1];
if (ch[x][chk ^ 1]) fa[ch[x][chk ^ 1]] = y;
ch[x][chk ^ 1] = y;
fa[y] = x;
fa[x] = z;
if (z) ch[z][y == ch[z][1]] = x;
maintain(y); //记得更新节点信息(有的时候 z 也需要更新)
maintain(x);
}

Splay

Splay 操作规定,每次访问了一个节点都要强制把它旋转到根节点,并且在旋转的过程中要尽量保证树高(感性理解一下)。

于是我们有下列三种操作:

ZIG/ZAG

当 $x$ 的父亲 $p$ 是这棵树的根节点的时候,直接沿着 $x-p$ 这条边旋转一次即可,要么 ZIG 要么 ZAG。

ZIG-ZIG/ZAG-ZAG

当 $x$ 和父亲 $p$ 都是它们父亲的左儿子或者右儿子(即 $x-p-q$,$q$ 为 $p$ 的父亲,这三个点构成一条链的时候),需要先沿着 $p-q$ 旋转,再沿着 $x-p$ 旋转,旋转后,这条链就会反向。如果不是这样旋转,那么时间复杂度会出错。

ZIG-ZAG/ZAG-ZIG

最后一种情况,$x$ 和父亲 $p$ 不全是左儿子或者右儿子,那么先旋转 $x-p$,然后 $x$ 和 $p$ 的父亲 $q$ 会生成新的边,最后沿着这条边旋转即可。

写成代码就是下面这个样子:

1
2
3
4
5
void splay(int x) {
for (int f = fa[x]; f = fa[x], f; rotate(x))
if (fa[f]) rotate(get(x) == get(f) ? f : x);
rt = x;
}

插入

插入节点 $x$:

  • 树空,直接插入。
  • 如果有节点与 $x$ 权值相同,那么令 cnt 改变即可。
  • 否则按照二叉搜索树的性质找,找到空节点插入即可,最后 Splay。

查询排名/排名为 $x$ 的数

同 Treap,直接在树上通过二叉搜索树的性质找即可。

最后找到了再 Splay 到根。

查询前驱/后继

直接插入 $x$,然后因为这个时候 $x$ 在根节点,所以 $x$ 的前驱是左子树中最后一个节点;后继是右子树中最前面一个节点。

返回答案前记得 Splay 找到的节点到根。

合并两棵 Splay 树

这个操作需要保证值域无交,即一棵树的最大值严格小于另外一棵树的最小值。

  • 如果 $x$ 和 $y$ 其中之一或两者都为空树,直接返回不为空的那一棵树的根节点或空树。
  • 否则将 $x$ 树中的最大值 Splay 到根,然后把它的右子树设置为 $y$​ 并更新节点的信息,然后返回这个节点。

删除

直接把节点 $x$ Splay 到根,然后看 cnt 是否为 $1$,如果不是,直接减去 $1$ 即可,否则合并其左右两棵子树。

其他操作

例如 tag 下放等,需要注意下放的时间以及 pushtag 这个函数里面的边界条件判断,这个东西并不算难,不过细节有的时候有点多。

代码

以洛谷普通平衡树为例,下面展示一下此题代码,时间复杂度为 $O(n \log n)$:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#include <cstdio>
const int N = 100005;
int rt, tot, fa[N], ch[N][2], val[N], cnt[N], sz[N];

struct Splay {
void maintain(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + cnt[x]; }

bool get(int x) { return x == ch[fa[x]][1]; }

void clear(int x) {
ch[x][0] = ch[x][1] = fa[x] = val[x] = sz[x] = cnt[x] = 0;
}

void rotate(int x) {
int y = fa[x], z = fa[y], chk = get(x);
ch[y][chk] = ch[x][chk ^ 1];
if (ch[x][chk ^ 1]) fa[ch[x][chk ^ 1]] = y;
ch[x][chk ^ 1] = y;
fa[y] = x;
fa[x] = z;
if (z) ch[z][y == ch[z][1]] = x;
maintain(y);
maintain(x);
}

void splay(int x) {
for (int f = fa[x]; f = fa[x], f; rotate(x))
if (fa[f]) rotate(get(x) == get(f) ? f : x);
rt = x;
}

void ins(int k) {
if (!rt) {
val[++tot] = k;
cnt[tot]++;
rt = tot;
maintain(rt);
return;
}
int cur = rt, f = 0;
while (1) {
if (val[cur] == k) {
cnt[cur]++;
maintain(cur);
maintain(f);
splay(cur);
break;
}
f = cur;
cur = ch[cur][val[cur] < k];
if (!cur) {
val[++tot] = k;
cnt[tot]++;
fa[tot] = f;
ch[f][val[f] < k] = tot;
maintain(tot);
maintain(f);
splay(tot);
break;
}
}
}

int rk(int k) {
int res = 0, cur = rt;
while (1) {
if (k < val[cur]) {
cur = ch[cur][0];
} else {
res += sz[ch[cur][0]];
if (!cur) return res + 1;
if (k == val[cur]) {
splay(cur);
return res + 1;
}
res += cnt[cur];
cur = ch[cur][1];
}
}
}

int kth(int k) {
int cur = rt;
while (1) {
if (ch[cur][0] && k <= sz[ch[cur][0]]) {
cur = ch[cur][0];
} else {
k -= cnt[cur] + sz[ch[cur][0]];
if (k <= 0) {
splay(cur);
return val[cur];
}
cur = ch[cur][1];
}
}
}

int pre() {
int cur = ch[rt][0];
if (!cur) return cur;
while (ch[cur][1]) cur = ch[cur][1];
splay(cur);
return cur;
}

int nxt() {
int cur = ch[rt][1];
if (!cur) return cur;
while (ch[cur][0]) cur = ch[cur][0];
splay(cur);
return cur;
}

void del(int k) {
rk(k);
if (cnt[rt] > 1) {
cnt[rt]--;
maintain(rt);
return;
}
if (!ch[rt][0] && !ch[rt][1]) {
clear(rt);
rt = 0;
return;
}
if (!ch[rt][0]) {
int cur = rt;
rt = ch[rt][1];
fa[rt] = 0;
clear(cur);
return;
}
if (!ch[rt][1]) {
int cur = rt;
rt = ch[rt][0];
fa[rt] = 0;
clear(cur);
return;
}
int cur = rt;
int x = pre();
fa[ch[cur][1]] = x;
ch[x][1] = ch[cur][1];
clear(cur);
maintain(rt);
}
} tree;

int main() {
int n, opt, x;
for (scanf("%d", &n); n; --n) {
scanf("%d%d", &opt, &x);
if (opt == 1)
tree.ins(x);
else if (opt == 2)
tree.del(x);
else if (opt == 3)
printf("%d\n", tree.rk(x));
else if (opt == 4)
printf("%d\n", tree.kth(x));
else if (opt == 5)
tree.ins(x), printf("%d\n", val[tree.pre()]), tree.del(x);
else
tree.ins(x), printf("%d\n", val[tree.nxt()]), tree.del(x);
}
return 0;
}

可持久化平衡树

有旋转的平衡树处理可持久化十分困难,于是我们可持久化一般针对于无旋 Treap。

实现起来也很简单,只需要把 splitmerge 过的所有节点保存下来,因为每次操作是 $\log$ 级别的,空间也是 $\log$ 级别的,最好是能开多大就开多大,因为 Treap 的树高完全取决于随机数。

特别的,如果还有 pushdown 操作的话,pushdown 的时候也要把左右儿子复制一份才行,不然的话会修改到之前的数据。

这里以 P5055【模板】可持久化文艺平衡树 为例介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include<bits/stdc++.h>
#define ll long long
#define N 500005
using namespace std;
struct node{ll l,r,siz,p,key,cnt,cntt,tag;}tr[N*60];
mt19937 rnd(time(0));
ll n,i,opt,x,y,z,tot,root[N],ans;
inline void upd(ll x){tr[x].siz = tr[tr[x].l].siz+tr[tr[x].r].siz+1,tr[x].cnt = tr[x].cntt+tr[tr[x].l].cnt+tr[tr[x].r].cnt;}
inline void pushtag(ll p){if(p) tr[p].tag^=1;}
inline void pushdown(ll p){
if(tr[p].tag){
ll tmp;
if(tr[p].l) tmp=tr[p].l,tr[p].l=++tot,tr[tot]=tr[tmp],pushtag(tr[p].l); //此处需要复制
if(tr[p].r) tmp=tr[p].r,tr[p].r=++tot,tr[tot]=tr[tmp],pushtag(tr[p].r); //此处需要复制
swap(tr[p].l,tr[p].r);
tr[p].tag=0;
}
}
inline void split(ll x,ll k,ll &p1,ll &p2){
if(!x){p1=p2=0;return;}
pushdown(x);
if(k<=tr[tr[x].l].siz){
p2=++tot,tr[p2]=tr[x]; //此处需要复制
split(tr[p2].l,k,p1,tr[p2].l),upd(p2);
}
else{
p1=++tot,tr[p1]=tr[x]; //此处需要复制
split(tr[p1].r,k-tr[tr[p1].l].siz-1,tr[p1].r,p2),upd(p1);
}
}
inline ll merge(ll p1,ll p2){
if(!p1||!p2) return p1+p2;
pushdown(p1),pushdown(p2);
ll tmp = ++tot;
if(tr[p1].key<tr[p2].key) return tr[tmp]=tr[p1],tr[tmp].r=merge(tr[tmp].r,p2),upd(tmp),tmp; //此处需要复制
else return tr[tmp]=tr[p2],tr[tmp].l=merge(p1,tr[tmp].l),upd(tmp),tmp; //此处需要复制
}
inline ll rankk(ll p,ll k){
if(!p) return 0;
pushdown(p);
if(k<tr[p].p) return rankk(tr[p].l,k);
else return tr[tr[p].l].siz+1+rankk(tr[p].r,k);
}
inline void insert(ll k){
tr[++tot] = (node){0,0,1,k,rnd()};
ll now = tot;
ll rank = rankk(root[i],k),r1,r2;
split(root[i],rank,r1,r2);
root[i] = merge(merge(r1,now),r2);
}
inline void erase(ll k){
ll rank = rankk(root[i],k),r1,r2,r3,tmp;
split(root[i],rank,r1,r2),split(r1,rank-1,r3,tmp);
if(tr[tmp].p==k) root[i]=merge(r3,r2);
else root[i]=merge(merge(r3,tmp),r2);
}
inline ll find(ll k){
ll r1,r2,r3,tmp;
split(root[i],k,r1,r2),split(r1,k-1,r3,tmp),root[i]=merge(merge(r3,tmp),r2);
return tr[tmp].p;
}
int main(){
ios::sync_with_stdio(false);
cin>>n;
for(i=1;i<=n;i++){
cin>>opt>>x;
root[i] = root[opt];
if(x==1){
cin>>y>>z,y^=ans,z^=ans;
ll r1,r2;
split(root[i],y,r1,r2);
tr[++tot] = (node){0,0,1,z,rnd(),z,z,0};
root[i]=merge(merge(r1,tot),r2);
}
if(x==2){
cin>>y,y^=ans;
ll r1,r2,r3,tmp;
split(root[i],y,r1,r2),split(r1,y-1,r3,tmp),root[i]=merge(r3,r2);
}
if(x==3){
cin>>y>>z,y^=ans,z^=ans;
ll r1,r2,r3,tmp;
split(root[i],z,r1,r2),split(r1,y-1,r3,tmp),pushtag(tmp),root[i]=merge(merge(r3,tmp),r2);
}
if(x==4){
cin>>y>>z,y^=ans,z^=ans;
ll r1,r2,r3,tmp;
split(root[i],z,r1,r2),split(r1,y-1,r3,tmp),cout<<(ans=tr[tmp].cnt)<<endl,root[i]=merge(merge(r3,tmp),r2);
}
}
return 0;
}

最后提醒一下,若操作节点编号为 $0$ 请不要操作,因为这是边界情况。

学到这里,才算是真正入门了平衡树。