替罪羊树

替罪羊树

平衡树的一种。其特点是采用 “摧毁—重建” 的方法维护 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);
}

其余操作和一般的平衡树写法无异。

模板

P3369 【模板】普通平衡树

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\) 的值,查询时直接查排名即可。

posted @ 2025-06-28 09:00  Laoshan_PLUS  阅读(244)  评论(0)    收藏  举报