「笔记」FHQ-Treap

时间:2021-07-25
本文章向大家介绍「笔记」FHQ-Treap,主要包括「笔记」FHQ-Treap使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、简介

Treap=Tree+Heap。Treap 是利用堆的性质来维护平衡的一种平衡树。

对每个节点额外存储一个随机值 \(rnd\)(作为关键值),根据随机值调整 Treap 的形态,使其满足 BST 性质外,还使其随机值满足堆的性质(大根堆或小根堆)。

关键值随机生成,则 Treap 树高期望 \(\mathcal O(\log n)\)

无旋 Treap 以分裂和合并为核心。其操作方式使它天生支持维护序列、可持久化等特性。

考场上建议写 FHQ-Treap。

二、核心操作

1. 分裂

按权值分割:根据一个值 \(k\) 将一棵树分裂为两棵,第一棵树中的权值都 \(\leq k\),第二棵树中的权值都 \(>k\)

设权值较小的树为左树,另一棵为右树。

根据二叉搜索树左小右大的性质,可确定分裂的过程:

  • 比较根节点权值 与 给定权值的大小关系
    • 若小于给定权值,则根及左子树点都 \(\leq\) 给定权值,将其归入左树中。然后向右子树递归,继续分裂子树。
    • 否则,根及右子树点都 \(\geq\) 给定权值,将其归入右树中。然后向左子树递归,继续分裂子树。
  • 递归至叶节点后退出。
void split(int p,int &x,int &y,int k){
	if(!p){x=y=0;return ;}
	if(val[p]<=k) x=p,split(rc[p],rc[p],y,k);
	else y=p,split(lc[p],x,lc[p],k);
	upd(p);
}

按大小分割:第一棵树的大小为 \(k\),第二棵树为剩下的部分。

void split2(int p,int &x,int &y,int k){
	if(!p){x=y=0;return ;}
	if(sz[lc[p]]+1<=k) x=p,split(rc[p],rc[p],y,k-sz[lc[p]]-1);
	else y=p,split2(lc[p],x,lc[p],k); 
	upd(p);
}

显然单次分裂复杂度是 \(\mathcal O(\text{height})\) 的。期望树高 \(\log n\),单次分裂期望复杂度为 \(\mathcal O(\log n)\)

2. 合并

\(\text{merge}(x,y)\) 将以 \(x,y\) 节点为根的两棵树合并为一棵,并返回新树根的编号。

注意:合并的两棵树 一定是 Split 分裂获得的两棵树!

因此,合并的前提是,\(x\) 的权值全部 \(<\) \(y\) 的权值

经过 Split 后,得到的两棵 Treap 是有序的,左树任一点权值 必然小于右树任一点。

只需要考虑按关键值,确定父子关系即可(这里关键值满足 小根堆 的性质)。假设两个根为 \(l,r\)

  • \(rnd(l)<rnd(r)\),那么将 \(l\) 作为新树根,保留 \(l\) 的左子树,然后将 \(l\) 的右儿子和 \(r\)​​ 合并得到的新树作为右子树。
  • 否则,将 \(r\) 作为新树根,保留 \(r\) 的右子树,然后将 \(l\)\(r\) 的左儿子合并得到的新树作为左子树。
  • 否则,将右树根作为新树根,新树继承 右树的右子树
    递归,将 左树 与 右树的左子树 合并,作为新树的左子树。
  • 当左 / 右子树不存在时退出。
int merge(int x,int y){	//前提:x 的权值全部 < y 的权值
	if(!x||!y) return x+y;
	if(rnd[x]<rnd[y]){rc[x]=merge(rc[x],y),upd(x);return x;}
	else lc[y]=merge(x,lc[y]),upd(y);
	return y;
} 

三、其他操作

剩下的所有操作都以分裂、合并为基础即可。

例如插入就是先把树分裂成两半,然后把这两半和新节点分别合并。

1. 基本操作

\(\text{upd}(x)\) 更新以 \(x\) 为根的子树大小(或其他信息)。

\(\text{getnew}(k)\) 新建一权值为 \(k\) 的节点,并返回其编号。

void upd(int x){
	sz[x]=sz[lc[x]]+sz[rc[x]]+1;
} 
int getnew(int k){
	val[++tot]=k,rnd[tot]=rand(),sz[tot]=1;
	return tot;
}

2. 插入操作

\(\text{insert}(k)\) 新建一个权值为 \(k\) 的节点,并将其插入至 Treap 中。

\(\text{split}(k)\),得到左树 \(x\)(左树点权值 \(\leq k\))和右树 \(y\)(右树点权值 \(>k\))。

新建一权值为 \(k\) 的节点,然后把左树 \(x\)\(k\) 节点、右树 \(y\) 三部分 \(\text{merge}\) 起来。

void insert(int k){
	int x=0,y=0;
	split(rt,x,y,k),rt=merge(merge(x,getnew(k)),y); 
}

3. 删除操作

\(\text{erase}(k)\) 删除 Treap 中一个权值为 \(k\) 的节点。

考虑一棵树上可能有重复权值的节点,我们需要得到小于、等于、大于 \(k\) 三棵树,然后删除等于\(k\) 的那棵树的根节点(可以通过合并两个儿子实现删根),最后重新合并起来。

void erase(int k){
	int x=0,y=0,z=0;
	split(rt,x,z,k),split(x,x,y,k-1);
	rt=merge(merge(x,merge(lc[y],rc[y])),z);
}

4. 查询排名

\(\text{rank}(k)\) 查询权值 \(k\) 的排名。

\(1\)\(k-1\) 的元素提出来得到树 \(x\),那么 \(k\) 的排名就是 \(x\) 的大小加 \(1\)

int rank(int k){
	int x=0,y=0,ans;
	split(rt,x,y,k-1),ans=sz[x]+1,rt=merge(x,y);
	return ans;
}

5. 第 k 小数

\(\text{kth}(rt,k)\) 查询排名 \(k\) 在子树 \(rt\) 中的权值,返回对应节点的编号。

提供两种方法:

  1. 把前 \(k-1\) 个数拿出来,再从剩下的树种找第 \(1\)​ 个元素就是答案。使用 \(\text{split2}\)(按大小分割)可以实现。

    int kth(int rt,int k){
    	int x=0,y=0,z=0,ans;
    	split2(rt,x,y,k-1),split2(y,1,y,z);
    	ans=y,rt=merge(merge(x,y),z);
    	return ans;
    }
    
  2. 递归。根据二叉树搜索树左小右大的性质 和 维护的子树大小可得答案。

    int kth(int rt,int k){
    	if(!rt) return 0;
    	if(sz[lc[rt]]+1==k) return rt;
    	if(sz[lc[rt]]+1>k) return kth(lc[rt],k);
    	return kth(rc[rt],k-sz[lc[rt]]-1);
    }
    

6. 查询前驱/后继

查询前驱:

  • \(\text{pre}(k)\) 查询 \(k\) 的前驱。前驱定义为小于 \(k\) 且最大的数。
  • 先得到 \(1\)\(k-1\) 构成的数,用 \(\text{kth}\) 函数求出其中最后一个元素即可。
int pre(int k){
	int x=0,y=0,ans;
	split(rt,x,y,k-1),ans=kth(x,sz[x]),rt=merge(x,y);
	return ans;
}

查询后继:

  • \(\text{nxt}(k)\) 查询 \(k\) 的后继。后继定义为大于 \(k\) 且最小的数。
  • 先得到 \(1\)\(k\) 构成的树,用 \(\text{kth}\) 函数求出剩下的树种的最后一个即可。
int nxt(int k){
	int x=0,y=0,ans;
	split(rt,x,y,k),ans=kth(y,1),rt=merge(x,y);
	return ans;
}

四、模板

P3369 【模板】普通平衡树

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,op,x;
struct Treap{
	int rt,tot,lc[N],rc[N],val[N],sz[N],rnd[N];
	void upd(int x){
		sz[x]=sz[lc[x]]+sz[rc[x]]+1;
	} 
	int getnew(int k){
		val[++tot]=k,rnd[tot]=rand(),sz[tot]=1;
		return tot;
	}
	void split(int p,int &x,int &y,int k){
		if(!p){x=y=0;return ;}
		if(val[p]<=k) x=p,split(rc[p],rc[p],y,k);
		else y=p,split(lc[p],x,lc[p],k);
		upd(p);
	}
	int merge(int x,int y){	//前提:x 的权值全部 < y 的权值
		if(!x||!y) return x+y;
		if(rnd[x]<rnd[y]){rc[x]=merge(rc[x],y),upd(x);return x;}
		else lc[y]=merge(x,lc[y]),upd(y);
		return y;
	} 
	void insert(int k){
		int x=0,y=0;
		split(rt,x,y,k),rt=merge(merge(x,getnew(k)),y); 
	}
	void erase(int k){
		int x=0,y=0,z=0;
		split(rt,x,z,k),split(x,x,y,k-1);
		rt=merge(merge(x,merge(lc[y],rc[y])),z);
	}
	int rank(int k){
		int x=0,y=0,ans;
		split(rt,x,y,k-1),ans=sz[x]+1,rt=merge(x,y);
		return ans;
	}
	int kth(int rt,int k){
		if(!rt) return 0;
		if(sz[lc[rt]]+1==k) return rt;
		if(sz[lc[rt]]+1>k) return kth(lc[rt],k);
		return kth(rc[rt],k-sz[lc[rt]]-1);
	}
	int pre(int k){
		int x=0,y=0,ans;
		split(rt,x,y,k-1),ans=kth(x,sz[x]),rt=merge(x,y);
		return ans;
	}
	int nxt(int k){
		int x=0,y=0,ans;
		split(rt,x,y,k),ans=kth(y,1),rt=merge(x,y);
		return ans;
	}
}T;
signed main(){
	srand(time(0));
	scanf("%d",&n);
	while(n--){
		scanf("%d%d",&op,&x);
		if(op==1) T.insert(x);
		else if(op==2) T.erase(x);
		else if(op==3) printf("%d\n",T.rank(x));
		else if(op==4) printf("%d\n",T.val[T.kth(T.rt,x)]); 
		else if(op==5) printf("%d\n",T.val[T.pre(x)]);
		else printf("%d\n",T.val[T.nxt(x)]);
	}
	return 0;
}

五、区间操作

区间修改只需分裂出这个区间,然后打个标记合并即可。举个栗子:

P3391 【模板】文艺平衡树

维护一个序列,支持区间翻转,动态查询每个位置上的数字。

查询区间 \([l,r]\),那么将整个序列分裂为 \([1,l-1]\)\([l,r]\)\([r+1,n]\)(按大小分裂),然后再在 \([l,r]\) 打一个 reverse 区间标记即可。

最后将三个区间合并。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,l,r;
struct Treap{
	int rt,tot,lc[N],rc[N],val[N],sz[N],rnd[N],rev[N];
	void pushup(int x){
		sz[x]=sz[lc[x]]+sz[rc[x]]+1;
	} 
	void pushdown(int x){
		if(!rev[x]) return ;
		swap(lc[x],rc[x]),rev[lc[x]]^=1,rev[rc[x]]^=1;
		rev[x]=0;
	}
	int getnew(int k){
		val[++tot]=k,rnd[tot]=rand(),sz[tot]=1;
		return tot;
	}
	void split(int p,int &x,int &y,int k){	//按子树大小分裂
		if(!p){x=y=0;return ;}
		pushdown(p);
		if(sz[lc[p]]+1<=k) x=p,split(rc[p],rc[p],y,k-sz[lc[p]]-1);
		else y=p,split(lc[p],x,lc[p],k);
		pushup(p);
	}
	int merge(int x,int y){
		if(!x||!y) return x+y;
		if(rnd[x]<rnd[y]){
			pushdown(x),rc[x]=merge(rc[x],y),pushup(x);
			return x;
		}
		else pushdown(y),lc[y]=merge(x,lc[y]),pushup(y);
		return y;
	} 
}T;
void print(int x){
	T.pushdown(x);
	if(T.lc[x]) print(T.lc[x]);
	printf("%d ",T.val[x]);
	if(T.rc[x]) print(T.rc[x]);
}
signed main(){
	srand(time(0));
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		T.rt=T.merge(T.rt,T.getnew(i));
	while(m--){
		scanf("%d%d",&l,&r);
		int x=0,y=0,z=0;
		T.split(T.rt,x,y,l-1),T.split(y,y,z,r-l+1);
		T.rev[y]^=1,T.rt=T.merge(x,T.merge(y,z));
	}
	print(T.rt);
	return 0;
}

原文地址:https://www.cnblogs.com/mytqwqq/p/15057231.html