简介
平衡树,顾名思义,平衡的搜索二叉树。
常见的平衡树都能将树的深度保持在 lg n \lg_n lgn 的级别内,防止退化成链。
一些平衡树可以通过旋转、分裂、合并等操作完成更加高级的、二叉搜索树做不到的操作。
二叉搜索树
在讲平衡树之前,先介绍这种二叉树。
1、性质&特点
首先,规定:对于节点 x x x , x x x 左子树内任一点权值< x x x 权值< x x x 右子树内任一点权值。
这一条性质也被称为 bst ( b i n a r y s e a r c h t r e e ) \text{bst}(binary\ search\ tree) bst(binary search tree) 性质。
由此性质可以容易地发现对于一棵二叉搜索树,如果没有插入或删除,节点的中序遍历是固定的。
当然,是在没有重复元素的前提下。至于重复元素在下面。
2、支持操作
先规定以下变量、数组:
-
tot,rt
:节点数量以及根的编号。 -
v[i]
:节点 i i i 的权值。 -
fa[i]
:节点 i i i 的父亲编号。 -
chi[i][2]
:节点 i i i 的左/右儿子。 -
cnt[i]
:节点 i i i 的权值存在数量(如数列1 1 2 3,如果v[3]=1
,那么cnt[3]=2
)。 -
sz[i]
:节点 i i i 的子树中权值数量。形式化地写,sz[i]=sz[chi[i][0]]+sz[chi[i][1]]+cnt[i]
。
特别地,空节点编号为0。
插入一个元素:根据 bst \text{bst} bst 性质,从根开始,向左右两侧跳。直到空位或者已有该元素大小的节点。
查询元素 x x x 的前驱:先根据 bst \text{bst} bst 性质,找到元素 x x x 所在的节点。如果 x x x 有左子树,那么前驱是它左子树内最靠右的节点;如果 x x x 没有左子树但有父亲,那前驱就是它的深度最深的、 x x x 位于右子树内的祖先。
查询元素 x x x 的后继:类似前驱。
查询元素 x x x 的排名:
- 如果 x x x 小于当前权值,向左子树。
- 答案加上左子树大小,如果 x x x 等于当前权值,将答案加1并返回;否则加上当前点的 cnt \text{cnt} cnt 并向右子树。
查询排名为 k k k 的数值:
- 如果 k k k 小于左子树大小,向左子树。
- 将 k k k 减去左子树大小。如果 k ≤ 0 k\le0 k≤0 ,返回当前权值;否则向右子树。
3、死因
不难发现,如果我一直插入 1 , 2 , 3...... n 1,2,3......n 1,2,3......n ,这棵树会因此而退化成一条链。
如果离线处理,可以考虑将插入的数排序后再递归建树:
Function : d g ( l , r ) i f l = r t h e n i n s e r t ( l ) , r e t u r n . m i d = ( l + r ) / 2. i n s e r t ( m i d ) . d g ( l , m i d − 1 ) , d g ( m i d + 1 , r ) . \text{Function}:dg(l,r)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \\ \ \ \ if\ \ l=r\ \ then\ \ insert(l),return.\\ mid=(l+r)/2.\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \\ insert(mid). \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \\ dg(l,mid-1),dg(mid+1,r).\ Function:dg(l,r) if l=r then insert(l),return.mid=(l+r)/2. insert(mid). dg(l,mid−1),dg(mid+1,r).
但是,如果强制在线的话,普通的平衡二叉树便会不得不退化成一条链了。
平衡树的故事从此开始。
Splay树
由Tarjan爷爷和另一位曾获图灵奖的计算机学家Daniel Sleator于1985年发明的平衡树。
1、工作原理
旋转。
具体来说,以一种方式在特定情况下(例如插入节点后)将一个节点旋转到根并不破坏 bst \text{bst} bst 性质。在这个过程中以保持树的“平衡”。
2、旋转
看起来,一步一步转比较合理。
图1:
设 x x x 的父亲是 f f f , f f f 的父亲是 g f gf gf 。
如果我们要旋转一次 x x x ,那么会出现这种情况:
图2:
我们不可能像对顶堆那样做,因为我们要的是二叉树,而不是这个“乱七八糟”的玩意。(尽管现在它还是比较合理)
这时,我们发现,子树2与 f f f 空余的左子树完全吻合。因为由图一我们知道 x < [ 2 ] < f x<[2]<f x<[2]<f ,而 f f f 的左子树需要的条件同样是大于 x x x 且小于 f f f ,c此时如果我们把子树2“移到” f f f 的左子树上,问题就解决了,如图3。
图3:
所以,可以得出旋转的步骤(以上述图为例):
- 将 f f f 的左儿子设为 x x x 的右儿子,将 x x x 右儿子的父亲设为 f f f 。
- 将 x x x 的右儿子设为 f f f ,将 f f f 的父亲设为 x x x 。
- 将 g f gf gf 的右儿子设为 x x x ,将 x x x 的父亲设为 g f gf gf 。
- 重构 x , f x,f x,f 的子树大小。
代码在第4节给出。
那么,什么时候应该旋转?
明显地,插入一个数后应该旋转以保持平衡。
并且,对于查询前驱后继,可以先把值为 x x x 的节点旋转到根。这样就只有一种情况了。
但是不难发现,如果一直旋转 x x x ,对于二叉搜索树的死因,依然会死(一条右链转为左链)。
所以,大师们有有了一个办法:如果 g f , f , x gf,f,x gf,f,x 三点“共线”,先旋转 f f f 。
“共线”就是说 x x x 是 f f f 的左儿子并且 f f f 是 g f gf gf 的左儿子,或者 x x x 是 f f f 的右儿子并且 f f f 是 g f gf gf 的右儿子。
至于这样操作的时间复杂度,详见大佬的证明。
3、删除&实现技巧
问题来了,删除一个值为 x x x 的数怎么办?
4、一(亿)些代码&实现
-
几个基本函数:
//返回x作为它父亲的左还是右儿子 int get(int x) { return chi[fa[x]][1]==x; } //清除点x void clear(int x) { sz[x]=v[x]=chi[x][0]=chi[x][1]=fa[x]=cnt[x]=0; } //重构x的子树大小 void mt(int x) { sz[x]=sz[chi[x][0]]+sz[chi[x][1]]+cnt[x]; }
-
旋转:
//旋转一次x void rotate(int x) { int f=fa[x],gf=fa[fa[x]],lx=get(x),lf=get(f); chi[f][lx]=chi[x][!lx]; if(chi[x][!lx]) fa[chi[x][!lx]]=f; chi[x][!lx]=f; fa[f]=x; if(g) chi[g][lf]=x; fa[x]=gf; mt(f); mt(x); } //将x旋转到根 void splay(int x) { for(int f=fa[x];f=fa[x],f;rotate(x)) //for中间的约束代表是x的父亲不是空节点 if(fa[f]) rotate(get(x)==get(f)?f:x); rt=x; }
-
插入:
void ins(int x) { //空树 if(!rt) { v[++tot]=x; cnt[tot]++; rt=tot; mt(rt); return; } int y=rt,f=0; while(1) { if(v[y]==x) { //有相同元素 cnt[y]++; mt(y); mt(f); //别忘了splay splay(y); break; } f=y; y=chi[y][v[y]<x]; if(!y) { //空元素 v[++tot]=x; cnt[tot]++; fa[tot]=f; chi[f][v[f]<x]=tot; mt(tot); mt(f); //别忘了splay splay(tot); break; } } }
-
将权值为 x x x 的点旋到根
void find(int x) { int y=rt; while(1) { if(v[y]==x) { splay(y); return; } if(v[y]>x) y=chi[y][0]; else y=chi[y][1]; } }
-
删除值为 x x x 的点
//删除值为x的点 void del(int x) { find(x); if(cnt[rt]>1) { cnt[rt]--; mt(rt); return; } int ls=chi[rt][0],rs=chi[rt][1]; if(ls&&rs) { int p=pren(x); clear(rt); rt=ls; fa[ls]=fa[rs]=0; splay(p); chi[rt][1]=rs; fa[rs]=rt; mt(rt); } else if(ls) { clear(rt); fa[ls]=0; rt=ls; } else if(rs) { clear(rt); fa[rs]=0; rt=rs; } else { clear(rt); tot=rt=0; } }
-
查询 x x x 的排名
int rk(int x) { //防止不存在x ins(x); //此时x已经是根 int s=sz[chi[rt][0]]+1; del(x); return s; }
-
前驱后继的事宜
//返回pre编号 int pren(int x) { ins(x); int y=chi[rt][0]; if(!y) return 0; while(chi[y][1]) y=chi[y][1]; del(x); return y; } //返回nxt编号 int nxtn(int x) { ins(x); int y=chi[rt][1]; if(!y) return 0; while(chi[y][0]) y=chi[y][0]; del(x); return y; } //返回pre值 int pre(int x) { return v[pren(x)]; } //返回nxt值 int nxt(int x) { return v[nxtn(x)]; }
-
查排名为 x x x 的值
//返回排名为x点的值 int kth(int x) { int y=rt; while(1) { if(chi[y][0]&&x<=sz[chi[y][0]]) y=chi[y][0]; else { x=x-sz[chi[y][0]]-cnt[y]; if(x<=0) { splay(y); return v[y]; } y=chi[y][1]; } } return v[y]; }
5、例题:[Luogu] P3369 【模板】普通平衡树
#include<cstdio>
using namespace std;
const int N=4e5+5;
int n;
struct phs {
int v[N],cnt[N],fa[N],sz[N],chi[N][2],rt,tot;
void init() {
rt=tot=0;
}
int get(int x) {
return chi[fa[x]][1]==x;
}
void clear(int x) {
sz[x]=v[x]=chi[x][0]=chi[x][1]=fa[x]=cnt[x]=0;
}
void mt(int x) {
sz[x]=sz[chi[x][0]]+sz[chi[x][1]]+cnt[x];
}
void rotate(int x) {
int f=fa[x],gf=fa[fa[x]],lx=get(x),lf=get(f);
chi[f][lx]=chi[x][!lx];
if(chi[x][!lx]) fa[chi[x][!lx]]=f;
chi[x][!lx]=f;
fa[f]=x;
if(gf) chi[gf][lf]=x;
fa[x]=gf;
mt(f);
mt(x);
}
void splay(int x) {
if(x==rt) return;
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 x) {
if(!rt) {
v[++tot]=x;
cnt[tot]++;
rt=tot;
mt(rt);
return;
}
int y=rt,f=0;
while(1) {
if(v[y]==x) {
cnt[y]++;
mt(y);
mt(f);
splay(y);
break;
}
f=y;
y=chi[y][v[y]<x];
if(!y) {
v[++tot]=x;
cnt[tot]++;
fa[tot]=f;
chi[f][v[f]<x]=tot;
mt(tot);
mt(f);
splay(tot);
break;
}
}
}
void find(int x) {
int y=rt;
while(1) {
if(v[y]==x) {
splay(y);
return;
}
if(v[y]>x) y=chi[y][0];
else y=chi[y][1];
}
}
int rk(int x) {
ins(x);
int s=sz[chi[rt][0]]+1;
del(x);
return s;
}
int pren(int x) {
ins(x);
int y=chi[rt][0];
if(!y) return 0;
while(chi[y][1]) y=chi[y][1];
del(x);
return y;
}
int nxtn(int x) {
ins(x);
int y=chi[rt][1];
if(!y) return 0;
while(chi[y][0]) y=chi[y][0];
del(x);
return y;
}
int pre(int x) {
return v[pren(x)];
}
int nxt(int x) {
return v[nxtn(x)];
}
void del(int x) {
find(x);
if(cnt[rt]>1) {
cnt[rt]--;
mt(rt);
return;
}
int ls=chi[rt][0],rs=chi[rt][1];
if(ls&&rs) {
int p=pren(x);
clear(rt);
rt=ls;
fa[ls]=fa[rs]=0;
splay(p);
chi[rt][1]=rs;
fa[rs]=rt;
mt(rt);
} else if(ls) clear(rt),fa[ls]=0,rt=ls;
else if(rs) clear(rt),fa[rs]=0,rt=rs;
else clear(rt),tot=rt=0;
}
int kth(int x) {
int y=rt;
while(1) {
if(chi[y][0]&&x<=sz[chi[y][0]]) y=chi[y][0];
else {
x=x-sz[chi[y][0]]-cnt[y];
if(x<=0) {
splay(y);
return v[y];
}
y=chi[y][1];
}
}
return v[y];
}
}t;
int main() {
t.init();
scanf("%d",&n);
for(int i=1;i<=n;i++) {
int f,x;
scanf("%d%d",&f,&x);
switch(f) {
case 1:
t.ins(x);
break;
case 2:
t.del(x);
break;
case 3:
printf("%d\n",t.rk(x));
break;
case 4:
printf("%d\n",t.kth(x));
break;
case 5:
printf("%d\n",t.pre(x));
break;
case 6:
printf("%d\n",t.nxt(x));
break;
}
}
}
6、高级操作
不难发现,上述操作通通都可以用权值线段树完成。
那这么长的代码能干什么!
当然是干一些很多数据结构干不了的事情,例如区间翻转。
[Luogu] P3391 【模板】文艺平衡树
大意就是翻转很多个区间。
问题来了:怎么在Splay中找到 [ l , r ] [l,r] [l,r] ?
我们让Splay的中序遍历等于原序列,树上「中序遍历」的第 k k k 个点就是序列的第 k k k 位。
接着把「中序遍历」的第 l − 1 l-1 l−1 个点旋转到根,「中序遍历」的第 r + 1 r+1 r+1 个点旋转到 l − 1 l-1 l−1 的右儿子(显然它不会到左儿子)。
现在的中序遍历: [ 1 ] , l − 1 , [ 2 ] , r + 1 , [ 3 ] [1],l-1,[2],r+1,[3] [1],l−1,[2],r+1,[3] 。
显然, [ 2 ] [2] [2] 就是区间 [ l , r ] [l,r] [l,r] 。
然后我们要把 [ 2 ] [2] [2] 的全部左右儿子交换,这样Splay的中序遍历就是翻转后的序列。
类比线段树,我们用懒标记。
因为我们旋转的是「中序遍历」,所以丝毫不用担心翻转后影响什么。
此时的树不满足 bst \text{bst} bst 性质。
实现上,找「中序遍历」的第 k k k 个点,魔改一下查排名就可以了。
而且 l − 1 l-1 l−1 可能为0, r + 1 r+1 r+1 可能为 n + 1 n+1 n+1 。所以要把这两个点建到树里,然后一切查「中序遍历」的第 k k k 个点都要变成 k + 1 k+1 k+1 。
建树类比线段树。
可以发现,如果要让Splay像线段树一样维护区间和的话,也是如此的操作。
本题代码:
#include<cstdio>
using namespace std;
const int N=100005;
int n,m;
void swap(int&a,int&b) {
int t=a;a=b,b=t;
}
struct vsplay {
int rt,tot,chi[N][2],sz[N],fa[N],v[N],lz[N];
vsplay() {
rt=tot=0;
}
void mt(int x) {
sz[x]=sz[chi[x][0]]+sz[chi[x][1]]+1;
}
int get(int x) {
return x==chi[fa[x]][1];
}
void check(int x) {
if(lz[x]) {
if(chi[x][0]) lz[chi[x][0]]^=1;
if(chi[x][1]) lz[chi[x][1]]^=1;
swap(chi[x][0],chi[x][1]);
lz[x]=0;
}
}
void rotate(int x) {
int f=fa[x],g=fa[fa[x]],lx=get(x),lf=get(f);
check(x),check(f);
chi[f][lx]=chi[x][!lx];
if(chi[x][!lx]) fa[chi[x][!lx]]=f;
chi[x][!lx]=f;
fa[f]=x;
fa[x]=g;
if(g) chi[g][lf]=x;
mt(f);
mt(x);
}
void splay(int x,int to) {
fa[rt]=0;
for(int f=fa[x];f=fa[x],f!=to;rotate(x))
if(fa[f]!=to) rotate(get(x)==get(f)?f:x);
if(!to) rt=x;
}
int biuld(int l,int r,int f) {
if(l>r) return 0;
int mid=l+r>>1;
int x=++tot;
v[x]=mid;
fa[x]=f;
sz[x]=1;
chi[x][0]=biuld(l,mid-1,x);
chi[x][1]=biuld(mid+1,r,x);
mt(x);
return x;
}
int kth(int k) {
int y=rt;
while(1) {
check(y);
if(chi[y][0]&&k<=sz[chi[y][0]]) y=chi[y][0];
else {
k=k-sz[chi[y][0]]-1;
if(k<=0) return y;
y=chi[y][1];
}
}
}
void solve(int x,int y) {
//因为都要+1
int l=kth(x),r=kth(y+2);
splay(l,0);
splay(r,rt);
int gt=chi[chi[rt][1]][0];
if(gt) lz[gt]^=1;
}
void ip(int x) {
check(x);
if(chi[x][0]) ip(chi[x][0]);
if(1<=v[x]&&v[x]<=n) printf("%d ",v[x]);
if(chi[x][1]) ip(chi[x][1]);
}
void print() {
ip(rt);
printf("\n");
}
}t;
int main() {
scanf("%d%d",&n,&m);
t.rt=t.biuld(0,n+1,0);
while(m--) {
int l,r;
scanf("%d%d",&l,&r);
t.solve(l,r);
}
t.print();
}