替罪羊树
替罪羊树
平衡树的一种。其特点是采用 “摧毁—重建” 的方法维护 BST 的平衡。
具体而言,当我们发现树上有一棵子树不平衡了,就摧毁它,然后重新建一棵平衡的子树。
不平衡率
我们定义不平衡率 \(\alpha\in[0.5,1]\) 为:一棵以 \(u\) 为根的子树,若其左子树或右子树占比大于 \(\alpha\),就认为不平衡。
显然,当 \(\alpha=0.5\) 时,BST 是绝对平衡的;当 \(\alpha=1\) 时,BST 表现为一条链。
替罪羊树复杂度的关键就在于 \(\alpha\) 的设定。根据某 OIer_Automation 的经验,有修改时,\(\alpha=0.75\) 最优;无修改时,\(\alpha=0.85\) 最优。但是特殊地,在替罪羊树套某些其他数据结构时,应当尽量减少重构的次数,即 \(\alpha\) 应尽量取大。
基本操作
重构
判断是否需要重构,只需判断最大的一个儿子的大小是否大于当前子树的大小 \(\times\alpha\) 即可。
bool nbl(int u){
return max(t[lu].sm,t[ru].sm)>=alpha*t[u].sm;
}
然后重构的时候,先把当前子树拍平,然后按照拍平后的数组建树即可。
void flatten(int u){
if(!u) return;
flatten(lu);
if(t[u].cnt) ord[++cnt]=u;
flatten(ru);
}
void build(int l,int r,int&u){
if(l>r) return u=0,void();
int mid=(l+r)>>1;
u=ord[mid];
build(l,mid-1,lu),build(mid+1,r,ru);
update(u);
}
void redo(int&u){
cnt=0;
flatten(u);
build(1,cnt,u);
}
重构时的代码就是
if(nbl(u)) redo(u);
插入
替罪羊树上的一个节点保存一个当前点的数的数量。插入时从根往下走,如果没有新建节点就新建一个,否则按照大小关系递归即可。
void ins(int&u,int x){
if(!u) return t[u=++tot]={0,0,x,1,1,1,1},void();
if(x==t[u].val) t[u].cnt++;
else if(x<t[u].val) ins(lu,x);
else ins(ru,x);
update(u);
if(nbl(u)) redo(u);
}
删除
也是同理的。
void del(int&u,int x){
if(t[u].val==x) t[u].cnt--;
else if(x<t[u].val) del(lu,x);
else del(ru,x);
update(u);
if(nbl(u)) redo(u);
}
其余操作和一般的平衡树写法无异。
模板
constexpr int MAXN=1e5+5;
constexpr double alpha=0.75;
int rt;
struct{
#define lu t[u].ls
#define ru t[u].rs
struct ScapeGoatTree{
int ls,rs,val,cnt,s,sd,siz;
}t[MAXN];
int ord[MAXN],cnt;
int tot;
void flatten(int u){
if(!u) return;
flatten(lu);
if(t[u].cnt) ord[++cnt]=u;
flatten(ru);
}
void update(int u){
t[u].s=t[lu].s+t[ru].s+1;
t[u].sd=t[lu].sd+t[ru].sd+(t[u].cnt!=0);
t[u].siz=t[lu].siz+t[ru].siz+t[u].cnt;
}
void build(int l,int r,int&u){
if(l>r) return u=0,void();
int mid=(l+r)>>1;
u=ord[mid];
build(l,mid-1,lu),build(mid+1,r,ru);
update(u);
}
void redo(int&u){
cnt=0;
flatten(u);
build(1,cnt,u);
}
bool nbl(int u){
return max(t[lu].s,t[ru].s)>=alpha*t[u].s||alpha*t[u].s>=t[u].sd;
}
void ins(int&u,int x){
if(!u) return t[u=++tot]={0,0,x,1,1,1,1},void();
if(x==t[u].val) t[u].cnt++;
else if(x<t[u].val) ins(lu,x);
else ins(ru,x);
update(u);
if(nbl(u)) redo(u);
}
void del(int&u,int x){
if(t[u].val==x) t[u].cnt--;
else if(x<t[u].val) del(lu,x);
else del(ru,x);
update(u);
if(nbl(u)) redo(u);
}
int rnk(int u,int x){
if(!u) return 0;
if(t[u].val==x&&t[u].cnt) return t[lu].siz;
else if(t[u].val<x) return t[lu].siz+t[u].cnt+rnk(ru,x);
else return rnk(lu,x);
}
int kth(int u,int k){
if(!u) return 0;
if(t[lu].siz<k&&k<=t[lu].siz+t[u].cnt) return t[u].val;
else if(k<=t[lu].siz) return kth(lu,k);
else return kth(ru,k-t[lu].siz-t[u].cnt);
}
int pre(int u,int x){
return kth(u,rnk(u,x));
}
int nxt(int u,int x){
return kth(u,rnk(u,x+1)+1);
}
}T;
int main(){
int n=read();
while(n--){
int opt=read(),x=read();
switch(opt){
case 1: T.ins(rt,x);break;
case 2: T.del(rt,x);break;
case 3: write(T.rnk(rt,x)+1);break;
case 4: write(T.kth(rt,x));break;
case 5: write(T.pre(rt,x));break;
default:write(T.nxt(rt,x));break;
}
}
return fw,0;
}
典型应用
P3920 [WC2014] 紫荆花之恋
点分树套替罪羊树,又称动态点分树。
点分树可以很方便地求解带修处理树上路径问题,但是在这道题中这棵树的形态还在发生变化,为了保证树高为 \(\log n\),我们需要不断地重构点分树,而这样做显然不优。
于是我们采取替罪羊树的思想:对于一棵子树,如果它超出了我们设定的不平衡率,我们就重构它。这样做就可以在一个 \(O(n\log^2n)\) 的时间内实现点分树的动态插入了。
然后对应到本道题,既然支持了动态插入,考虑去做 \(\text{dist}(i,j)\le r_i+r_j\) 的计数。那这个问题就很经典了,对于当前子树的根 \(p\),转化为 \(\text{dist}(i,p)+\text{dist}(p,j)\le r_i+r_j\),移项得 \(\text{dist}(i,p)-r_i\le r_j-\text{dist}(p,j)\),按照套路对每个节点维护 \(\rm SG\) 和 \(\rm CH\) 两棵平衡树,分别维护子树内所有点的 \(\text{dist}(i,p)-r_i\) 的值和 \(\text{dist}(i,\text{fa}(p))-r_i\) 的值,查询时直接查排名即可。

浙公网安备 33010602011771号