替罪羊树学习笔记
替罪羊树学习笔记
史!
思想
众所周知,替罪羊树是一种平衡二叉搜索树,其拥有虽然我不理解为什么,但是很牛的复杂度。其思想在于通过一个系数进行定期重构,使得维护所有信息的树是一棵接近平衡树的伪平衡树,那么他依然拥有
但是,显然对于重构来说,同时需要一定的复杂度,那么我们应该按照什么样的标准评判当前子树是否需要重构呢?这个时候就要用到我们自主决定的系数
更多的,如果这棵树上有删除操作,我们采用惰性删除,即只对于当且节点的大小进行维护,即使这个节点已经被删除,我们也认为它仍然存在,只在查询时特判,不对答案产生贡献。那么显然,如果这样名存实亡的节点过多,会使得查询复杂度下降,因此当已经被删除的节点过多时,我们也认为当前子树是失衡的。
重构
根据刚才的思想,我们为重构提出了两个条件:子树大小相当和删除节点较少。那么我们可以维护下面两个信息:子树中的节点大小和子树中有效的节点大小。这样就可以判断了。另外,因为重构本质上只是将节点换了位置,因此编号等信息无需更改,因此,最多插入多少个节点,替罪羊树的空间复杂度就是多少。
当然在没有删除的时候,后面的条件是无用的。
代码
void Update(int id){
Sz(id)=Sz(L(id))+Sz(R(id))+1;
Sm(id)=Sm(L(id))+Sm(R(id))+C(id);
Rs(id)=Rs(L(id))+Rs(R(id))+(C(id)!=0);
return ;
}//更新信息
void Clear(int id){
if(id==0)return ;
Clear(L(id));
if(C(id)!=0)b[++top]=id;
Clear(R(id));
return ;
}//统计需要重构的编号
int Build(int l,int r){
if(l>r)return 0;
int mid=(l+r)>>1;
int id=b[mid];
for(int i=l;i<=r;i++)t.Insert(id,V(b[i]));
L(id)=Build(l,mid-1);
R(id)=Build(mid+1,r);
Update(id);
return id;
}//这样子重构出来的一定是平衡到不能再平衡的平衡树
bool IsReb(int id){
return Sz(id)*A<=(double)max(Sz(L(id)),Sz(R(id)))||Sz(id)*A>=(double)Rs(id);
}//两个条件满足其一即可重构
void Rebuild(int &id){
top=0;
Clear(id);
id=Build(1,top);
return ;
}//当满足重构条件时才调用这个函数
修改
对于修改,一般分为插入和删除,同时还分为按照权值排序和按照位置排序两种。
权值相关
为了降低相关复杂度,我们选择将所有权值相同的点归为一个点(当然也可以不这么做),让所有节点关于权值是一棵二叉搜索树即可。没有对应的当前节点时,我们直接维护新节点;当前节点的权值等于
代码
void Insert(int &x,int p){
if(x==0){//新建节点
x=++tot;
if(rt==0)rt=1;
V(x)=p;//维护的对应点的权值
L(x)=R(x)=0;//维护的左右子树编号
C(x)=Sz(x)=Sm(x)=Rs(x)=1;//分别是当前节点有多少个数、子树内有多少个节点、子树内有多少个数,子树内有多少个有效节点
return ;
}
if(p==V(x))C(x)++;//只增加节点数的个数
else if(p<V(x))Insert(L(x),p);//递归左子树
else Insert(R(x),p);//递归右子树
Update(x);//更新贡献
if(IsReb(x))Rebuild(x);//判断是否重构
return ;
}
void Del(int &x,int p){
if(x==0)return ;//没有能删除的
if(p==V(x)){
if(C(x)!=0)C(x)--;//只减少节点中数的个数
}else if(p<V(x))Del(L(x),p);//递归左子树
else Del(R(x),p);//递归右子树
Update(x);//更新贡献
if(IsReb(x))Rebuild(x);//判断是否重构
return ;
}
位置相关
考虑如果每个数在不同的位置上的贡献互不一致,那么就只能按照位置建立二叉搜索树。一般以前面有多少个数为媒介。如果当前节点的前面的数的个数不少于
代码
void Insert(int &id,int p,int k){
if(id==0){//新建节点
id=++tot;
if(rt==0)rt=id;
V(id)=p;
C(id)=Sz(id)=Rs(id)=1;//因为一个节点要么有,要么没有,不会有多个数同时在里面,因此不用维护子树内数的个数
L(id)=R(id)=0;
return ;
}
if(Rs(L(id))>=k)Insert(L(id),p,k);//注意空节点没有贡献
else Insert(R(id),p,k-Rs(L(id))-C(id));
Update(id);
if(IsReb(id))Rebuild(id);
return ;
}
void Delete(int &id,int k){
if(Rs(L(id))==k&&C(id)!=0)C(id)--;//注意空节点不会作为被删除的节点
else if(Rs(L(id))>k)Delete(L(id),k);
else Delete(R(id),k-Rs(L(id))-C(id));
Update(id);
if(IsReb(id))Rebuild(id);
return ;
}
查询
查询主要有查询第
但是查询没有办法讲解,按照线段树的思路:特判能否直接通过子树维护的信息概括
提醒
首先对于系数的选择,需要先进行粗略的评估,即重构的复杂度和不重构的复杂度那一个占比更大从而选择更合适的系数。一般来说,我们都不希望重构进行太多次,因此我们都会选择能够让重构更少的系数。根据我个人的经验,一般情况下,有删除时
接着对于重构,有多种写法进行优化,但是一般针对于无删除。因为在有删除时重构下面的子树可能导致上面的子树从不平衡变得平衡,因此只针对无删除进行优化。更一般的,可以理解为,既然下面的节点不论如何重构都不影响上面的重构结果,那么不妨只重构最上面的位置,这样一定可以优化。但因为重构需要更新左右子树信息,因此同时需要知道重构的节点是隶属上一层的左子树还是右子树。 下面给出代码:
void Insert(int &id,int p,int k){
if(id==0){
id=++tot;
if(rt==0)rt=id;
V(id)=p;
C(id)=Sz(id)=Rs(id)=1;
L(id)=R(id)=0;
return ;
}
if(Rs(L(id))>=k)Insert(L(id),p,k);
else Insert(R(id),p,k-Rs(L(id))-C(id));
Update(id);
if(IsReb(id)){
S=id;
F=0;
}else if(F!=0)F=id;
return ;
}
void Change(int p,int k){
Insert(rt,p,k);
if(s!=0){
if(f==0)Rebuild(rt);
else{
if(L(f)==s)Rebuild(L(f));
else Rebuild(R(f));
f=0;
}
s=0;
}
return ;
}
还有,替罪羊不仅用于平衡树,在一些需要重构树形数据结构的情况下,我们也可以利用这样的思想,也就是当其失衡导致求解层数过大时进行重构。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· 一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略