平衡树
算法简介
平衡树,一种数据结构。是一种特殊的二叉搜索树,能够支持许多操作。
算法思想
典中典:
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入一个数
。 - 删除一个数
(若有多个相同的数,应只删除一个)。 - 定义排名为比当前数小的数的个数
。查询 的排名。 - 查询数据结构中排名为
的数。 - 求
的前驱(前驱定义为小于 ,且最大的数)。 - 求
的后继(后继定义为大于 ,且最小的数)。
二叉搜索树
在学习平衡树之前,我们要搞定二叉搜索树。
二叉搜索树(简称
性质:假定每个节点都有一个
值,则对于任何一个节点 ,其左子树任意节点的 值小于 的 值;其右子树任意节点 的 值大于 的 值。
根据
准备:节点
操作一:插入
运行分两种情况
如果当前
操作二:删除
如果删除节点副本数大于
对于操作一二,如果要在以
操作三:根据 找
定义函数
直接返回
返回
返回
返回
操作四:根据 找
跟上述基本思想一致
// a[p].l,a[p].r,a[p].size,a[p].cnt 分别是左右子节点,子树大小,副本数
int get_val(int p,int rank){
if(!p)return inf;
else if(rank<=a[a[p].l].size)return get_val(a[p].l,rank);
else if(rank<=a[a[p].l].size+a[p].cnt)return a[p].val;
else return get_val(a[p].r,rank-a[p].cnt-a[a[p].l].size);
}
操作五:找前倾
初始化答案
更新答案
遍历
核心思想:我们在遍历过程中不断接近
操作六:找后继
同操作五。
现在我们实现了上述操作,因为我们每当决定遍历某个节点的左/右子树时,我们都会舍弃另一个子树。相当于减小一半范围。因此每个操作时间复杂度在理想情况下(数据随机)为
但现在远远没有结束,因为现实中总会有出题人用构造的数据卡掉我们的程序
平衡树定义:
我们会发现,
平衡树的特点在于,虽然其仍然是一棵
传统平衡树-带旋
带旋
A 操作为右旋,B 操作反之。
可以看到左右旋改变了树的结构,但是没有改变
那我们应该在什么时候进行左右旋,众所周知,在随机数据中,朴素
右旋的意义是将一个节点的左节点旋转为父节点,左旋的意义是将一个节点的有节点旋转为父节点。这是有旋
对于普通
在代码中,会存在大量的传址调用。一定要分辨清楚。
点击查看代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
const int N=1e5+10,inf=1e9+10;;
int tot,root,n,op,x;
struct node{
int l,r,cnt,size,dat,val;
}a[N];
int New(int val){
a[++tot].val=val;
a[tot].dat=rand();
a[tot].size=a[tot].cnt=1;
return tot;
}
void update(int p){
a[p].size=a[a[p].l].size+a[a[p].r].size+a[p].cnt;
}
void build(){
root=New(-inf),a[root].r=New(inf);
update(root);
}
void zig(int &p){//右旋
int q=a[p].l;
a[p].l=a[q].r,a[q].r=p;
p=q,update(p),update(a[p].r);
}
void zag(int &p){//左旋
int q=a[p].r;
a[p].r=a[q].l,a[q].l=p;
p=q;update(p),update(a[p].l);
}
void insert(int &p,int val){// 插入
if(!p){
p=New(val);
return;
}else if(a[p].val==val){
a[p].cnt++;
update(p);
return;
}
else if(a[p].val>val){
insert(a[p].l,val);
if(a[p].dat<a[a[p].l].dat)zig(p);
}else if(a[p].val<val){
insert(a[p].r,val);
if(a[p].dat<a[a[p].r].dat)zag(p);
}
update(p);
}
void Delete(int &p,int val){// 删除
if(p==0)return;
if(a[p].val==val){
if(a[p].cnt>1){
a[p].cnt--,update(p);
return;
}
if(a[p].l||a[p].r){
if(a[p].r==0||a[a[p].l].dat>a[a[p].r].dat)
zig(p),Delete(a[p].r,val);
else
zag(p),Delete(a[p].l,val);
update(p);
}else p=0;
return;
}
a[p].val>val?Delete(a[p].l,val):Delete(a[p].r,val);
update(p);
}
int get_rank(int p,int val,int f){//查排名
if(!p)return 1;
else if(a[p].val==val)return a[a[p].l].size+1;
else if(val<a[p].val)return get_rank(a[p].l,val,1);
else return get_rank(a[p].r,val,2)+a[a[p].l].size+a[p].cnt;
}
int get_val(int p,int rank){// 查数
if(!p)return inf;
else if(rank<=a[a[p].l].size)return get_val(a[p].l,rank);
else if(rank<=a[a[p].l].size+a[p].cnt)return a[p].val;
else return get_val(a[p].r,rank-a[p].cnt-a[a[p].l].size);
}
int get_pre(int val){//前倾
int ans=-inf,p=root;
while(p){
if(a[p].val<val)ans=a[p].val,p=a[p].r;
else p=a[p].l;
}
return ans;
}
int get_next(int val){//后继
int ans=inf,p=root;
while(p){
if(a[p].val>val)ans=a[p].val,p=a[p].l;
else p=a[p].r;
}
return ans;
}
int main(){
build();
scanf("%d",&n);
while(n--){
scanf("%d %d",&op,&x);
if(op==1)insert(root,x);
if(op==2)Delete(root,x);
if(op==3)printf("%d\n",get_rank(root,x,0)-1);
if(op==4)printf("%d\n",get_val(root,x+1));
if(op==5)printf("%d\n",get_pre(x));
if(op==6)printf("%d\n",get_next(x));
}
return 0;
}
非旋
非旋
为什么说,
是神?
这个问题很简单,我们将可以通过平衡树板子的所有算法一一对比即可。
首先是犯下傲慢之罪的
数据水就认为自己可以代替神,取代平衡树。相比之下,神无疑是谦虚的。错误的时间复杂度
使得它无法通过加强版。这无疑是神对它的惩罚
其次是犯下愤怒之罪的权值线段树。
仗着神的常数大,没他跑的快就大放厥词“线段树的常数是优于平衡树的常数的”(zcysky 的博客)。和他相比,神更加宽和。因为算法必须离线,导致其也无法通过加强版。
然后是犯下懒惰之罪的分块
认为自己相比于神能够处理更多的问题,便在大部分的平衡树题解中出现,让人们不思进取,只知道分块。故被神降下神罚,使得
的时间复杂度无法通过加强版 的数据。
紧接着是犯下贪婪之罪的
因为自己自己能通过加强版的数据,就贪婪的希望取代神。无法进行更多复杂的操作使得其成为冷门算法,这甚至不是神的惩罚!神对于区间操作的掌握也一直了然于心,这样的能力岂是凡人所能比拟的。
紧接着是犯下暴食之罪的带旋
虽然贵为神父,但以更小的常数为借口放纵自己。代码长度的问题一直没有解决。因而被神降下惩罚。使其
多行的代码不被蒟蒻所接受,且无法可持久化,好在神念及旧情,再次允许它活跃于神犇的讨论之间
最后是其他做法。例如
虽然能通过加强版,但是因为码长问题,而被蒟蒻摈弃。但是宽容的神仍然给予其新生,让他们在神犇的题解中出现
平衡树不能失去
整活完毕
算法讲解
前置:
更新函数和结构体:
struct node{
int l,r,size,dat,val;
}a[N];
void update(int p){
a[p].size=a[a[p].l].size+a[a[p].r].size+1;
}
(按 分裂)
定义函数
为空。
是个空树,则
根据
反之,调用
我们在分裂完成后,还要更新节点的子树大小。
void split(int p,int v,int &x,int &y){
if(!p)return x=y=0,void();//一定要return,不然会update(p)
if(a[p].val<=v)split(a[p].r,v,a[x=p].r,y);//x的根是p
else split(a[p].l,v,x,a[y=p].l);//y的根是p
update(p);
}
(按 分裂)
定义函数
void split(int p,int k,int &x,int &y){
if(!p)return x=y=0,void();//一定要return,不然会update(p)
if(a[a[p].l].size+1<=k)split(a[p].r,k,a[x=p].r,y);//x的根是p
else split(a[p].l,k,x,a[y=p].l);//y的根是p
update(p);
}
打乱
还是分情况讨论
为空或 为空。
我们只需返回非空的哪一个,如果两个都为空,返回 。
C++:return x+y
- 其他情况
此时,因为保证 或 。所以只会有 或 两种情况。这是我们就要比较二者的随机权值,如果 ,那么 就是 的父亲,反之则是其儿子。
int merge(int x,int y){
if(!x||!y)return x+y;
if(a[x].dat>a[y].dat)return a[x].r=merge(a[x].r,y),update(x),x;
else return a[y].l=merge(x,a[y].l),update(y),y;
}
注意,因为合并后的新根可能不是原来的节点,所以一定要赋值。
新建,插入和删除
int New(int val){//新建节点
a[++tot].val=val;
a[tot].dat=rand();
a[tot].size=1;
return tot;
}
void Insert(int val){//插入
int x=0,y=0;
split(root,val-1,x,y),root=merge(merge(x,New(val)),y);
//先将根节点分裂,然后将新节点插入其中合并
}
void Delete(int val){//删除
int x=0,y=0,z=0;
split(root,val,x,z),split(x,val-1,x,y);
root=merge(merge(x,y=merge(a[y].l,a[y].r)),z);
//这个程序没写副本数,用了一种更巧妙的方法
//先二次分裂出平衡树y,此时y中的所有节点的val 都是给定val
//我们扔掉y的根节点,并用它左右儿子合并产生的新节点代替
}
其他功能
此时的平衡树已经具有平衡性,直接按照原有的 BST 函数就好了。但我们可以用
根据
将平衡树按照
根据
如果分裂函数写的是按值分裂,建议写普通
前倾/后继
重点将前倾。先按
点击查看代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
const int N=1e5+10,inf=1e9+10;
int tot,root,n,op,x;
struct node{
int l,r,size,dat,val;
}a[N];
int New(int val){
a[++tot].val=val;
a[tot].dat=rand();
a[tot].size=1;
return tot;
}
void update(int p){
a[p].size=a[a[p].l].size+a[a[p].r].size+1;
}
void split(int p,int v,int &x,int &y){
if(!p)return x=y=0,void();
if(a[p].val<=v)split(a[p].r,v,a[x=p].r,y);
else split(a[p].l,v,x,a[y=p].l);
update(p);
}
int merge(int x,int y){
if(!x||!y)return x+y;
if(a[x].dat<a[y].dat)return a[y].l=merge(x,a[y].l),update(y),y;
return a[x].r=merge(a[x].r,y),update(x),x;
}
void Insert(int val){
int x=0,y=0;
split(root,val-1,x,y),root=merge(merge(x,New(val)),y);
}
void Delete(int val){
int x=0,y=0,z=0;
split(root,val,x,z),split(x,val-1,x,y);
root=merge(merge(x,y=merge(a[y].l,a[y].r)),z);
}
int get_rank(int val){
int x=0,y=0,ans;
split(root,val-1,x,y);
ans=a[x].size+1;
return root=merge(x,y),ans;
}
int get_val(int k){
int p=root;
while(1){
if(k<=a[a[p].l].size)p=a[p].l;
else if(k==a[a[p].l].size+1)return a[p].val;
else k-=a[a[p].l].size+1,p=a[p].r;
}
}
int get_pre(int val){
int ans=0,p=root;
while(p){
if(a[p].val>=val)p=a[p].l;
else ans=a[p].val,p=a[p].r;
}
return ans;
}
int get_next(int val){
int ans=0,p=root;
while(p){
if(a[p].val>val)ans=a[p].val,p=a[p].l;
else p=a[p].r;
}
return ans;
}
int main(){
scanf("%d",&n);
while(n--){
scanf("%d %d",&op,&x);
if(op==1)Insert(x);
if(op==2)Delete(x);
if(op==3)cout<<get_rank(x)<<endl;
if(op==4)cout<<get_val(x)<<endl;
if(op==5)cout<<get_pre(x)<<endl;
if(op==6)cout<<get_next(x)<<endl;
}
return 0;
}
例题
例 :【模板】文艺平衡树
众所周知,如果一棵平衡树代表了
于是我们就可以用
但如果我们暴力旋转,时间复杂度会退化到
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N=1e5+10;
int tot,root,n,m,l,r;
struct node{
int l,r,val,dat,size,tag=0;
}a[N];
int New(int val){
a[++tot].val=val;
a[tot].dat=rand();
a[tot].size=1;
return tot;
}
void update(int p){
a[p].size=a[a[p].l].size+a[a[p].r].size+1;
}
void push_down(int p){
if(a[p].tag)a[a[p].l].tag^=1,a[a[p].r].tag^=1,swap(a[p].l,a[p].r),a[p].tag=0;
}
void split(int p,int k,int &x,int &y){
if(!p)return x=y=0,void();
push_down(p);
if(a[a[p].l].size+1<=k)split(a[p].r,k-a[a[p].l].size-1,a[x=p].r,y);
else split(a[p].l,k,x,a[y=p].l);
update(p);
}
int merge(int x,int y){
if(!x||!y)return x+y;
push_down(x),push_down(y);
if(a[x].dat>a[y].dat)return a[x].r=merge(a[x].r,y),update(x),x;
else return a[y].l=merge(x,a[y].l),update(y),y;
}
void fan(int l,int r){
int x=0,y=0,z=0;
split(root,l-1,x,z),split(z,r-l+1,y,z);
a[y].tag^=1,root=merge(x,merge(y,z));
}
void print(int p){
if(!p)return;
push_down(p);
print(a[p].l);
printf("%d ",a[p].val);
print(a[p].r);
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)root=merge(root,New(i));
while(m--){
scanf("%d %d",&l,&r);
fan(l,r);
}
print(root);
return 0;
}
不是
例 :[NOI2004] 郁闷的出纳员
这题有两个写法
-
对于修改工资的操作,因为次数太少,直接暴力修改
-
记录一个工资幅度变化值
。每次加减都直接修改 。查询就查询小于 。因为会有新插入的员工,我们假定员工的初始工资为 。再次之前它错过了工资增加 的机会,所以插入平衡树要插入 . -
应为可能要删除所有低于
的人,我们直接从根节点分裂出来,然后直接丢掉,在这里就可以感受到 暴力的优雅
例 :[TJOI2007] 书架
我们直接按排名分裂,然后与新节点合并。查询可以直接分裂两次得到第
点击查看代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
const int N=1.5e5;
int tot,root,n,q,m,x;
string str;
struct node{
int l,r,size,dat;
string val;
}a[N];
int New(string s){
a[++tot].val=s;
a[tot].size=1;
a[tot].dat=rand();
return tot;
}
void update(int p){
a[p].size=a[a[p].l].size+a[a[p].r].size+1;
}
void split(int p,int k,int &x,int &y){//T_x<=k,T_u>k;
if(!p)return x=y=0,void();
else if(a[a[p].l].size+1<=k)split(a[p].r,k-a[a[p].l].size-1,a[x=p].r,y);
else split(a[p].l,k,x,a[y=p].l);
update(p);
}
int merge(int x,int y){
if(!x||!y)return x+y;
if(a[x].dat>a[y].dat)return a[x].r=merge(a[x].r,y),update(x),x;
else return a[y].l=merge(x,a[y].l),update(y),y;
}
void insert(string s,int k){
int x=0,y=0;
split(root,k,x,y);
root=merge(merge(x,New(s)),y);
}
string kth_val(int k){
int x=0,y=0,z=0;string ans;
split(root,k,x,z),split(x,k-1,x,y);
ans=a[y].val;
root=merge(merge(x,y),z);
return ans;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
cin>>str;
insert(str,i-1);
}
scanf("%d",&m);
for(int i=1;i<=m;i++){
cin>>str>>x;
insert(str,x);
}
scanf("%d",&q);
for(int i=1;i<=q;i++){
scanf("%d",&x);
cout<<kth_val(x+1)<<'\n';
}
return 0;
}
例 : [NOI2003] 文本编辑器
定义一个指针
对于操作
对于操作
对于操作
这道题中的分裂均指按照排名分裂。
The End
平衡树给我学吐了,最近几个月不学数据结构了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话