平衡树总结

从BST引入。

我们要高效查找一个值,那么在保证左儿子小于右儿子的二叉树上跳,期望O(d)d为深度。

二叉搜索树BST

最好O(logn),最坏O(n)

左子树的权值小于根的权值小于右子树的权值。

P用没有

替罪羊树

是一种依靠重构来维持平衡的重量平衡树。在插入删除时发现沿途失衡,就将当前子树暴力重构。

通过设置比例常数α(0.5,1)来判断是否能重构。若当前节点的子节点的大小占它的大小的比例超过了α

另外由于采用惰性删除,只是计数器减一,以删除节点过多也会影响效率,所以当未被删除的节点个数占总大小的比例低于α时,也要重构。

时间复杂度O(nlogn)。这种树的重构方式比起根号重构更优秀,在其他东西(比如带修的点分树)中也可以借鉴。

板子
#include<bits/stdc++.h>

using namespace std;

const int maxn=1e6+1e5+1;
const double alpha=0.8;
int lc[maxn],rc[maxn],val[maxn],w[maxn],s[maxn],siz[maxn],sd[maxn],ldr[maxn],sz,rt,n,m,last=0,ans=0,op,p,rnk,num,pre,sub;

inline void pushup(int k){
	s[k]=s[lc[k]]+s[rc[k]]+1;
	siz[k]=siz[lc[k]]+siz[rc[k]]+w[k];
	sd[k]=sd[lc[k]]+sd[rc[k]]+(w[k]!=0);
}

inline bool canreb(int k){
	return w[k]&&((double)max(s[lc[k]],s[rc[k]])>=alpha*s[k]||(double)sd[k]<=alpha*s[k]);
}

void rebfla(int &ldc,int k){
	if(!k) return;
	rebfla(ldc,lc[k]);
	if(w[k]) ldr[ldc++]=k;
	rebfla(ldc,rc[k]);
}

int rebuild(int l,int r){
	if(l>=r) return 0;
	int mid=(l+r)>>1;
	lc[ldr[mid]]=rebuild(l,mid);
	rc[ldr[mid]]=rebuild(mid+1,r);
	pushup(ldr[mid]);
	return ldr[mid];
}

void reb(int &k){
	int ldc=0;
	rebfla(ldc,k);
	k=rebuild(0,ldc);
}

void insert(int &k,int x){
	if(!k){
		k=++sz;
		if(!rt) rt=1;
		val[k]=x;
		lc[k]=rc[k]=0;
		w[k]=s[k]=siz[k]=sd[k]=1;
		return;
	}
	else{
		if(val[k]==x) w[k]++;
		else if(val[k]<x) insert(rc[k],x);
		else insert(lc[k],x);
		pushup(k);
		if(canreb(k)) reb(k);
	}
}

void del(int &k,int x){
	if(!k) return;
	else {
		if(val[k]==x){
			if(w[k]) w[k]--;
        }
		else{
			if(val[k]<x) del(rc[k],x);
			else del(lc[k],x);
		}
		pushup(k);
		if(canreb(k)) reb(k);
	}
}

int upper(int k,int x){
	if(!k) return 1;
	else if(val[k]==x&&w[k]) return siz[lc[k]]+w[k]+1;
	else if(val[k]<x) return siz[lc[k]]+w[k]+upper(rc[k],x);
	else return upper(lc[k],x);
}

int unupper(int k,int x){
	if(!k) return 0;
	else if(val[k]==x&&w[k]) return siz[lc[k]];
	else if(val[k]<x) return siz[lc[k]]+w[k]+unupper(rc[k],x);
	else return unupper(lc[k],x);
}

int querynum(int k,int x){
	if(!k) return 0;
	else if(siz[lc[k]]>=x) return querynum(lc[k],x);
	else if(siz[lc[k]]+w[k]<x) return querynum(rc[k],x-siz[lc[k]]-w[k]);
	else return val[k];
}

int querypre(int k,int x){
	return querynum(k,unupper(k,x));
}

int querysub(int k,int x){
	return querynum(k,upper(k,x));
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){
		scanf("%d",&p);
		insert(rt,p);
	}
	while(m--){
		scanf("%d%d",&op,&p);
		p=p^last;
		if(op==1) insert(rt,p);
		else if(op==2) del(rt,p);
		else if(op==3){
			rnk=unupper(rt,p)+1;
			last=rnk;
			ans^=rnk;
		}
		else if(op==4){
			num=querynum(rt,p);
			last=num;
			ans^=num;
		}
		else if(op==5){
			pre=querypre(rt,p);
			last=pre;
			ans^=pre;
		}
		else if(op==6){
			sub=querysub(rt,p);
			last=sub;
			ans^=sub;
		}
	}
	printf("%d\n",ans);
	return 0;
}

Treap

是一种弱平衡的二叉搜索树。同时符合堆和二叉搜索树的性质。

堆的权值为随机权值,以保证复杂度。

当堆的性质被破坏需要调整时,有旋Treap通过左旋或右旋来调整。FHQ-Treap在合并时使用随机赋的权值。

小根堆或者大根堆都行,都是为了复杂度正确。

注意到随机权值可以保证树高为logn的,于是可以大胆做一些看似暴力实则正确的操作。

有旋Treap

板子
#include<bits/stdc++.h>
using namespace std;
const int maxn=100004;
int l[maxn],r[maxn],val[maxn],siz[maxn],rnd[maxn],w[maxn],sz,ans,rt,n,op,a;
inline void pushup(int k)
{
	siz[k]=siz[l[k]]+siz[r[k]]+w[k];
}
inline void rrotate(int &k)
{
	int t=l[k];
	l[k]=r[t];
	r[t]=k;
	siz[t]=siz[k];
	pushup(k);
	k=t;
}
inline void lrotate(int &k)
{
	int t=r[k];
	r[k]=l[t];
	l[t]=k;
	siz[t]=siz[k];
	pushup(k);
	k=t;
}
void insert(int &k,int x)
{
	if(!k)
	{
		++sz;
		k=sz;
		val[k]=x;
		siz[k]=1;
		w[k]=1;
		rnd[k]=rand();
		return;
	}
	siz[k]++;
	if(val[k]==x)
	{
		w[k]++;
	}
	else if(val[k]<x)
	{
		insert(r[k],x);
		if(rnd[r[k]]<rnd[k]) lrotate(k);
	}
	else
	{
		insert(l[k],x);
		if(rnd[l[k]]<rnd[k]) rrotate(k);
	}
}
bool del(int &k,int x)
{
	if(!k) return false;
	if(val[k]==x)
	{
		if(w[k]>1)
		{
			w[k]--;
			siz[k]--;
			return true;
		}
		if(l[k]==0||r[k]==0)
		{
			k=l[k]+r[k];
			return true;
		}
		else if(rnd[l[k]]<rnd[r[k]])
		{
			rrotate(k);
			return del(k,x);
		}
		else
		{
			lrotate(k);
			return del(k,x);
		}
	}
	else if(val[k]<x)
	{
		bool suc=del(r[k],x);
		if(suc) siz[k]--;
		return suc;
	}
	else
	{
		bool suc=del(l[k],x);
		if(suc) siz[k]--;
		return suc;
	}
}
int queryrank(int k,int x)
{
	if(!k) return 0;
	if(x==val[k])
	{
		return siz[l[k]]+1;
	}
	else if(x>val[k])
	{
		return siz[l[k]]+w[k]+queryrank(r[k],x);
	}
	else
	{
		return queryrank(l[k],x);
	}
}
int querynum(int k,int x)
{
	if(!k) return 0;
	else if(x<=siz[l[k]])
	{
		return querynum(l[k],x);
	}
	else if(x>siz[l[k]]+w[k])
	{
		return querynum(r[k],x-siz[l[k]]-w[k]);
	}
	else 
	{
		return val[k];
	}
}
void querypre(int k,int x)
{
	if(!k) return;
	else if(val[k]<x)
	{
		ans=k;
		querypre(r[k],x);
	}
	else querypre(l[k],x);
}
void querysub(int k,int x)
{
	if(!k) return;
	else if(val[k]>x)
	{
		ans=k;
		querysub(l[k],x);
	}
	else querysub(r[k],x);
}
int main()
{
	srand(1919810);
	scanf("%d",&n);
	while(n--)
	{
		scanf("%d%d",&op,&a);
		if(op==1)
		{
			insert(rt,a);
		}
		else if(op==2)
		{
			del(rt,a);
		}
		else if(op==3)
		{
			printf("%d\n",queryrank(rt,a));
		}
		else if(op==4)
		{
			printf("%d\n",querynum(rt,a));
		}
		else if(op==5)
		{
			ans=0;
			querypre(rt,a);
			printf("%d\n",val[ans]);
		}
		else if(op==6)
		{
			ans=0;
			querysub(rt,a);
			printf("%d\n",val[ans]);
		}
	}
	return 0;
}

无旋Treap(FHQ-Treap)

最重要的操作是分裂与合并。分裂通过引用l,r返回分裂出的两棵树的根,分为按值分裂(valx的在l,其余在r),和按排名分裂(rkk的在l,其余在r)。合并通过引用返回合并后的根。

板子
#include<bits/stdc++.h>

using namespace std;

#define gc getchar
int rd(){
	int f=1,r=0;
	char ch=gc();
	while(!isdigit(ch)){ if(ch=='-') f=-1;ch=gc();}
	while(isdigit(ch)){ r=(r<<3)+(r<<1)+(ch^48);ch=gc();}
	return f*r;
} 

mt19937 rnd(19260817);

const int maxn=1e5+10;
int n,tot,rt,sz[maxn],rv[maxn],val[maxn],ch[2][maxn];

#define ls(k) (ch[0][k])
#define rs(k) (ch[1][k])

void pushup(int u){
	sz[u]=sz[ls(u)]+sz[rs(u)]+1;
}

int newnode(int v){
	sz[++tot]=1;
	rv[tot]=rnd();
	val[tot]=v;
	return tot;
}

void splitv(int u,int v,int &l,int &r){
	if(!u){
		l=r=0;
		return;
	}
	if(val[u]<=v) l=u,splitv(rs(u),v,rs(l),r);
	else r=u,splitv(ls(u),v,l,ls(r));
	pushup(u);
}

void splitsz(int u,int k,int &l,int &r){
	if(!u){
		l=r=0;
		return;
	}
	if(sz[ls(u)]+1<=k) l=u,splitsz(rs(u),k-sz[ls(u)]-1,rs(l),r);
	else r=u,splitsz(ls(u),k,l,ls(r));
	pushup(u);
}

void merge(int &u,int l,int r){
	if(!l||!r){
		u=l+r;
		if(u) pushup(u);
		return;
	}
	if(rv[l]>rv[r]) u=l,merge(rs(u),rs(l),r);
	else u=r,merge(ls(u),l,ls(r));
	pushup(u);
}

void insert(int x){
	int rtl=0;
	splitv(rt,x,rtl,rt);
	merge(rtl,rtl,newnode(x));
	merge(rt,rtl,rt);
}

void del(int x){
	int rt1=0;
	splitv(rt,x,rt1,rt);
	int rt2=0;
	splitv(rt1,x-1,rt2,rt1);
	int rt3=0;
	splitsz(rt1,1,rt3,rt1);
	merge(rt1,rt2,rt1);
	merge(rt,rt1,rt);
}

int qryrk(int x){
	int rt1=0;
	splitv(rt,x-1,rt1,rt);
	int rs=sz[rt1]+1;
	merge(rt,rt1,rt);
	return rs;
}

int qryval(int x){
	int rt1=0;
	splitsz(rt,x,rt1,rt);
	int rt2=0;
	splitsz(rt1,x-1,rt2,rt1);
	int rs=val[rt1];
	merge(rt1,rt2,rt1);
	merge(rt,rt1,rt);
	return rs;
}

int qrymx(int u){
	if(rs(u)) return qrymx(rs(u));
	return val[u];
}

int qrymi(int u){
	if(ls(u)) return qrymi(ls(u));
	return val[u];
}

int qrypre(int x){
	int rt1=0;
	splitv(rt,x-1,rt1,rt);
	int rs=qrymx(rt1);
	merge(rt,rt1,rt);
	return rs;
}

int qrysuf(int x){
	int rt1=0;
	splitv(rt,x,rt1,rt);
	int rs=qrymi(rt);
	merge(rt,rt1,rt);
	return rs;
}

void solve(){
	int op=rd(),x=rd();
	if(op==1) insert(x);
	else if(op==2) del(x);
	else if(op==3) printf("%d\n",qryrk(x));
	else if(op==4) printf("%d\n",qryval(x));
	else if(op==5) printf("%d\n",qrypre(x));
	else printf("%d\n",qrysuf(x));
	return;
}

int main(){
	n=rd();
	while(n--) solve(); 
	return 0;
}

FHQ-Treap更好写,可以持久化,结构更加灵活,这是相较于有旋Treap的优势。

值域有交的合并

不得不进行值域有交的合并时,可以做到O(log2n)

我们考虑每次从两棵树中把最小值分出来,比个大小,然后按次序合并到一起。

这是O(段数logn),一顿势能分析可知是O(log2n)

Splay

时间复杂度靠均摊保证,每次操作后需要Splay()操作来保证时间复杂度,于是不太稳定,但很重要。可以说Splay的结构是平衡树中最灵活的,除了不能分裂合并。

板子
#include<bits/stdc++.h>

using namespace std;

const int maxn=1e5+10;
int n,m,sz=0,rt,a[maxn];
struct TREE{
	int val,siz,fa,ch[2],tag;
	inline void init(int _v,int _fa,int _siz,int _tag){
		val=_v,fa=_fa,siz=_siz,tag=_tag;
	}
}t[maxn];

inline void pushup(int k){
	t[k].siz=t[t[k].ch[0]].siz+t[t[k].ch[1]].siz+1; 
}

inline void pushdown(int k){
	if(t[k].tag){
		swap(t[k].ch[0],t[k].ch[1]);
		t[t[k].ch[0]].tag^=1;
		t[t[k].ch[1]].tag^=1;
		t[k].tag=0;
	}
} 

int build(int f,int l,int r){
	if(l>r) return 0;
	int mid=(l+r)>>1;
	int k=++sz;
	t[k].init(a[mid],f,1,0);
	t[k].ch[0]=build(k,l,mid-1);
	t[k].ch[1]=build(k,mid+1,r);
	pushup(k);
	return k;
}

inline void rotate(int x){
	int y=t[x].fa,z=t[y].fa,k=(t[y].ch[1]==x);
	t[x].fa=z,t[z].ch[(t[z].ch[1]==y)]=x;
	t[y].ch[k]=t[x].ch[k^1],t[t[x].ch[k^1]].fa=y;
	t[x].ch[k^1]=y,t[y].fa=x;
	pushup(y);
	pushup(x);
}

inline void splay(int x,int g){
	pushdown(x);
	if(!g) rt=x;
	while(t[x].fa!=g){
		int y=t[x].fa,z=t[y].fa;
		if(z!=g) (x==t[y].ch[0])^(y==t[z].ch[0])?rotate(x):rotate(y);
		rotate(x);
	}
}

inline int find(int x){
	int u=rt;
	if(!u) return -1;
	while(u){
		pushdown(u);
		if(t[t[u].ch[0]].siz+1==x){
			return u;
		}
		else if(t[t[u].ch[0]].siz>=x){
			u=t[u].ch[0];
		}
		else{
			x-=t[t[u].ch[0]].siz+1;
			u=t[u].ch[1];
		}
	}
	return -1;
}

void print(int u){
	pushdown(u);
	if(t[u].ch[0]) print(t[u].ch[0]);
	if(t[u].val>=1&&t[u].val<=n) printf("%d ",t[u].val);
	if(t[u].ch[1]) print(t[u].ch[1]);
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n+2;++i) a[i]=i-1;
	rt=build(0,1,n+2);
	for(int i=1;i<=m;++i){
		int l,r;
		scanf("%d%d",&l,&r);
		l=find(l),r=find(r+2);
		splay(l,0),splay(r,l);
		t[t[r].ch[0]].tag^=1;
	}
	print(rt);
	return 0;
} 

可持久化平衡树(可持久化FHQ-Treap)

可持久化的思想都是一样的,把改变过的节点存下来以可持久化。

但是在平衡树里结构改变的地方太多了,FHQ稍微少一点但也很多。

splitvsplitszmergepushdown,以及打标记的时候都要可持久化。

特别的,如果不涉及从很久远的版本继承过来进行操作,而是和邻近的版本有关,但是有复制之类的操作时(这个东西在随机数据下是真的,刻意构造就假了),也需要可持久化。这时如果空间很紧,可以设置上限,达到后便线性地重构一下FHQ,释放一下废弃节点。大小尽量开大,往50倍开。

板子
#include<bits/stdc++.h>

using namespace std;

#define gc getchar
int rd(){
	int f=1,r=0;
	char ch=gc();
	while(!isdigit(ch)){ if(ch=='-') f=-1;ch=gc();}
	while(isdigit(ch)){ r=(r<<3)+(r<<1)+(ch^48);ch=gc();}
	return f*r;
}

mt19937 rnd(19260817);
const int maxn=5e5+10,inf=(1ll<<31)-1;
int n,tot,p,rt[maxn];
struct FHQ{
	int rv,val,sz,ch[2];
}t[maxn<<6];

#define ls(u) (t[u].ch[0])
#define rs(u) (t[u].ch[1])

void pushup(int u){
	t[u].sz=t[ls(u)].sz+t[rs(u)].sz+1;
}

int newnode(int v){
	t[++tot].sz=1;
	t[tot].rv=rnd();
	t[tot].val=v;
	return tot;
}

void splitv(int u,int v,int &l,int &r){
	if(!u){
		l=r=0;
		return;
	}
	if(t[u].val<=v){
		l=++tot;
		t[l]=t[u];
		splitv(rs(u),v,rs(l),r);
		pushup(l);
	}
	else{
		r=++tot;
		t[r]=t[u];
		splitv(ls(u),v,l,ls(r));
		pushup(r);
	}
}

void splitsz(int u,int k,int &l,int &r){
	if(!u){
		l=r=0;
		return;
	}
	if(t[ls(u)].sz+1<=k){
		l=++tot;
		t[l]=t[u];
		splitsz(rs(u),k-t[ls(u)].sz-1,rs(l),r);
		pushup(l);
	}
	else{
		r=++tot;
		t[r]=t[u];
		splitsz(ls(u),k,l,ls(r));
		pushup(r);
	}
}

void merge(int &u,int l,int r){
	if(!l||!r){
		u=l+r;
		if(u) pushup(u);
		return;
	}
	u=++tot;
	if(t[l].rv>t[r].rv){
		t[u]=t[l];
		merge(rs(u),rs(l),r);
	}
	else{
		t[u]=t[r];
		merge(ls(u),l,ls(r));
	}
	pushup(u);
}

void insert(int x,int &prt){
	int rt1=0;
	splitv(prt,x,prt,rt1);
	merge(prt,prt,newnode(x));
	merge(prt,prt,rt1);
}

void del(int x,int &prt){
	int rt1=0;
	splitv(prt,x,rt1,prt);
	int rt2=0;
	splitv(rt1,x-1,rt2,rt1);
	if(t[rt1].sz) merge(rt1,ls(rt1),rs(rt1));
	merge(rt1,rt2,rt1);
	merge(prt,rt1,prt);
}

int qryrk(int x,int &prt){
	int rt1=0;
	splitv(prt,x-1,rt1,prt);
	int rs=t[rt1].sz+1;
	merge(prt,rt1,prt);
	return rs;
}

int qrymx(int u){
	if(rs(u)) return qrymx(rs(u));
	return t[u].val;
}

int qrymi(int u){
	if(ls(u)) return qrymi(ls(u));
	return t[u].val;
}

int qryval(int x,int &prt){
	int rt1=0;
	splitsz(prt,x,rt1,prt);
	int rs=qrymx(rt1);
	merge(prt,rt1,prt);
	return rs;
}

int qrypre(int x,int &prt){
	int rt1=0;
	splitv(prt,x-1,rt1,prt);
	int rs=t[rt1].sz?qrymx(rt1):-inf;
	merge(prt,rt1,prt);
	return rs;
}

int qrysuf(int x,int &prt){
	int rt1=0;
	splitv(prt,x,prt,rt1);
	int rs=t[rt1].sz?qrymi(rt1):inf;
	merge(prt,prt,rt1);
	return rs;
}

void solve(){
	int v=rd(),op=rd(),x=rd();
	rt[++p]=rt[v];
	if(op==1) insert(x,rt[p]);
	else if(op==2) del(x,rt[p]);
	else if(op==3) printf("%d\n",qryrk(x,rt[p]));
	else if(op==4) printf("%d\n",qryval(x,rt[p]));
	else if(op==5) printf("%d\n",qrypre(x,rt[p]));
	else printf("%d\n",qrysuf(x,rt[p]));
}

int main(){
	n=rd();
	while(n--) solve();
	return 0;
}
posted @   RandomShuffle  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示