例题1:[bzoj3224]&[luogu3369]普通平衡树(平衡树模板题)
题意:维护一个集合,支持:1.加入x;2.删除x;3.查询x排名;4.查询排名x的数;5.查询x前驱;6.查询x后继
0.3查询(有多少个数小于x)+1,4查询存在x个数小于等于它的最小数,5查询排名为(x排名-1)的数,6查询排名为((x+1)排名)的数
1.可以开一棵权值线段树/trie树(动态开点)来维护,维护区间和即可,比较简单

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define mid (0LL+l+r>>1)
4 #define N 10000005
5 int V,n,r,p,x,f[N],ls[N],rs[N];
6 void update(int &k,int l,int r,int x,int y){
7 if (!k)k=++V;
8 if (l==r){
9 f[k]+=y;
10 return;
11 }
12 if (x<=mid)update(ls[k],l,mid,x,y);
13 else update(rs[k],mid+1,r,x,y);
14 f[k]=f[ls[k]]+f[rs[k]];
15 }
16 int query1(int k,int l,int r,int x,int y){
17 if ((l>y)||(x>r))return 0;
18 if ((x<=l)&&(r<=y))return f[k];
19 return query1(ls[k],l,mid,x,y)+query1(rs[k],mid+1,r,x,y);
20 }
21 int query2(int k,int l,int r,int x){
22 if (l==r)return l;
23 if (f[ls[k]]>=x)return query2(ls[k],l,mid,x);
24 return query2(rs[k],mid+1,r,x-f[ls[k]]);
25 }
26 int main(){
27 scanf("%d",&n);
28 while (n--){
29 scanf("%d%d",&p,&x);
30 if (p==1)update(r,-2e9,2e9,x,1);
31 if (p==2)update(r,-2e9,2e9,x,-1);
32 if (p==3)printf("%d\n",query1(r,-2e9,2e9,1,x-1)+1);
33 if (p==4)printf("%d\n",query2(r,-2e9,2e9,x));
34 if (p==5)printf("%d\n",query2(r,-2e9,2e9,query1(r,-2e9,2e9,1,x-1)));
35 if (p==6)printf("%d\n",query2(r,-2e9,2e9,query1(r,-2e9,2e9,1,x)+1));
36 }
37 }
BST:
1.BST(二叉查找树),它是二叉树且具有以下性质:1.左子树上所有结点小于根结点;2.右子树上所有结点大于根结点;3.左、右子树为BST
2.BST的节点:BST的每一个节点有节点大小、节点权值、子树大小、左右儿子5个属性需要维护
3.BST的删除:BST删除时需要将一个点不断旋转(见下)到其任意子树并递归下去,直到该节点只有0/1个儿子就用那个儿子替代他
4.发现这道题可以用BST来做这道题,但时间复杂度为o(树高),当插入数据单调而退化为链使得单次复杂度为o(n)
5.发现要维护这颗BST使其树高为o(log),即让其平衡,主要有旋转维护和非旋转维护两种方式
旋转:
1.旋转是维护BST平衡的重要操作,其主要作用是在保证BST性质的前提下将某一个儿子转到根节点
2.旋转示意图(又叫左旋和右旋):
3.说明:容易发现这样的旋转是符合BST性质的,同时也改变了树的形状(具体方法详见代码)

1 void rotate(int &k,int u,int p){//将k的p儿子u旋转到k的位置
2 s(p)=ch[u][p^1];//s(p)表示k的p儿子
3 ch[u][p^1]=k;//以上两步即上图所示的旋转
4 up(k);
5 up(k=u);//重新计算k和u的子树大小,并让k(根)变为u
6 }
treap:
0.treap的基本思路是用随机的方式来保证树高
1.插入完一个点后,有50%的概率旋转他的每一个祖先(在搜索回去的时候旋转)
2.删除点时,选择左右儿子也用50%的概率选择

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 100005
4 #define s(p) ch[k][p]
5 int V,r,n,p,x,sz[N],a[N],tot[N],ch[N][2];
6 void New(int k,int x){
7 a[k]=x;
8 s(0)=s(1)=0;
9 sz[k]=tot[k]=1;
10 }
11 void up(int k){
12 sz[k]=sz[s(0)]+sz[s(1)]+tot[k];
13 }
14 void rotate(int &k,int u,int p){
15 s(p)=ch[u][p^1];
16 ch[u][p^1]=k;
17 up(k);
18 up(k=u);
19 }
20 void add(int &k,int x){
21 if (!k){
22 New(k=++V,x);
23 return;
24 }
25 sz[k]++;
26 if (a[k]==x){
27 tot[k]++;
28 return;
29 }
30 bool p=(a[k]<x);
31 add(s(p),x);
32 }
33 void del(int &k,int x){
34 if (!k)return;
35 sz[k]--;
36 if (a[k]==x){
37 if (--tot[k])return;
38 tot[k]++;
39 if (s(0)*s(1)==0)k=s(0)+s(1);
40 else{
41 rotate(k,s(p),p);
42 del(k,x);
43 }
44 return;
45 }
46 del(s(a[k]<x),x);
47 }
48 int rank(int k,int x){
49 if (!k)return 1;
50 return rank(s(a[k]<x),x)+(a[k]<x)*(sz[s(0)]+tot[k]);
51 }
52 int kth(int k,int x){
53 if ((sz[s(0)]<x)&&(x<=sz[s(0)]+tot[k]))return a[k];
54 return kth(s(sz[s(0)]<x),x-(sz[s(0)]<x)*(sz[s(0)]+tot[k]));
55 }
56 int main(){
57 srand(time(0));
58 scanf("%d",&n);
59 while (n--){
60 scanf("%d%d",&p,&x);
61 if (p==1)add(r,x);
62 if (p==2)del(r,x);
63 if (p==3)printf("%d\n",rank(r,x));
64 if (p==4)printf("%d\n",kth(r,x));
65 if (p==5)printf("%d\n",kth(r,rank(r,x)-1));
66 if (p==6)printf("%d\n",kth(r,rank(r,x+1)));
67 }
68 }
3.然而这个东西随机次数较多,因此可以再维护一个堆性质的随机域,再用堆的方式up和down(详见代码)

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 100005
4 #define s(p) ch[k][p]
5 int V,r,n,p,x,sz[N],a[N],tot[N],ch[N][2],ra[N];
6 void New(int k,int x){
7 a[k]=x;
8 s(0)=s(1)=0;
9 sz[k]=tot[k]=1;
10 ra[k]=rand();
11 }
12 void up(int k){
13 sz[k]=sz[s(0)]+sz[s(1)]+tot[k];
14 }
15 void rotate(int &k,int u,int p){
16 s(p)=ch[u][p^1];
17 ch[u][p^1]=k;
18 up(k);
19 up(k=u);
20 }
21 void add(int &k,int x){
22 if (!k){
23 New(k=++V,x);
24 return;
25 }
26 sz[k]++;
27 if (a[k]==x){
28 tot[k]++;
29 return;
30 }
31 bool p=(a[k]<x);
32 add(s(p),x);
33 if (ra[s(p)]<ra[k])rotate(k,s(p),p);
34 }
35 void del(int &k,int x){
36 if (!k)return;
37 sz[k]--;
38 if (a[k]==x){
39 if (--tot[k])return;
40 tot[k]++;
41 if (s(0)*s(1)==0)k=s(0)+s(1);
42 else{
43 int p=(ra[s(0)]>ra[s(1)]);
44 rotate(k,s(p),p);
45 del(k,x);
46 }
47 return;
48 }
49 del(s(a[k]<x),x);
50 }
51 int rank(int k,int x){
52 if (!k)return 1;
53 return rank(s(a[k]<x),x)+(a[k]<x)*(sz[s(0)]+tot[k]);
54 }
55 int kth(int k,int x){
56 if ((sz[s(0)]<x)&&(x<=sz[s(0)]+tot[k]))return a[k];
57 return kth(s(sz[s(0)]<x),x-(sz[s(0)]<x)*(sz[s(0)]+tot[k]));
58 }
59 int main(){
60 srand(time(0));
61 scanf("%d",&n);
62 while (n--){
63 scanf("%d%d",&p,&x);
64 if (p==1)add(r,x);
65 if (p==2)del(r,x);
66 if (p==3)printf("%d\n",rank(r,x));
67 if (p==4)printf("%d\n",kth(r,x));
68 if (p==5)printf("%d\n",kth(r,rank(r,x)-1));
69 if (p==6)printf("%d\n",kth(r,rank(r,x+1)));
70 }
71 }
4.时间复杂度:以上两种方法复杂度都是$o(nlogn)$的(很显然,也不需要具体证明了要我也不会啊)
splay:
0.splay的基本性质是利用旋转的性质来保证时间复杂度(不保证树高)
1.旋转的性质:发现旋转的作用不仅仅是改变根,还可以用来改变树的结构
2.具体旋转方式:不断将被操作数/被询问数按以下方式旋转到根
(1)当这个节点已经是根节点的儿子,旋转到根后结束
(2)当这个节点自己和父亲是父亲和祖父的同一个儿子,先旋转父亲,再旋转自己(如下图)
(3)当这个节点自己和父亲是父亲和祖父的不同儿子,旋转两次自己(如下图)
(4)为了维护这个操作,还需要记录每一个节点的父亲(详见代码splay函数)

1 bool pd(int k){//判断k是哪一个儿子
2 return (ch[fa[k]][1]==k);
3 }
4 void up(int k){//计算子树大小
5 sz[k]=sz[s(0)]+sz[s(1)]+tot[k];
6 }
7 void rotate(int k){//旋转k
8 int x=fa[k],y=fa[x];
9 bool p=pd(k);
10 ch[y][pd(x)]=k;
11 fa[k]=y;
12 ch[x][p]=ch[k][p^1];
13 fa[ch[x][p]]=x;
14 ch[k][p^1]=x;
15 fa[x]=k;//旋转的过程(多修改一个f)
16 up(x);
17 up(k);//重新计算子树大小
18 }
19 void splay(int k,int x){//将k旋转到x的儿子
20 for(int i=fa[k];i!=x;rotate(k),i=fa[k])
21 if (fa[i]!=x)rotate(pd(k)==pd(i)?i:k);
22 if (!x)r=k;
23 }
3.splay的删除:splay删除如果像treap那样就无法保证时间复杂度了,所以要借助splay操作
将该点的前驱旋转到根,该点的后继旋转到前驱的右儿子,只要删除就在后继的左儿子即可(注意特判最大和最小)

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 100005
4 #define s(p) ch[k][p]
5 int V,r,n,p,x,sz[N],fa[N],a[N],tot[N],ch[N][2];
6 void New(int k,int x,int f){
7 a[k]=x;
8 s(0)=s(1)=0;
9 fa[k]=f;
10 }
11 bool pd(int k){
12 return (ch[fa[k]][1]==k);
13 }
14 void up(int k){
15 sz[k]=sz[s(0)]+sz[s(1)]+tot[k];
16 }
17 void rotate(int k){
18 int x=fa[k],y=fa[x];
19 bool p=pd(k);
20 ch[y][pd(x)]=k;
21 fa[k]=y;
22 ch[x][p]=ch[k][p^1];
23 fa[ch[x][p]]=x;
24 ch[k][p^1]=x;
25 fa[x]=k;
26 up(x);
27 up(k);
28 }
29 void splay(int k,int x){
30 for(int i=fa[k];i!=x;rotate(k),i=fa[k])
31 if (fa[i]!=x)rotate(pd(k)==pd(i)?i:k);
32 if (!x)r=k;
33 }
34 void add(int &k,int x,int f){
35 if (!k)New(k=++V,x,f);
36 sz[k]++;
37 if (a[k]==x){
38 tot[k]++;
39 splay(k,0);
40 return;
41 }
42 bool p=(a[k]<x);
43 add(s(p),x,k);
44 }
45 int rank(int k,int x,int f){
46 if (!k){
47 splay(f,0);
48 return 1;
49 }
50 return (a[k]<x)*(sz[s(0)]+tot[k])+rank(s(a[k]<x),x,k);
51 }
52 int kth(int k,int x){
53 if ((sz[s(0)]<x)&&(x<=sz[s(0)]+tot[k])){
54 splay(k,0);
55 return k;
56 }
57 return kth(s(sz[s(0)]<x),x-(sz[s(0)]<x)*(sz[s(0)]+tot[k]));
58 }
59 int find(int x,int p){
60 if (!p)x=rank(r,x,0)-1;
61 else x=rank(r,x+1,0);
62 return kth(r,x);
63 }
64 void del(int x){
65 if ((rank(r,x,0)==1)||(rank(r,x+1,0)>sz[r])){
66 splay(find(x-1,1),0);
67 if (--tot[r]){
68 sz[r]--;
69 return;
70 }
71 r=ch[r][(ch[r][1]>0)];
72 fa[r]=0;
73 return;
74 }
75 int k=find(x,1);
76 splay(find(x,0),0);
77 splay(k,r);
78 sz[r]--;
79 sz[k]--;
80 if (--tot[s(0)])sz[s(0)]--;
81 else fa[s(0)]=s(0)=0;
82 }
83 int main(){
84 scanf("%d",&n);
85 while (n--){
86 scanf("%d%d",&p,&x);
87 if (p==1)add(r,x,0);
88 if (p==2)del(x);
89 if (p==3)printf("%d\n",rank(r,x,0));
90 if (p==4)printf("%d\n",a[kth(r,x)]);
91 if (p==5)printf("%d\n",a[find(x,0)]);
92 if (p==6)printf("%d\n",a[find(x,1)]);
93 }
94 }
4.时间复杂度:当询问节点3很浅时,询问复杂度很低;当3比较深时,我们会将3及其子树内深度减小,所以最终复杂度为$o(nlogn)$
严谨的证明需要势能分析,详见:http://www.doc88.com/p-2763562792969.html
替罪羊树:
0.替罪羊树的基本思路是利用重构来保证树高
1.重构的操作:重构一棵子树,将这棵子树变成完全二叉树的BST(dfs一遍再建树)
2.重构的条件:当$max(sz[ls],sz[rs])>sz[k]*\alpha$(sz表示子树大小,ls和rs表示k的左右儿子)将k的子树重构为完全二叉树
3.替罪羊树的删除:如果发现出现次数-1后变为0,直接对那个点打上一个删除标记即
4.alpha的选择:$\alpha$的值域是[0.5,1),但经过理性分析,发现大约取[0,6,0.7](可以通过时间复杂度证明得出)

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 100005
4 #define s(p) ch[k][p]
5 int V,r,n,p,x,sz[N],a[N],tot[N],v[N],ch[N][2];
6 bool check(int k){
7 return max(sz[s(0)],sz[s(1)])*4>sz[k]*3;
8 }
9 void up(int k){
10 sz[k]=sz[s(0)]+sz[s(1)]+tot[k];
11 }
12 void rotate(int &k,int u,int p){
13 s(p)=ch[u][p^1];
14 ch[u][p^1]=k;
15 up(k);
16 up(k=u);
17 }
18 void add(int &k,int x){
19 if (!k)a[k=++V]=x;
20 sz[k]++;
21 if (a[k]==x){
22 tot[k]++;
23 return;
24 }
25 add(s(a[k]<x),x);
26 }
27 void del(int &k,int x){
28 if (!k)return;
29 sz[k]--;
30 if (a[k]==x){
31 tot[k]--;
32 return;
33 }
34 del(s(a[k]<x),x);
35 }
36 int rank(int k,int x){
37 if (!k)return 1;
38 return rank(s(a[k]<x),x)+(a[k]<x)*(sz[s(0)]+tot[k]);
39 }
40 int kth(int k,int x){
41 if ((sz[s(0)]<x)&&(x<=sz[s(0)]+tot[k]))return a[k];
42 return kth(s(sz[s(0)]<x),x-(sz[s(0)]<x)*(sz[s(0)]+tot[k]));
43 }
44 void dfs(int k){
45 if (!k)return;
46 dfs(s(0));
47 if (tot[k])v[++v[0]]=k;
48 dfs(s(1));
49 }
50 void build(int &k,int l,int r){
51 if (l>r){
52 k=0;
53 return;
54 }
55 int mid=(l+r>>1);
56 k=v[mid];
57 build(s(0),l,mid-1);
58 build(s(1),mid+1,r);
59 up(k);
60 }
61 void find(int &k,int x){
62 if (check(k)){
63 v[0]=0;
64 dfs(k);
65 build(k,1,v[0]);
66 return;
67 }
68 if (a[k]==x)return;
69 find(s(a[k]<x),x);
70 }
71 int main(){
72 scanf("%d",&n);
73 while (n--){
74 scanf("%d%d",&p,&x);
75 if (p==1)add(r,x);
76 if (p==2)del(r,x);
77 if (p==3)printf("%d\n",rank(r,x));
78 if (p==4)printf("%d\n",kth(r,x));
79 if (p==5)printf("%d\n",kth(r,rank(r,x)-1));
80 if (p==6)printf("%d\n",kth(r,rank(r,x+1)));
81 if (p<3)find(r,x);
82 }
83 }
5.时间复杂度:感觉网上没有很好的证明(大家都觉得显然啊),我就口胡一下吧
复杂度的证明同样需要用到势能分析(可以看https://wenku.baidu.com/view/2b93a9552af90242a995e513.html)
定义点k的势能$R(k)=|sz[ls]-rs[rs]|$,则$\phi=\sum R(k)/(2\alpha-1)$
有一个比较重要的东西:发现n个点完全二叉树(最平衡的)$\phi=0$
容易发现越平衡$\phi$值越小,因此$\phi(0)-\phi(n)\le 0$,接下来考虑$\sum ai$
询问复杂度:每一个节点都会让子树大小至少乘上$1/\alpha$,因此树高/复杂度为$o(log_{1/\alpha}n)$,无势能变化
插入/删除复杂度:同上,实际复杂度是$o(log_{1/\alpha}n)$,势能至多加上$o((log_{1/\alpha}n)/(2\alpha-1))$(全部都是重儿子)
重构复杂度:假设对一棵大小为m的子树修改,实际复杂度为o(m),重构结束后$\phi(i)=子树外$
最小化重构前的势能,即左右子树为完全二叉树,$\phi(i-1)=o(m)+子树外$,最终$ai=o(m)+\phi(i)-\phi(i-1)=o(1)$
同时,发现时间复杂度为$o(nlog_{1/\alpha}n)/(2\alpha-1)=o(nln(n)\cdot(-ln(\alpha)\cdot (2\alpha-1))$
前者只与n有关,考虑后者,即$f(\alpha)=-ln(\\alpha)\cdot (2\alpha-1)$,$f'(\alpha)=ln(\alpha)+2\alpha-1=0$
解得$\alpha=0.6$(左右),但实际一般取0.7也可以,还是要根据具体情况对拍调整一下
总结:
| 数据结构 | 实际时间 | 实际空间 | 实际代码 |
| 线段树 | 772ms | 118480kb | 1136B |
| treap | 632ms | 3636kb | 1531B |
| splay | 860ms | 3636kb | 1979B |
| 替罪羊树 | 512ms | 3636kb | 1615B |
当然,时间只能说明相对来说可能是这样的,毕竟有评测器、数据、人为等影响,只供参考
例题2:[bzoj3223]&[luogu3391]文艺平衡树(区间翻转模板题)
0.平衡树维护序列:类似于一种区间划分,不断选择一个点将区间分成两部分,同时保证了划分的次数不超过log(树高)
平衡树维护区间:可以用splay,类似于删除,将前驱提到根,后继放提到根右儿子,后继的左儿子的子树即该区间(特判首尾)
还可以用其他平衡树,但需要将区间划分为log段,因此区间要支持可合并,同线段树
1.翻转:显然暴力翻转复杂度太高(与暴力相同),可以用线段树的思想——打翻转的懒标记(注意下传)

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 100005
4 #define s(p) ch[k][p]
5 int V,r,n,m,x,y,a[N],tag[N],sz[N],fa[N],ch[N][2];
6 bool pd(int k){
7 return (ch[fa[k]][1]==k);
8 }
9 void New(int k,int x,int f){
10 a[k]=x;
11 tag[k]=s(0)=s(1)=0;
12 fa[k]=f;
13 sz[k]=1;
14 }
15 void rev(int k){
16 tag[k]^=1;
17 swap(s(0),s(1));
18 }
19 void up(int k){
20 sz[k]=sz[s(0)]+sz[s(1)]+1;
21 }
22 void down(int k){
23 if (tag[k]){
24 tag[k]=0;
25 rev(s(0));
26 rev(s(1));
27 }
28 }
29 void rotate(int k){
30 int x=fa[k],y=fa[x];
31 bool p=pd(k);
32 ch[y][pd(x)]=k;
33 fa[k]=y;
34 ch[x][p]=ch[k][p^1];
35 fa[ch[x][p]]=x;
36 ch[k][p^1]=x;
37 fa[x]=k;
38 up(x);
39 up(k);
40 }
41 void splay(int k,int x){
42 down(k);
43 for(int i=fa[k];i!=x;rotate(k),i=fa[k])
44 if (fa[i]!=x)rotate(pd(k)==pd(i)?i:k);
45 if (!x)r=k;
46 }
47 void add(int &k,int x,int f){
48 if (!k){
49 New(k=++V,x,f);
50 splay(k,0);
51 return;
52 }
53 add(s(a[k]<x),x,k);
54 }
55 int find(int k,int x){
56 if (sz[s(0)]+1==x)return k;
57 down(k);
58 return find(s(sz[s(0)]<x),x-(sz[s(0)]<x)*(sz[s(0)]+1));
59 }
60 void dfs(int k){
61 if (!k)return;
62 down(k);
63 dfs(s(0));
64 if ((1<k)&&(k<n+2))printf("%d ",k-1);
65 dfs(s(1));
66 }
67 int main(){
68 scanf("%d%d",&n,&m);
69 for(int i=1;i<=n+2;i++)add(r,i,0);
70 for(int i=1;i<=m;i++){
71 scanf("%d%d",&x,&y);
72 x=find(r,x);
73 splay(x,0);
74 y=find(r,y+2);
75 splay(y,r);
76 rev(ch[y][0]);
77 }
78 dfs(r);
79 }
例题3:[bzoj3065]带插入区间K小值(动态带插入区间第k小模板题,强制在线)
题意:询问给定序列动态(单点修改和单点插入)区间第k小
1.用平衡树,首先想到splay,但splay无法知道子树第k小(因为并不以权值为关键字)
2.发现只需要每一个节点开一棵权值线段树记录子树信息即可(即树套树),但平衡树就不能旋转了(改变量太大)
3.发现替罪羊树支持此操作,同时线段树是可合并的,所以划分成log个区间就可以了
4.替罪羊树重构时要使用线段树合并才可以,注意不能线段树合并共用节点且修改时不新建节点

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 70005
4 #define s(p) ch[k][p]
5 #define mid (l+r>>1)
6 int r,n,m,p,x,y,z,ans,ro[N],a[N],v[N],v2[N],st[N*300],sz[N*300],ch[N*300][2];
7 char s[11];
8 void update(int &k,int l,int r,int x,int y){
9 if (!k)k=st[st[0]--];
10 sz[k]+=y;
11 if (l==r)return;
12 if (x<=mid)update(s(0),l,mid,x,y);
13 else update(s(1),mid+1,r,x,y);
14 if (!sz[k]){
15 st[++st[0]]=k;
16 s(0)=s(1)=k=0;
17 }
18 }
19 int query(int l,int r,int x){
20 if (l==r)return l;
21 int s=0;
22 for(int i=1;i<=v[0];i++)s+=sz[ch[v[i]][0]];
23 for(int i=1;i<=v2[0];i++)s+=(((l<=v2[i])&&(v2[i]<=mid)));
24 for(int i=1;i<=v[0];i++)v[i]=ch[v[i]][(s<x)];
25 if (s>=x)query(l,mid,x);
26 else query(mid+1,r,x-s);
27 }
28 void merge(int &k,int k1,int k2){
29 if (k1+k2==0)return;
30 sz[k=st[st[0]--]]=sz[k1]+sz[k2];
31 merge(s(0),ch[k1][0],ch[k2][0]);
32 merge(s(1),ch[k1][1],ch[k2][1]);
33 }
34 bool check(int k){
35 return max(sz[s(0)],sz[s(1)])*4>sz[k]*3;
36 }
37 void up(int k){
38 sz[k]=sz[s(0)]+sz[s(1)]+1;
39 merge(ro[k],ro[s(0)],ro[s(1)]);
40 update(ro[k],0,N,a[k],1);
41 }
42 void add(int &k,int x,int y){
43 if (!k){
44 sz[k=++n]=1;
45 update(ro[k],0,N,a[k]=y,1);
46 return;
47 }
48 sz[k]++;
49 update(ro[k],0,N,y,1);
50 bool p=(sz[s(0)]<x);
51 add(s(p),x-p*(sz[s(0)]+1),y);
52 }
53 int update(int k,int x,int y){
54 update(ro[k],0,N,y,1);
55 if (x==sz[s(0)]){
56 swap(a[k],y);
57 update(ro[k],0,N,y,-1);
58 return y;
59 }
60 bool p=(sz[s(0)]<x);
61 y=update(s(p),x-p*(sz[s(0)]+1),y);
62 update(ro[k],0,N,y,-1);
63 return y;
64 }
65 void find(int k,int x,int y){
66 if ((x==1)&&(y==sz[k])){
67 v[++v[0]]=ro[k];
68 return;
69 }
70 if (x<=sz[s(0)])find(s(0),x,min(y,sz[s(0)]));
71 x-=sz[s(0)]+1;
72 y-=sz[s(0)]+1;
73 if (0<y)find(s(1),max(x,1),y);
74 if ((x<=0)&&(0<=y))v2[++v2[0]]=a[k];
75 }
76 void dfs(int k){
77 if (!k)return;
78 dfs(s(0));
79 v[++v[0]]=k;
80 dfs(s(1));
81 }
82 void build(int &k,int l,int r){
83 if (l>r){
84 k=0;
85 return;
86 }
87 k=v[mid];
88 build(s(0),l,mid-1);
89 build(s(1),mid+1,r);
90 up(k);
91 }
92 void find(int &k,int x){
93 if (check(k)){
94 v[0]=0;
95 dfs(k);
96 build(k,1,v[0]);
97 return;
98 }
99 if (x==sz[s(0)])return;
100 bool p=(sz[s(0)]<x);
101 find(s(p),x-p*(sz[s(0)]+1));
102 }
103 int main(){
104 scanf("%d",&n);
105 for(int i=N;i<=300*(N-5);i++)st[++st[0]]=i;
106 for(int i=1;i<=n;i++){
107 scanf("%d",&a[i]);
108 v[i]=i;
109 }
110 build(r,1,n);
111 scanf("%d",&m);
112 for(int i=1;i<=m;i++){
113 scanf("%s%d%d",s,&x,&y);
114 x^=ans;
115 y^=ans;
116 if (s[0]=='M')update(r,x-1,y);
117 if (s[0]=='I'){
118 add(r,x-1,y);
119 find(r,x-1);
120 }
121 if (s[0]=='Q'){
122 scanf("%d",&z);
123 v[0]=v2[0]=0;
124 find(r,x,y);
125 printf("%d\n",ans=query(0,N,z^ans));
126 }
127 }
128 }
5.类似的题目还有[bzoj3600],也是要利用替罪羊树不需要旋转做
例题4:[luogu3835]可持久化平衡树(可持久化treap模板题)
题意:同例1,但询问为历史询问
0.很显然,发现这就是要可持久化一棵平衡树
1.可持久化的条件:1.时间复杂度非均摊;2.不能用旋转维护(单次修改量由1来保证)
2.发现上面三种平衡树并不能可持久化,于是fhq提出了一种基于treap思想的非旋treap
3.分裂——split(k,x,k1,k2):将k的小于x的数和剩下的分裂出来并存在k1和k2中,分为三种情况递归:
(1)子树为空,返回空树标记;
(2)全部在左区间,递归左区间确定根;
(3)包含了根/不全在左子树中,递归右区间确定根的右儿子。
4.合并——merge(k1,k2):将k1和k2合并并返回新的根(k1最大值小于k2最小值),分两种情况递归:
(1)k1的随机数<k2的随机数,将k2和k1的右子树合并并作为k1的右儿子;
(2)k1的随机数>k2的随机数,将k1和k2的左子树和并并作为k2的左儿子。
5.插入/删除:插入x,根据x分为两部分,并将x当作一棵树与另外两颗树合并即可;删除类似

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 100005
4 #define s(p) ch[k][p]
5 int V,r,n,p,x,sz[N],a[N],ch[N][2],ra[N];
6 int New(int x){
7 a[++V]=x;
8 sz[V]=1;
9 ra[V]=rand();
10 return V;
11 }
12 void up(int k){
13 sz[k]=sz[s(0)]+sz[s(1)]+1;
14 }
15 void split(int k,int x,int &k1,int &k2){
16 if (!k){
17 k1=k2=0;
18 return;
19 }
20 if (x<a[k]){
21 k2=k;
22 split(s(0),x,k1,s(0));
23 }
24 else{
25 k1=k;
26 split(s(1),x,s(1),k2);
27 }
28 up(k);
29 }
30 int merge(int k1,int k2){
31 if (k1*k2==0)return k1+k2;
32 if (a[k1]>a[k2])swap(k1,k2);
33 int p=(ra[k1]<ra[k2]),k=k1*p+k2*(p^1);
34 s(p)=merge(s(p),k1+k2-k);
35 up(k);
36 return k;
37 }
38 void add(int x){
39 int a,b;
40 split(r,x,a,b);
41 r=merge(merge(a,New(x)),b);
42 }
43 void del(int x){
44 int a,b,c;
45 split(r,x-1,a,c);
46 split(c,x,b,c);
47 r=merge(merge(a,merge(ch[b][0],ch[b][1])),c);
48 }
49 int kth(int k,int x){
50 if (x==sz[s(0)]+1)return a[k];
51 bool p=(sz[s(0)]<x);
52 return kth(s(p),x-p*(sz[s(0)]+1));
53 }
54 int rank(int x){
55 int a,b,c;
56 split(r,x-1,a,b);
57 c=sz[a]+1;
58 r=merge(a,b);
59 return c;
60 }
61 int main(){
62 srand(time(0));
63 scanf("%d",&n);
64 while (n--){
65 scanf("%d%d",&p,&x);
66 if (p==1)add(x);
67 if (p==2)del(x);
68 if (p==3)printf("%d\n",rank(x));
69 if (p==4)printf("%d\n",kth(r,x));
70 if (p==5)printf("%d\n",kth(r,rank(x)-1));
71 if (p==6)printf("%d\n",kth(r,rank(x+1)));
72 }
73 }
6.区间操作:类似于splay,直接用分别split掉左端点前和右端点后,然后提取出对应区间即可

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 100005
4 #define s(p) ch[k][p]
5 int r,n,m,x,y,sz[N],ra[N],laz[N],ch[N][2];
6 void update(int k){
7 swap(s(0),s(1));
8 laz[k]^=1;
9 }
10 void down(int k){
11 if (laz[k]){
12 update(s(0));
13 update(s(1));
14 laz[k]=0;
15 }
16 }
17 void up(int k){
18 sz[k]=sz[s(0)]+sz[s(1)]+1;
19 }
20 void split(int k,int x,int &k1,int &k2){
21 if (!k){
22 k1=k2=0;
23 return;
24 }
25 down(k);
26 if (x<=sz[s(0)]){
27 k2=k;
28 split(s(0),x,k1,s(0));
29 }
30 else{
31 k1=k;
32 split(s(1),x-sz[s(0)]-1,s(1),k2);
33 }
34 up(k);
35 }
36 int merge(int k1,int k2){
37 if (k1*k2==0)return k1+k2;
38 int p=(ra[k1]<ra[k2]),k=k1*p+k2*(p^1);
39 down(k);
40 if (p)s(1)=merge(s(1),k2);
41 else s(0)=merge(k1,s(0));
42 up(k);
43 return k;
44 }
45 void dfs(int k){
46 if (!k)return;
47 down(k);
48 dfs(s(0));
49 printf("%d ",k);
50 dfs(s(1));
51 }
52 int main(){
53 srand(time(0));
54 scanf("%d%d",&n,&m);
55 for(int i=1;i<=n;i++){
56 ra[i]=rand();
57 sz[i]=1;
58 r=merge(r,i);
59 }
60 for(int i=1;i<=m;i++){
61 scanf("%d%d",&x,&y);
62 int a,b,c;
63 split(r,x-1,a,b);
64 split(b,y-x+1,b,c);
65 update(b);
66 r=merge(merge(a,b),c);
67 }
68 dfs(r);
69 }
7.可持久化:分裂与合并都在原来的基础上进行即可(详见代码)

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define N 500005
4 #define s(p) ch[k][p]
5 int V,n,t,p,x,r[N],a[N*50],ra[N*50],sz[N*50],ch[N*50][2];
6 int New(int x){
7 a[++V]=x;
8 sz[V]=1;
9 ra[V]=rand();
10 return V;
11 }
12 int copy(int k){
13 a[++V]=a[k];
14 sz[V]=sz[k];
15 ra[V]=ra[k];
16 memcpy(ch[V],ch[k],8);
17 return V;
18 }
19 void up(int k){
20 sz[k]=sz[s(0)]+sz[s(1)]+1;
21 }
22 void split(int k,int x,int &k1,int &k2){
23 if (!k){
24 k1=k2=0;
25 return;
26 }
27 if (x<a[k]){
28 k2=copy(k);
29 split(ch[k2][0],x,k1,ch[k2][0]);
30 up(k2);
31 }
32 else{
33 k1=copy(k);
34 split(ch[k1][1],x,ch[k1][1],k2);
35 up(k1);
36 }
37 }
38 int merge(int k1,int k2){
39 if (k1*k2==0)return k1+k2;
40 int k;
41 if (ra[k1]<ra[k2]){
42 k=copy(k1);
43 s(1)=merge(s(1),k2);
44 }
45 else{
46 k=copy(k2);
47 s(0)=merge(k1,s(0));
48 }
49 up(k);
50 return k;
51 }
52 void add(int &r,int x){
53 int a,b;
54 split(r,x,a,b);
55 r=merge(merge(a,New(x)),b);
56 }
57 void del(int &r,int x){
58 int a,b,c;
59 split(r,x-1,a,c);
60 split(c,x,b,c);
61 r=merge(merge(a,merge(ch[b][0],ch[b][1])),c);
62 }
63 int kth(int k,int x){
64 if (x==sz[s(0)]+1)return a[k];
65 bool p=(sz[s(0)]<x);
66 return kth(s(p),x-p*(sz[s(0)]+1));
67 }
68 int rank(int &r,int x){
69 int a,b,c;
70 split(r,x-1,a,b);
71 c=sz[a]+1;
72 r=merge(a,b);
73 return c;
74 }
75 int main(){
76 srand(time(0));
77 scanf("%d",&n);
78 for(int i=1;i<=n;i++){
79 scanf("%d%d%d",&t,&p,&x);
80 r[i]=r[t];
81 if (p==1)add(r[i],x);
82 if (p==2)del(r[i],x);
83 if (p==3)printf("%d\n",rank(r[i],x));
84 if (p==4)printf("%d\n",kth(r[i],x));
85 if (p==5)printf("%d\n",kth(r[i],rank(r[i],x)-1));
86 if (p==6)printf("%d\n",kth(r[i],rank(r[i],x+1)));
87 }
88 }
8.类似的还有[luogu5055],只需要再维护一个子树和即可,注意标记的处理
总结:
各种平衡树所能处理的问题——
1.treap:码量较低,只能处理基本的平衡树问题
2.splay:码量较大,主要用于:1.不支持合并的区间问题;2.降低算法理论复杂度
3.替罪羊树:码量中等,主要用于单点信息量较大的问题(包括树套树)
4.非旋treap:码量较低,基本支持一切平衡树的问题,同时还支持可持久化
