FHQ Treap

FHQ Treap

简介

FHQ Treap是一棵利用分裂和合并实现插入,删除,查询操作的平衡树。

先提一嘴:所有平衡树的基础操作都能通过合并与分裂实现,所以先实现这两个看似与平衡树无关的操作是必要的,它们的重要性就像其它平衡树的 rotate


分裂

我们需要将一棵 BST 分成两棵,一棵中的每个节点的权值小于等于数字 \(k\) ,另一棵中每个节点的权值都要比数字 \(k\) 大。

为了方便,我们不妨把节点权值小的那棵树叫做小树,另一棵叫做大树。

怎么实现呢?

我们想,当我们遇到一个节点时,它的权值可能有两种情况:

  1. 小于等于 \(k\)
  2. 大于 \(k\)

如果我们遇到的节点小于等于 \(k\) ,根据 BST 性质,它的左子树中的所有节点,权值一定都小于等于 \(k\)

所以呢?

统统丢进我们的小树里就好啦~

那如果大于呢?

把它的右子树统统丢进我们的大树里就好啦~

然后呢?

递归一直丢,丢到叶子节点就好啦~

怎么丢?

我也不知道啦~

咳咳,我们可以先整理一下刚才的成果,无非就下面一句话:

小进小,递归右;大进大,递归左。

意思就是:

如果当前节点的权值是小过 \(k\) 的,进小树,然后递归右子树,反之同理。

那么:

如果当前节点权值小过 \(k\) ,我们的小树当前的左子树就是当前节点的左子树,差右子树没接,同时我们往右递归。

所以接下来,我们再要接小树时,应该接到当前节点的右儿子位置上。

同理,如果当前节点权值大过 \(k\) ,我们接下来如果要接大树时,就要接在当前节点的左儿子位置上?

知道这个有什么用呢?

我们可以粗略考虑像下面这样完成分裂。

now 表示当前节点, x 表示接小树该接的地方, y 表示接大树该接的地方, ls 指代左儿子, rs 指代右儿子, val[] 为记录每个节点权值的数组 )

void split(int now,int k,int &x,int &y)
{
	if (val[now]<=k) x=now,split(rs[now],k,rs[now],y);
	else y=now,split(ls[now],k,x,ls[now]);
}

这里的 x=now , y=now ,就是前面所说的:

小进小;大进大。

后面的就是:

递归左;递归右。

不过我们为了完成接下来的操作,我们还要记录每个节点的子树,这需要我们在分裂合并时更新。

inline void update(int x) { siz[x]=siz[ls[x]]+siz[rs[x]]+1; }

我们还要为递归加上边界,所以最后的分裂代码是这样的:

void split(int now,int k,int &x,int &y)
{
	if (!now) x=y=0;
	else
	{
		if (val[now]<=k) x=now,split(rs[now],k,rs[now],y);
		else y=now,split(ls[now],k,x,ls[now]);
		update(now);
	}
}

合并

顾名思义,合并就是将两棵 BST 合并为 一棵 BST 。特别的是,因为我们在 split 操作中严格规定了哪棵树中的节点更大,所以在合并时我们也可以利用这一点,传递变量时保护好这个性质,这样我们合并时就只用比较 rd 值的大小了。

rd 是什么?

因为 FHQ Treap 是一棵 Treap ,所以它维持平衡的原理是让每个节点带上一个随机值,在维护权值 BST 性质的同时维护随机值的堆性质,形成一棵笛卡尔树,因为堆键值是随机的,所以期望高度为 \(\log n\)

像是下面这样:

void merge(int x,int y)
{
	if (rd[x]<rd[y]) rs[x]=merge(rs[x],y);
	else ls[y]=merge(x,ls[y]);
}

上面这段代码已经能够概括合并的思路了,但是有细节上的问题:

  1. 我们在合并时改变了 BST 的结构,所以需要重新维护 siz 的大小。
  2. 这样的合并是成功了,但是我们也丢失了新树的根,会为下一次的分裂操作带来麻烦。
  3. 我们没有设置递归边界。

所以我们对其进行修改:

int merge(int x,int y)
{
	if (!x||!y) return x+y;
	if (rd[x]<rd[y])
	{
		rs[x]=merge(rs[x],y);
		update(x);
		return x;
	}
	else
	{
		ls[y]=merge(x,ls[y]);
		update(y);
		return y;
	}
}

其中的 if (!x||!y) return x+y; 表示若二者之一为 \(0\) ,返回另一者,即递归边界。


\(k\) 大数

这个操作其实与后面的 num() 目的相同,即求 BST 中第 \(k\) 大的数,但因为可以用于求前驱后继,所以先单独拎出来写。

kth() 的原理非常简单,这得益于我们之前费力维护的 siz 数组,它将在这个环节大显身手。

如果当前节点的 siz 大过 \(k\) ,向左子树递归;

如果当前节点的 siz 等于 \(k-1\) ,那答案就是当前节点;

否则当前节点的 siz 一定小于 \(k-1\) ,将 \(k\) 减去左子树的 siz ,再减去 \(1\) ( 即当前节点 ),向右子树递归。

代码如下:

inline int kth(int now,int k)
{
	while (1)
		if (k<=siz[ls[now]]) now=ls[now];
		else if (k==siz[ls[now]]+1) return now;
		else k-=siz[ls[now]]+1,now=rs[now];
}

插入

终于进入到平衡树所需要完成的操作了,接下来的方便一定会让你感到之前的努力没有白费。

因为我们已经能够将一棵 BST 分裂,所以插入一个点时,直接新建这个点,让原来的树沿着这个点的权值裂开,再将左右子树与这个点合并即可。

我们不妨顺手写一个新建节点的函数,这是我们最后的准备了:

inline int create(int x)
{
	siz[++tot]=1;
	val[tot]=x;
	rd[tot]=rand();
	return tot;
}

那么插入操作的代码如下:

inline void ins(int k)
{
	split(root,k,x,y);
	root=merge(merge(x,create(k)),y);
}

删除

有了分裂操作,删除也是易如反掌。

我们直接将需要删除的节点的两边裂开,把中间的节点 ”取走“ ,再把裂出来的两棵树合并回来就好。

代码如下:

inline void del(int k)
{
	split(root,k,x,z);
	split(x,k-1,x,y);
	y=merge(ls[y],rs[y]);
	root=merge(merge(x,y),z);
}

查询排名

因为我们处理了 siz 数组,所以一个数的排名就等于比它小的节点构成的树的大小,我们同样可以用 split() 完成这项工作。

代码如下:

inline void rank(int k)
{
	split(root,k-1,x,y);
	printf("%d\n",siz[x]+1);
	root=merge(x,y);
}

查询第 \(k\) 大数

这不就是我们刚才写的 kth() 嘛,摆上摆上。

代码如下:

inline void num(int k) { printf("%d\n",val[kth(root,k)]); }

前驱

求前驱,相当于求比这个节点权值小的节点构成的树( 小树 )中的权值最大节点,说起来有点绕,上码。

代码如下:

inline void last(int k)
{
	split(root,k-1,x,y);
	printf("%d\n",val[kth(x,siz[x])]);
	root=merge(x,y);
}

后继

与求前驱类似,求后继就是求比这个节点权值大的节点构成的树( 大树 )中的权值最小节点。

代码如下:

inline void next(int k)
{
	split(root,k,x,y);
	printf("%d\n",val[kth(y,1)]);
	root=merge(x,y);
}

Code

P3369 【模板】普通平衡树

#include <stdlib.h>
#include <cstdio>
#include <ctime>
using namespace std;
#define MAXN (int)(1e5+233)

int n,opt,in;
int tot,root,x,y,z;
int ls[MAXN],rs[MAXN],val[MAXN],rd[MAXN],siz[MAXN];

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

inline void update(int x) { siz[x]=siz[ls[x]]+siz[rs[x]]+1; }

inline int create(int x)
{
	siz[++tot]=1;
	val[tot]=x;
	rd[tot]=rand();
	return tot;
}

void split(int now,int k,int &x,int &y)
{
	if (!now) x=y=0;
	else
	{
		if (val[now]<=k) x=now,split(rs[now],k,rs[now],y);
		else y=now,split(ls[now],k,x,ls[now]);
		update(now);
	}
}

int merge(int x,int y)
{
	if (!x||!y) return x+y;
	if (rd[x]<rd[y])
	{
		rs[x]=merge(rs[x],y);
		update(x);
		return x;
	}
	else
	{
		ls[y]=merge(x,ls[y]);
		update(y);
		return y;
	}
}

inline int kth(int now,int k)
{
	while (1)
		if (k<=siz[ls[now]]) now=ls[now];
		else if (k==siz[ls[now]]+1) return now;
		else k-=siz[ls[now]]+1,now=rs[now];
}

inline void ins(int k)
{
	split(root,k,x,y);
	root=merge(merge(x,create(k)),y);
}

inline void del(int k)
{
	split(root,k,x,z);
	split(x,k-1,x,y);
	y=merge(ls[y],rs[y]);
	root=merge(merge(x,y),z);
}

inline void rank(int k)
{
	split(root,k-1,x,y);
	printf("%d\n",siz[x]+1);
	root=merge(x,y);
}

inline void num(int k) { printf("%d\n",val[kth(root,k)]); }

inline void last(int k)
{
	split(root,k-1,x,y);
	printf("%d\n",val[kth(x,siz[x])]);
	root=merge(x,y);
}

inline void next(int k)
{
	split(root,k,x,y);
	printf("%d\n",val[kth(y,1)]);
	root=merge(x,y);
}

int main()
{
	srand(time(NULL));
	n=read();
	for (int i=1;i<=n;i++)
	{
		opt=read(),in=read();
		if (opt==1) ins(in);
		else if (opt==2) del(in);
		else if (opt==3) rank(in);
		else if (opt==4) num(in);
		else if (opt==5) last(in);
		else next(in);
	}
	return 0;
}

区间翻转

平衡树还能实现一种操作,就是维护一个有序序列,支持对一个区间内的元素进行区间翻转。

因为我们维护的是节点的顺序,而不是节点的大小排序,所以我们在分裂时并不用考虑节点的权值大小,而是考虑节点的排名。

相对应,原来的分裂方式我们称为权值分裂,而现在的分裂方式我们称为排名分裂。

void split(int now,int k,int &x,int &y)
{
	if (!now) { x=y=0; return; }
	if (tag[now]) pushdown(now);
	if (siz[ls[now]]<k) x=now,split(rs[now],k-siz[ls[now]]-1,rs[now],y);
	else y=now,split(ls[now],k,x,ls[now]);
	update(now);
}

如上,我们在分裂时比较的关键字由节点权值变为节点排名。因为要支持翻转,所以我们要同时维护一个 lazy_tag 。

那么翻转操作也就很简单了。无非就是把要改的那棵子树拉出来,根打上 tag ,再塞回去。( 好恶心口区 )

inline void reverse(int l,int r)
{
	split(root,l-1,x,y);
	split(y,r-l+1,y,z);
	tag[y]^=1;
	root=merge(x,merge(y,z));
}

模板:P3391 【模板】文艺平衡树

#include <stdlib.h>
#include <cstdio>
#include <ctime>
using namespace std;
#define MAXN (int)(1e5+233)

int n,m,l,r;
int tot,root,x,y,z;
int ls[MAXN],rs[MAXN],val[MAXN],rd[MAXN],siz[MAXN];
bool tag[MAXN];

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

inline void swap(int &x,int &y) { int t=x;x=y,y=t; }

inline void update(int x) { siz[x]=siz[ls[x]]+siz[rs[x]]+1; }

void pushdown(int x)
{
    swap(ls[x],rs[x]);
    if (ls[x]) tag[ls[x]]^=1;
    if (rs[x]) tag[rs[x]]^=1;
    tag[x]=0;
}

inline int create(int x)
{
	siz[++tot]=1;
	val[tot]=x;
	rd[tot]=rand();
	return tot;
}

void split(int now,int k,int &x,int &y)
{
	if (!now) { x=y=0; return; }
	if (tag[now]) pushdown(now);
	if (siz[ls[now]]<k) x=now,split(rs[now],k-siz[ls[now]]-1,rs[now],y);
	else y=now,split(ls[now],k,x,ls[now]);
	update(now);
}

int merge(int x,int y)
{
	if (!x||!y) return x+y;
	if (rd[x]<rd[y])
	{
		if (tag[x]) pushdown(x);
		rs[x]=merge(rs[x],y);
		update(x);
		return x;
	}
	else
	{
		if (tag[y]) pushdown(y);
		ls[y]=merge(x,ls[y]);
		update(y);
		return y;
	}
}

inline void ins(int k)
{
	split(root,k,x,y);
	root=merge(merge(x,create(k)),y);
}

inline void reverse(int l,int r)
{
	split(root,l-1,x,y);
	split(y,r-l+1,y,z);
	tag[y]^=1;
	root=merge(x,merge(y,z));
}

void dfs(int now)
{
	if (!now) return;
	if (tag[now]) pushdown(now);
	dfs(ls[now]);
	printf("%d ",val[now]);
	dfs(rs[now]);
}

int main()
{
	srand(time(NULL));
	n=read(),m=read();
	for (int i=1;i<=n;i++) ins(i);
	for (int i=1;i<=m;i++)
	{
		l=read(),r=read();
		reverse(l,r);
	}
	dfs(root);
	return 0;
}
posted @ 2022-04-25 19:03  jzcrq  阅读(42)  评论(0编辑  收藏  举报