【算法竞赛学习笔记/数据结构】平衡树专题之FHQ Treap

28 篇文章 0 订阅
12 篇文章 1 订阅

title : 平衡树之FHQ Treap
date : 2021-11-23
tags : ACM,数据结构
author: Linno


平衡树之FHQ Treap

前置芝士

BST
平衡树的基本概念
Treap

“Treap=BST+Heap”

它整体是拥有BST的性质的,但每一个节点都有一个附加权值,它的附加权值符合堆的性质。因此树的形态以及平衡与否都是由这个附加权值来决定的。而这个权值是随机选取的,因此这棵树大概平衡

FHQ Treap

普通的Treap很难实现恼人的旋转。FHQ Treap应运而生。

这东西只有两种操作:分离和合并,但是可以实现所有BST、Splay、Treap能搞的东西。

Split

将一颗Treap树拆成两颗。

按权值分配
void split(int now,int k,int &x,int &y){
    if(!now) x=y=0;
    else{
        if(val[now]<=k)
            x=now,split(ch[now][1],k,ch[now][1],y);
        else
            y=now,split(ch[now][0],k,x,ch[now][0]);
        update(now);
    }
}

对于所有的点,如果它的权值小于k,那么它的所有左子树就放在左边的树里,如何遍历其右儿子;如果大于k,把它的所有右子树分到右边的树里,遍历左儿子。

按前k个分配
void split(int now,int k,int &x,int &y){
    if(!now) x=y=0;
    else{
        if(k<=sz[ch[now][0]])
            y=now,split(ch[now][0],k,x,ch[now][0]);
        else
            x=now,split(ch[now][1],k-sz[ch[now][0]]-1,ch[now][1],y);
        update(now);
    }
}
merge

把两个Treap合成一个,保证第一个的权值小于第二个。

int merge(int x,int y){
    if(!x||!y) return x+y;
    if(pri[x]<pri[y]){
        ch[x][1]=merge(ch[x][1],y);
        update(x);
        return x;
    }else{
        ch[y][0]=merge(x,ch[y][0]);
        update(y);
        return y;
    }
}
其他操作
insert:插入元素

插入一个权值为v的点,就把树按v拆分成x和y两部分,把v看成是一棵树,先和x合并,再和y合并就变成了插入之后的树。

split(root,v,x,y);
root=merge(merge(x,new_node(v)),y);
del:删除元素

删除权值为v的点,把树按v位最大值分成x和z两部分,此时x树中的最大权值为v,然后按v-1将其分成x和y两部分,此时x中最大权值为v-1,权值为y的结点一定为y的根节点,然后我们无视y的根节点,再将其他部分拼回来。

split(root,v,x,z);
split(x,v-1,x,y);
y=merge(ch[y][0],ch[y][1]);
root=merge(merge(x,y),z);
pre:寻找前驱

找前驱就把root按v-1分成x和y,在x里面找最大的即可。

split(root,v-1,x,y);
printf("%d\n",val[kth(x,sz[x])]);
root=merge(x,y);
nxt:寻找后继

找后继就把root按v分成x和y,在y里面找最小的。

split(root,v,x,y);
printf("%d\n",val[kth(y,1)]);
root=merge(x,y);
rank:查询值v的排名

把root按v-1拆分成x,y,v的排名就是x树的sz

split(root,v-1,x,y);
printf("%d\n",sz[x]+1);
root=merge(x,y);
kth:查询第k小的数

因为是BST,所以和其他平衡树的做法是一样的,k小于左子树大小则迭代,否则减去。

printf("%d\n",val[kth(rt,k)]);
区间操作

先把root的前r个split出来,再把前面的子树中split前l-1个,如何我们就得到了操作区间。详见例题。

例题

luoguP3369 【模板】普通平衡树

包含插入、删除、求第k大值,求值的排名,求前驱后继的经典模板。

#include<bits/stdc++.h>
//#define int long long
using namespace std;
const int N=1e5+7;

int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

int ch[N][3],val[N],pri[N],siz[N];//左右儿子,数值,随机权值,节点数量
int n,sz;//总结点的数量

void update(int x){
    siz[x]=1+siz[ch[x][0]]+siz[ch[x][1]];
}

int new_node(int v){
    siz[++sz]=1;// 新开辟一个节点
    val[sz]=v;
    pri[sz]=rand();
    return sz;
}

int merge(int x,int y){ // 合并操作 
    if(!x||!y) return x+y; //x和y中必定有一个是0
    if(pri[x]<pri[y]){ //比较两棵树根的随机权值 
        ch[x][1]=merge(ch[x][1],y);  //把x加到左边的树上
        update(x); //更新大小 
        return x;
    }else{
        ch[y][0]=merge(x,ch[y][0]);
        update(y);
        return y;
    }
}
void split(int now,int k,int &x,int &y) {//分离操作 
    if(!now) x=y=0;// 到达叶子节点
    else{
        if(val[now]<=k) x=now,split(ch[now][1],k,ch[now][1],y); // 分离右子树
        else y=now,split(ch[now][0],k,x,ch[now][0]);
        update(now);
    }
}
int kth(int now,int k){// 查询排名
    while(1){
        if(k<=siz[ch[now][0]]) now=ch[now][0];// 在左子树中,且数量小于左子树的大小,迭代寻找
        else if(k==siz[ch[now][0]]+1) return now;// 找到了
        else k-=siz[ch[now][0]]+1,now=ch[now][1];// 去右子树找
    }
}

signed main(){
    srand(time(0));
    n=read();
    int root=0,x,y,z,op,a;
    for(int i=1;i<=n;i++){
        op=read();a=read();
        if(op==1){// 插入
            split(root,a,x,y);
            root=merge(merge(x,new_node(a)),y);
        }else if(op==2){//删除x
            split(root,a,x,z);
            split(x,a-1,x,y);
            y=merge(ch[y][0],ch[y][1]);
            root=merge(merge(x,y),z);
        }else if(op==3){ //查询x的排名
            split(root,a-1,x,y);
            printf("%d\n",siz[x]+1);
            root=merge(x,y);
        }else if(op==4){  // 查询排名为a的数
            printf("%d\n",val[kth(root,a)]);
        }else if(op==5){   // 求a的前驱
            split(root,a-1,x,y);
            printf("%d\n",val[kth(x,siz[x])]);
            root=merge(x,y);
        }else if(op==6){ // 求a的后继
            split(root,a,x,y);
            printf("%d\n",val[kth(y,1)]);
            root=merge(x,y);
        }
    }
    return 0;
}
luoguP3391 【模板】文艺平衡树

这是一道平衡树的区间操作的模板题。

#include<bits/stdc++.h>
//#define int long long
using namespace std;
const int N=2e5+7;

int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

int ch[N][2],val[N],pri[N],sz[N],mrk[N],cnt,rt,n,m;

void update(int x){
	sz[x]=1+sz[ch[x][0]]+sz[ch[x][1]];
}

void pushdown(int x){
	if(x&&mrk[x]){
		mrk[x]=0;
		swap(ch[x][0],ch[x][1]);
		if(ch[x][0]) mrk[ch[x][0]]^=1;
		if(ch[x][1]) mrk[ch[x][1]]^=1;
	}
}

int new_node(int v){
	sz[++cnt]=1;
	val[cnt]=v;
	pri[cnt]=rand();
	return cnt;
}

int merge(int x,int y){
	if(!x||!y) return x+y;
	pushdown(x);pushdown(y);
	if(pri[x]<pri[y]){
		ch[x][1]=merge(ch[x][1],y);
		update(x);
		return x;
	}else{
		ch[y][0]=merge(x,ch[y][0]);
		update(y);
		return y;
	}
}

void split(int now,int k,int &x,int &y){
	if(!now) x=y=0;
	else{
		pushdown(now);
		if(k<=sz[ch[now][0]])
			y=now,split(ch[now][0],k,x,ch[now][0]);
		else
			x=now,split(ch[now][1],k-sz[ch[now][0]]-1,ch[now][1],y);
		update(now);
	}
}

int build(int l,int r){
	if(l>r) return 0;
	int mid=l+r>>1,v=mid-1;
	int now=new_node(v);
	ch[now][0]=build(l,mid-1);
	ch[now][1]=build(mid+1,r);
	update(now);
	return now;
}

void dfs(int x){
	if(!x) return;
	pushdown(x);
	dfs(ch[x][0]);
	if(val[x]>=1&&val[x]<=n) printf("%d ",val[x]);
	dfs(ch[x][1]);
}

void res(int l,int r){
	int a,b,c,d;
	split(rt,r+1,a,b);
	split(a,l,c,d);
	mrk[d]^=1;
	rt=merge(merge(c,d),b); 
}

signed main(){
	srand(time(0));
	scanf("%d%d",&n,&m);
	rt=build(1,n+2);
	for(int l,r,i=1;i<=m;i++){
		l=read();r=read();res(l,r);
	}
	dfs(rt);
	puts("");
	return 0;
}
luoguP2042 [NOI2005] 维护数列

综合性比较强的著名毒瘤题,支持区间插入、区间删除以及一些经典操作。

1插入 INSERT ⁡   p o s i   t o t   c 1   c 2 ⋯ c t o t \operatorname{INSERT}\ posi \ tot \ c_1 \ c_2 \cdots c_{tot} INSERT posi tot c1 c2ctot在当前数列的第 posi 个数字后插入 tot 个数字: c 1 , c 2 ⋯ c t o t ∗ c ∗ c_1, c_2 \cdots c_{tot}*c* c1,c2ctotc;若在数列首插入,则 posi为 0
2删除 DELETE ⁡   p o s i   t o t \operatorname{DELETE} \ posi \ tot DELETE posi tot从当前数列的第 posi个数字开始连续删除 tot个数字
3修改 MAKE-SAME ⁡   p o s i   t o t   c \operatorname{MAKE-SAME} \ posi \ tot \ c MAKE-SAME posi tot c从当前数列的第 posi个数字开始的连续 tot个数字统一修改为 c
4翻转 REVERSE ⁡   p o s i   t o t \operatorname{REVERSE} \ posi \ tot REVERSE posi tot取出从当前数列的第 posi个数字开始的 tot个数字,翻转后放入原来的位置
5求和 GET-SUM ⁡   p o s i   t o t \operatorname{GET-SUM} \ posi \ tot GET-SUM posi tot计算从当前数列的第 posi个数字开始的 tot 个数字的和并输出
6求最大子列和 MAX-SUM ⁡ \operatorname{MAX-SUM} MAX-SUM求出当前数列中和最大的一段子列,并输出最大和
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+7;
const int mod=1e9+7;
int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

struct FHQ{
	int ls,rs,sz,val,cov; //左儿子,右儿子,树大小,权值和标记值 
	bool lzy,tag; //两个标记,翻转区间和区间赋值 
	int sum,maxqz,maxhz,maxzd; //区间和,区间最大前缀,区间最大后缀,区间最大子段和 
}T[N];

int stk[N],len;  //栈 
int idx,rt; //结点个数和树根 
char op[10]; //指令 

inline int new_node(int v){  //新建结点 
	idx=stk[len--]; //数列中最多有5e5个数,所以可以用栈维护所有结点编号,做到重复利用空间 
	T[idx].ls=T[idx].rs=T[idx].lzy=T[idx].cov=0; 
	T[idx].sz=1;
	T[idx].val=T[idx].sum=v;
	T[idx].maxqz=T[idx].maxhz=max(0,v); //前缀、后缀可以为空 
	T[idx].maxzd=v; //字段不能为空 
	return idx; //返回节点编号 
}

inline void pushup(int x){ //结点更新信息 
	if(!x) return;
	T[x].sz=T[T[x].ls].sz+T[T[x].rs].sz+1;  //子树大小 
	T[x].sum=T[T[x].ls].sum+T[T[x].rs].sum+T[x].val; //区间和 
	T[x].maxqz=max(max(T[T[x].ls].maxqz,T[T[x].ls].sum+T[x].val+T[T[x].rs].maxqz),0); //区间最大前缀和 
	T[x].maxhz=max(max(T[T[x].rs].maxhz,T[T[x].rs].sum+T[x].val+T[T[x].ls].maxhz),0); //区间最大后缀和 
	T[x].maxzd=max(T[x].val,T[x].val+T[T[x].ls].maxhz+T[T[x].rs].maxqz); //区间最大字段和 
	if(T[x].ls) T[x].maxzd=max(T[x].maxzd,T[T[x].ls].maxzd); 
	if(T[x].rs) T[x].maxzd=max(T[x].maxzd,T[T[x].rs].maxzd); 
}

inline void reverse(int x){ //区间翻转 
	if(!x) return; 
	swap(T[x].ls,T[x].rs); 
	swap(T[x].maxhz,T[x].maxqz); //注意前后缀也要翻转 
	T[x].lzy^=1;
}

inline void cover(int x,int v){ //区间修改 
	T[x].val=T[x].cov=v;  //给子树贴标记 
	T[x].sum=T[x].sz*v; //修改区间(子树)和 
	T[x].maxqz=T[x].maxhz=max(0,T[x].sum); 
	T[x].maxzd=max(v,T[x].sum);
	T[x].tag=1;
}

inline void pushdown(int x){ //下传标记 
	if(!x) return;
	if(T[x].lzy){
		if(T[x].ls) reverse(T[x].ls);
		if(T[x].rs) reverse(T[x].rs);
		T[x].lzy=0;
	}
	if(T[x].tag){
		if(T[x].ls) cover(T[x].ls,T[x].cov);
		if(T[x].rs) cover(T[x].rs,T[x].cov);
		T[x].cov=T[x].tag=0;
	}
}

inline void del(int x){ //删除操作 
	if(!x) return;
	stk[++len]=x; //用栈维护删掉的数以重复利用可以解决空间问题 
	if(T[x].ls) del(T[x].ls);
	if(T[x].rs) del(T[x].rs);
}

inline void split(int x,int &L,int &R,int K){ //分离操作 
	if(!x){L=R=0;return;}
	pushdown(x);
	if(T[T[x].ls].sz+1<=K) L=x,split(T[x].rs,T[L].rs,R,K-T[T[x].ls].sz-1);
	else R=x,split(T[x].ls,L,T[R].ls,K);
	pushup(x);
} 

inline void merge(int x,int y,int &M){ //合并操作 
	if(!x||!y){M=x+y;return;} 
	if(mod%(T[x].sz+T[y].sz)<T[x].sz) pushdown(x),M=x,merge(T[x].rs,y,T[M].rs),pushup(x);//等同于随机权值 
	else pushdown(y),M=y,merge(x,T[y].ls,T[M].ls),pushup(y);
}
 
int n,m,a[N]; 

inline int build(int l,int r){  //将一个序列构建成树的形式 
	if(l==r) return new_node(a[l]);
	int x,mid=((l+r)>>1);
	merge(build(l,mid),build(mid+1,r),x);
	return x;
}

signed main(){
	n=read();m=read();
	for(int i=1;i<=500000;i++) stk[++len]=i;
	for(int i=1;i<=n;i++) a[i]=read();
	merge(rt,build(1,n),rt);
	while(m--){
		scanf("%s",op);
		if(op[0]=='I'){ //区间插入操作 
			int pos=read(),tot=read(),x,y;
			split(rt,x,y,pos); //以pos位置拆开再进行三棵树的合并 
			for(int i=1;i<=tot;i++) a[i]=read();
			merge(x,build(1,tot),x);
			merge(x,y,rt);
		}else if(op[0]=='D'){ //删除操作 
			int pos=read(),tot=read(),x,y,z;
			split(rt,x,y,pos-1); //以pos-1位置拆开 
			split(y,y,z,tot); //再数tot个数才开 
			del(y);
			merge(x,z,rt); //进行合并 
		}else if(op[0]=='M'&&op[2]=='K'){ //区间修改 
			int pos=read(),tot=read(),val=read(),x,y,z;
			split(rt,x,y,pos-1);
			split(y,y,z,tot);
			cover(y,val);  //将区间分离出一棵树来然后进行修改 
			merge(x,y,x);
			merge(x,z,rt); 
		}else if(op[0]=='R'){ //翻转区间 
			int pos=read(),tot=read(),x,y,z;
			split(rt,x,y,pos-1);
			split(y,y,z,tot);
			reverse(y);
			merge(x,y,y);
			merge(y,z,rt);
		}else if(op[0]=='G'){ //区间求和 
			int pos=read(),tot=read(),x,y,z;
			split(rt,x,y,pos-1);
			split(y,y,z,tot);
			printf("%d\n",T[y].sum);
			merge(x,y,y);
			merge(y,z,rt);
		}else if(op[0]=='M'&&op[2]=='X'){ //求最大子序列和 
			printf("%d\n",T[rt].maxzd);
		}
	}
	return 0;
}

luoguP2596 [ZJOI2006]书架

支持以下操作:

  • 若 op为 Top,则后有一个整数 s,表示把编号为 s的书放在最上面。
  • 若 op 为 Bottom,则后有一个整数 s,表示把编号为 s的书放在最下面。
  • 若 op 为 Insert,则后有两个整数 s, t,表示若编号为 s的书上面有 x本书,则放回这本书时他的上面有 x + t本书。
  • 若 op为 Ask,则后面有一个整数 s,表示询问编号为 s的书上面有几本书。
  • 若 op 为 Query,则后面有一个整数 s,询问从上面起第 s本书的编号。
#include<bits/stdc++.h>
#define int long long
#define ls(x) (T[x].ch[0])
#define rs(x) (T[x].ch[1])
#define sz(x) (T[x].sz)
#define val(x) (T[x].val)
#define pri(x) (T[x].pri)
#define fa(x) (T[x].fa)
using namespace std;
const int N=1e5+7;

int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

struct FHQ{
	int ch[2],val,pri,sz,fa;
}T[N];

int n,m,root=0,tot,id[N];

void update(int x){
	T[x].sz=sz(ls(x))+sz(rs(x))+1;
}

bool get(int x){
	return rs(fa(x))==x;
}

int cre(int v){ //新开辟一个结点 
    sz(++tot)=1;
    val(tot)=v;
	pri(tot)=rand();
	id[v]=tot;
    return tot;
}

int merge(int x,int y){ // 合并操作 
    if(!x||!y) return x+y; //x和y中必定有一个是0
    if(pri(x)<pri(y)){ //比较两棵树根的随机权值 
        rs(x)=merge(rs(x),y);  //把x加到左边的树上
        fa(rs(x))=x;
		update(x); //更新大小 
        return x;
    }else{
        ls(y)=merge(x,ls(y));
        fa(ls(y))=y;
        update(y);
        return y;
    }
}

void split(int x,int k,int &a,int &b,int faa=0,int fab=0){
	if(x==0){a=b=0;return;}
	if(k<=sz(ls(x)))  fa(x)=fab,b=x,split(ls(x),k,a,ls(x),faa,x);
	else fa(x)=faa,a=x,split(rs(x),k-sz(ls(x))-1,rs(x),b,x,fab);
	update(x);
}

int find(int k){
	int node=k,res=sz(ls(k))+1;
	while(node!=root&&k){
		if(get(k)) res+=sz(ls(fa(k)))+1;
		k=fa(k);
	}
	return res;
}

signed main(){
    srand(time(0));
    n=read();m=read();
    int x,y,z,w,s,t,k;
    string opt;
    for(int i=1;i<=n;i++){ //插入 
    	s=read();
		split(root,i-1,x,y);
		root=merge(merge(x,cre(s)),y);
	}
	for(int i=1;i<=m;i++){
		cin>>opt;s=read();
		if(opt=="Top"){ //将x放到最上面 
			k=find(id[s]);
			split(root,k,x,z);
			split(x,k-1,x,y);
			root=merge(y,merge(x,z)); 
		}else if(opt=="Bottom"){ //将x放到最下面 
			k=find(id[s]);
			split(root,k,x,z,0);
			split(x,k-1,x,y,0);
			root=merge(x,merge(z,y)); 
		}else if(opt=="Insert"){
			t=read();
			k=find(id[s]);
			if(t>0){ //与前驱交换 
				split(root,k+1,z,w);
				split(z,k,y,z);
				split(y,k-1,x,y);
				root=(merge(x,merge(z,merge(y,w))));
			}
			if(t<0){ //与后继交换 
				split(root,k,z,w);
				split(z,k-1,y,z);
				split(y,k-2,x,y);
				root=merge(x,merge(z,merge(y,w)));
			}
		}else if(opt=="Ask"){
			k=find(id[s]);
			printf("%lld\n",k-1);
		}else if(opt=="Query"){
			split(root,s,x,y);
			int node=x;
			while(rs(node)) node=rs(node);
			printf("%lld\n",val(node));
			root=merge(x,y);
		}
    }
    return 0;
}

参考资料

http://www.yhzq-blog.cc/fhqtreapzongjie/

https://www.luogu.com.cn/blog/acking/solution-FHQTreap

https://www.cnblogs.com/zwfymqz/p/7151959.html

https://www.luogu.com.cn/blog/zyxxs/solution-p2042

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RWLinno

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值