我的坟头应该开满玫瑰吗?|

YYYmoon

园龄:1年粉丝:20关注:41

替罪羊树

替罪羊树是一种平衡二叉搜索树。且它不依赖旋转操作,而只是基于一种暴力重构的操作来保证平衡。在暴力重构下它有 O(logn) 级别的树高,和奇妙的复杂度,一般操作都是 O(logn) 的。(证明好像要用势能分析)

关于它为什么要叫替罪羊树?我有一个猜测,就是随着插入,它的平衡会被破坏,这时就找一个破坏平衡的“替罪羊”,把它重构,就能平衡了。(以上纯口胡

重构

具体来说,定义一个平衡因子 alpha ,当某个节点 x 的某棵子树 x.ch.size>x.sizealpha 时便将这棵以 x 为根的子树拍扁重构。

另外,由于我们每次采用惰性删除(即不删除这个点,只给siz--),已删除节点过多也影响效率。因此若未被删除的子树大小占总大小的比例低于 alpha ,也重构。

重构的目的是让该子树变得平衡。考虑如何操作。

  1. 将该树拍扁。中序遍历展开,存入数组中。

  2. 重新建树。每次取区间中点为根,递归两边为左右子树建树。

重构的本质是交换节点位置,因此编号等信息无需更改。所以插入多少个节点,替罪羊树的空间复杂度就是多少。

时间复杂度分析:这样重构一次的复杂度为 O(n) ,而只有插入达到 sizealpha 的节点数目才会重构,可以通过势能分析证明替罪羊树单次操作均摊时间复杂度为 O(logn) .

经典操作

插入:同普通二叉搜索树,只是在递归返回的时候判断该子树是否需要重构

删除:并非真正删除,只是给点打一个标记,在重构时才删掉。

rank和kth操作:类似线段树,就是根据查询的k和当前节点的关系,分别往左或右子树递归。

平衡因子的取值

alpha 的范围在 0.5~1 左右,主要就是平衡树的深度和重构次数。

亲身尝试:我的代码 alpha 取 0.75~0.8 时比较快。

[模板] 普通平衡树
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
const double alpha=0.8;
int n,cnt,rt,tot,tmp[maxn];
struct node{
	int val,lc,rc;
	int c,s;//c本数据出现次数,s以本节点为根的子树大小(每个节点计num次
	int siz,sd;//siz以本节点为根的子树大小,sd已删除节点不计的子树大小(都是每个节点计1次 
}t[maxn];
void pushup(int x){
	t[x].siz=t[t[x].lc].siz+t[t[x].rc].siz+1;
	t[x].s=t[t[x].lc].s+t[t[x].rc].s+t[x].c;
	t[x].sd=t[t[x].lc].sd+t[t[x].rc].sd+(t[x].c!=0);
}
bool canrb(int x){
	return t[x].c&&(alpha*t[x].siz<=1.0*max(t[t[x].lc].siz,t[t[x].rc].siz)||t[x].sd<=alpha*t[x].siz);
}
void pia(int x){
	if(!x) return ;
	pia(t[x].lc);
	if(t[x].c) tmp[++tot]=x;
	pia(t[x].rc);
}
void build(int &x,int l,int r){
	if(l>r) return x=0,void();
	int mid=(l+r)>>1;
	x=tmp[mid];
	build(t[x].lc,l,mid-1);
	build(t[x].rc,mid+1,r);
	pushup(x);
}
void rbuild(int &x){
	tot=0;
	pia(x);
	build(x,1,tot);
}
void ins(int &x,int v){
	if(x==0){
		x=++cnt;
		t[x].val=v,t[x].lc=t[x].rc=0;
		t[x].c=t[x].s=t[x].siz=t[x].sd=1;
		return ;
	}
	if(v==t[x].val) t[x].c++;
	else if(v<t[x].val) ins(t[x].lc,v);
	else ins(t[x].rc,v);
	pushup(x);
	if(canrb(x)) rbuild(x);
}
void del(int &x,int v){
	if(x==0) return ;
	if(v==t[x].val) t[x].c--;
	else if(v<t[x].val) del(t[x].lc,v);
	else del(t[x].rc,v);
	pushup(x);
	if(canrb(x)) rbuild(x);
}
int kth(int x,int k){
	if(k<=t[t[x].lc].s) return kth(t[x].lc,k);
	else if(k>t[t[x].lc].s+t[x].c) return kth(t[x].rc,k-t[t[x].lc].s-t[x].c);
	return t[x].val;
}
int rnk(int x,int v){//这里直接查排名了,记得+1 
	if(x==0) return 1;
	if(t[x].val==v) return t[t[x].lc].s+1;
	else if(t[x].val>v) return rnk(t[x].lc,v);
	else return t[t[x].lc].s+t[x].c+rnk(t[x].rc,v);
}
int qpre(int v){
	return kth(rt,rnk(rt,v)-1);
}
int qnxt(int v){
	return kth(rt,rnk(rt,v+1));
}
int main(){
	scanf("%d",&n);
	while(n--){
		int opt,x;
		scanf("%d%d",&opt,&x);
		if(opt==1) ins(rt,x);
		else if(opt==2) del(rt,x);
		else if(opt==3) printf("%d\n",rnk(rt,x));
		else if(opt==4) printf("%d\n",kth(rt,x));
		else if(opt==5) printf("%d\n",qpre(x));
		else printf("%d\n",qnxt(x));
	}
	return 0;
}

例题

[湖北省队互测2014] 没有人的算术

这题最奇怪的地方是比较值的大小是递归定义的。考虑生成一个类似这个递归的结构并给数对赋上值从而方便比较。想到用二叉搜索树类似结构。

考虑需要维护的:有序的数对,且要随时插入保证平衡。普通的平衡树,要么是旋转,不能保证值的大小关系(splay);要么是节点父子关系随机,以及分裂合并次数过多(FHQ-treap),关键是难以直接给它赋一个有序的值。

因此想到重构式平衡树——替罪羊树。它能完全维护数列顺序。

单点修改区间查询用普通线段树就行,就是pushup比较的时候用替罪羊树维护过的赋值。

替罪羊式重构 K-D tree:[bzoj4066] 简单题

这东西就是 K-D tree。注意到空间限制+强制在线,我们没有什么投机取巧的方法。

它其实是为了说 K-D tree 的替罪羊式重构(其实用二进制分组写是一样的)。且替罪羊式重构可能导致查询时间复杂度退化,一般不是有删点的操作还是不建议使用。

//简单个* 调死我了。。。
#include<bits/stdc++.h>
using namespace std;
const double alpha=0.75;
const int maxn=2e5+5;
int n,ans,cnt,px,py,qx,qy,tot,rt,a[maxn];
struct kd_tree{
	int lc,rc,val,sum,siz,mx[2],mn[2],v[2];
}t[maxn];
void pushup(int x){
	t[x].sum=t[t[x].lc].sum+t[t[x].rc].sum+t[x].val;
	t[x].siz=t[t[x].lc].siz+t[t[x].rc].siz+1;
	for(int i=0;i<2;i++){
		t[x].mn[i]=t[x].mx[i]=t[x].v[i];
		if(t[x].lc){
			t[x].mn[i]=min(t[x].mn[i],t[t[x].lc].mn[i]);
			t[x].mx[i]=max(t[x].mx[i],t[t[x].lc].mx[i]);
		}
		if(t[x].rc){
			t[x].mn[i]=min(t[x].mn[i],t[t[x].rc].mn[i]);
			t[x].mx[i]=max(t[x].mx[i],t[t[x].rc].mx[i]);
		}
	} 
}
bool canrb(int x){
	return t[x].siz*alpha<=1.0*max(t[t[x].lc].siz,t[t[x].rc].siz);
}
int build(int l,int r,int d){
	if(l>r) return 0; 
	int mid=(l+r)>>1;
	nth_element(a+l,a+mid,a+r+1,[d](int x,int y){
		return t[x].v[d]<t[y].v[d];
	});
	int x=a[mid];
	t[x].lc=build(l,mid-1,d^1);
	t[x].rc=build(mid+1,r,d^1);
	pushup(x); 
	return x;
}
void pia(int x){
	if(!x) return ;
	a[++tot]=x;
	pia(t[x].lc);
	pia(t[x].rc);
}
void rebuild(int &x,int d){
	tot=0;
	pia(x);
	x=build(1,tot,d); 
}
void add(int &x,int d){
	if(!x){
		x=++cnt,t[x].lc=t[x].rc=0;
		t[x].val=t[x].sum=qx,t[x].siz=1;
		t[x].mn[0]=t[x].mx[0]=t[x].v[0]=px;
		t[x].mn[1]=t[x].mx[1]=t[x].v[1]=py; 
		return ;
	}
	if(t[x].v[0]==px&&t[x].v[1]==py){
		t[x].val+=qx,t[x].sum+=qx;
		return ;
	}
	int chk=(d==0)?px:py;
	if(chk<=t[x].v[d]) add(t[x].lc,d^1);
	else add(t[x].rc,d^1);
	pushup(x);
	if(canrb(x)) rebuild(x,d);
}
int query(int x){
	if(!x||t[x].mx[0]<px||t[x].mn[0]>qx||t[x].mx[1]<py||t[x].mn[1]>qy) return 0;
	if(t[x].mn[0]>=px&&t[x].mn[1]>=py&&t[x].mx[0]<=qx&&t[x].mx[1]<=qy) return t[x].sum;
	return ((t[x].v[0]>=px&&t[x].v[1]>=py&&t[x].v[0]<=qx&&t[x].v[1]<=qy)?t[x].val:0)+query(t[x].lc)+query(t[x].rc);
}
int main(){
	scanf("%d",&n);
	while(1){
		int opt;
		scanf("%d",&opt);
		if(opt==1){
			scanf("%d%d%d",&px,&py,&qx);
			px^=ans,py^=ans,qx^=ans;
			add(rt,0);
		} 
		else if(opt==2){
			scanf("%d%d%d%d",&px,&py,&qx,&qy);
			px^=ans,py^=ans,qx^=ans,qy^=ans;
			printf("%d\n",ans=query(rt));
		}
		else break;
	}
	return 0;
}

[bzoj3217] ALOEXT

思路并不算很难想,注意到替罪羊树的树高大约是logn的。

那么不妨用替罪羊树维护删除和添加操作,用01trie求解最大异或和问题。因此使用替罪羊树套01tire。(这里其他平衡树,一会分裂一会旋转的,都不能保证这个套01trie的正确性和复杂度。)

时空复杂度都是 O(nlognlogV) 的,数组要开够。

但这题就是代码死长,死难写。边界条件一堆细节。也就调了八九个小时吧……(破了我代码长度和调题时间的记录)

致敬传奇数据结构。。
//重构吧
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5,K=19,mod=1048576;
const double alpha=0.9;
int n,m,a[maxn],cnt,R,lst; 
struct trie{
	int son[maxn*150][2],siz[maxn*150],pol[maxn*150],top,tot,rt[maxn];
	int newnode(){
		return top?pol[top--]:++tot;
	}
	void del(int &x){
		if(!x) return ;
		pol[++top]=x;
		siz[x]=son[x][0]=son[x][1]=0;
		x=0;
	}
	void insert(int id,int v){
		if(!rt[id]) rt[id]=newnode();
		int p=rt[id];
		for(int i=K;i>=0;i--){
			int x=(v>>i)&1;
			siz[p]++;
			if(!son[p][x]) son[p][x]=newnode();
			p=son[p][x];
		}
		siz[p]++;
	}
	void Delt(int id,int v){
		int p=rt[id];
		for(int i=K;i>=0;i--){
			siz[p]--;
			int x=(v>>i)&1;
			int nxt=p;
			p=son[p][x];
			if(siz[son[nxt][x]]==1) son[nxt][x]=0;//注意这里的写法、、 
		}
		siz[p]--;
	}
	int query(int id,int v){
		int p=rt[id],ret=0;
//		printf("%d\n",siz[0]);
		for(int i=K;i>=0;i--){
			int x=(v>>i)&1;
			if(son[p][!x]){
				p=son[p][!x];
				ret|=(1<<i);
			} else p=son[p][x];
//			printf("%d %d %d\n",p,x,ret);
		}
		return ret;
	}
	void clear(int &x){
		if(!x) return ;
		clear(son[x][0]);
		clear(son[x][1]);
		del(x);
	}
}T;
struct goattree{
	int tot,tmp;
	struct node{
		int mx,cmx,v,c,siz,rs,lc,rc;
	}t[maxn];
	void pushup(int x){
		t[x].mx=max(t[t[x].lc].mx,t[t[x].rc].mx);
		if(t[t[x].lc].mx==t[x].mx) t[x].cmx=max(t[t[x].lc].cmx,t[t[x].rc].mx);
		else t[x].cmx=max(t[t[x].lc].mx,t[t[x].rc].cmx);
		if(t[x].c){
			if(t[x].v>t[x].mx) t[x].cmx=t[x].mx,t[x].mx=t[x].v;
			else if(t[x].v>t[x].cmx) t[x].cmx=t[x].v;
		}
		t[x].siz=t[t[x].lc].siz+t[t[x].rc].siz+1;
		t[x].rs=t[t[x].lc].rs+t[t[x].rc].rs+(t[x].c!=0);
	}
	int build(int l,int r){
		if(l>r) return 0;
		int mid=(l+r)>>1;
		int x=a[mid];
		for(int i=l;i<=r;i++) T.insert(x,t[a[i]].v);
		t[x].lc=build(l,mid-1);
		t[x].rc=build(mid+1,r);
		pushup(x);
		return x;
	}
	void pia(int x){
		if(!x) return ;
		pia(t[x].lc);
		if(t[x].c) a[++cnt]=x;
		pia(t[x].rc);
		T.clear(T.rt[x]);
	}
	void rebuild(int &x){
		if(t[x].siz*alpha<t[x].rs&&t[x].siz*alpha>1.0*max(t[t[x].lc].siz,t[t[x].rc].siz)) return ;
		cnt=0;
		pia(x);
		x=build(1,cnt);
	} 
	void insert(int &x,int k,int val){
		if(!x){
			x=++tot;
			t[x]={val,-1,val,1,1,1,0,0};
			T.insert(x,val);
			return ;
		}
		T.insert(x,val);
		if(k<=t[t[x].lc].rs+t[x].c) insert(t[x].lc,k,val);
		else insert(t[x].rc,k-t[t[x].lc].rs-t[x].c,val);
		pushup(x);
		rebuild(x); 
	}
	void Del(int &x,int k){
		if(k==t[t[x].lc].rs+t[x].c&&t[x].c) t[x].c--,tmp=t[x].v;
		else if(k<=t[t[x].lc].rs+t[x].c) Del(t[x].lc,k);
		else Del(t[x].rc,k-t[t[x].lc].rs-t[x].c);
		T.Delt(x,tmp);
		pushup(x);
		rebuild(x);
	} 
	pair<int,int> qcmx(int x,int l,int r){
		if(!x||r<1) return make_pair(-1,-1);
//		printf("check  %d %d %d %d %d %d\n",x,l,r,t[x].rs,t[x].mx,t[x].cmx);
		if(l<=1&&t[x].rs<=r) return make_pair(t[x].mx,t[x].cmx);
		int tt=t[t[x].lc].rs+t[x].c;
		if(l>tt) return qcmx(t[x].rc,l-tt,r-tt);
		else if(r<tt) return qcmx(t[x].lc,l,r);
		else{
//			printf("%d %d\n",x,t[x].c);
			pair<int,int> A=qcmx(t[x].lc,l,r),B=qcmx(t[x].rc,l-tt,r-tt);
			int mx=max(A.first,B.first),cmx;
			if(mx==A.first) cmx=max(A.second,B.first);
			else cmx=max(A.first,B.second);
//				printf("%d\n",t[x].c);
			if(t[x].c){
				if(t[x].v>mx) cmx=mx,mx=t[x].v;
				else if(t[x].v>cmx) cmx=t[x].v;
//				printf("%d %d\n",x,mx);
			}
//			printf("%d %d %d %d %d %d\n",A.first,A.second,B.first,B.second,mx,cmx);
			return make_pair(mx,cmx);
		}
	}
	int query(int x,int l,int r,int p){
		if(!x||r<1) return 0;
		if(l<=1&&t[x].rs<=r) return T.query(x,p);
		int tt=t[t[x].lc].rs+t[x].c;
		if(l>tt) return query(t[x].rc,l-tt,r-tt,p);
		else if(r<tt) return query(t[x].lc,l,r,p);//不能加等号!因为这个节点会有值!! 
		else{
			int ret=max(query(t[x].lc,l,r,p),query(t[x].rc,l-tt,r-tt,p));
			if(t[x].c) ret=max(ret,t[x].v^p);
			return ret;
		}
	}
}G;
int main(){
	scanf("%d%d",&n,&m);
	G.tot=n;
	G.t[0]={-1,-1,-1,0,0,0,0,0};
	for(int i=1,x;i<=n;i++){
		scanf("%d",&G.t[i].v);
		a[i]=i;
		G.t[i].c=1;
	}
	R=G.build(1,n);
	while(m--){
		char ch; int x,y;
		scanf(" %c%d",&ch,&x);
		if(ch=='I'){
			scanf("%d",&y);
			x=(x+lst)%n+1,y=(y+lst)%mod;
			G.insert(R,x,y);
			n++;
//			printf("1 %d %d\n",x,y);
		} else if(ch=='D'){
			x=(x+lst)%n+1;
			G.Del(R,x);
			n--;
//			printf("2 %d\n",x);
		} else if(ch=='C'){
			scanf("%d",&y);
			x=(x+lst)%n+1,y=(y+lst)%mod;
			G.Del(R,x);
			G.insert(R,x,y);
//			printf("3 %d %d\n",x,y); 
		} else{
			scanf("%d",&y);
			x=(x+lst)%n+1,y=(y+lst)%n+1;
//			printf("%d\n",G.t[2].rc);
			int p=G.qcmx(R,x,y).second;
//			printf("4 %d %d %d\n",x,y,p);
			lst=G.query(R,x,y,p);
			printf("%d\n",lst);
		} 
	}
	return 0;
}

替罪羊式重构点分树:[Wc2014] 紫荆花之恋

坑了,不会点分树、、(好了在我重学点分树之后我又回来了

替罪羊树在这题的应用就是,重构点分树 + 用平衡树查询时它比 FHQ-treap、Splay 等平衡树快(

点分树的经典应用是多组询问带修树上距离信息,但树的形态本身还在变化,我们就得重构点分树去做。这里依然用替罪羊树的思想,在单次插入点时直接将它与父亲连边,依然设一个平衡银子alpha,如果一个子树占它父亲的子树的比例超过alpha,则暴力重构它的父亲。暴力重构就是重新跑一遍点分树然后维护对应的信息。

按照点分树的套路,枚举LCT,设当前枚举到p,则条件为 dis(i,p)+dis(p,j)ri+rj,转化一下就是 dis(p,j)rjridis(i,p)

开两棵平衡树(显然它也需要重构故不能用线段树),分别维护x子树内所有点的 dis(x,i)ri 的值和 dis(fax,i)ri 的值,查询的时候直接查排名。(注意到这实际上是点分树的基本操作,由于点分树上的父子节点关系被打乱,所以 dis(x,i)dis(fax,i) 不具有直接联系性。而在枚举LCA时计算贡献需要把选择的点在同一子树的贡献减掉,所以多建一个平衡树去容斥。)

另外这题很卡时卡空,有若干优化技巧:

  1. 单次重构复杂度 O(nlog2n) 因此alpha应取大一点如0.85

  2. 重构时取最浅的需要重构的点操作,否则前面的重构会被浪费

  3. 写平衡树时注意用速度快的平衡树,如替罪羊树

  4. 平衡树开垃圾桶

本文作者:YYYmoon

本文链接:https://www.cnblogs.com/YYYmoon/p/18682930

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   YYYmoon  阅读(26)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起