「笔记」FHQ-Treap


前置知识 :二叉搜索树

简介

一种二叉搜索树的实现方法。
treap = tree + heap,是一种树和堆组合形成的数据结构。

treap 的每个节点上额外储存一个 关键值。
treap 除满足二叉搜索树性质外,还满足父节点的关键值 \(\ge\) 儿子的关键值。
若关键值随机生成,则 treap 树高期望为\(O(\log n)\)

treap 分为旋转式和无旋式(FHQ-treap)两种,码量均较小。
无旋 treap 以分裂和合并为核心。
其操作方式使它天生支持维护序列、可持久化等特性。

核心操作

分裂

\(\text{Split}(now, Val, \&x, \&y)\) 将以\(now\)节点为根的子树按 参数 \(Val\) 分裂为两颗子树 \(x\), \(y\),并返回其编号。

分为 按权值分裂 和 按关键值分裂 两种。
给定一棵子树,按照给定参数 分裂为两棵子树。
一棵节点的参数都小于等于给定参数,另一棵大于给定参数。

以下以按权值分裂为例,称权值较小的树为左树,另一棵为右树。

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

  • 比较根节点权值 与 给定权值的大小关系
    • 若小于给定权值,则根及左子树点都小于等于给定权值,将其归入左树中。
      向右子树递归,继续分裂子树。
    • 否则,根及右子树点都大于等于给定权值,将其归入右树中。
      向左子树递归,继续分裂子树。
  • 递归至叶节点后退出。

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

void Split(int now, int Val, int &x, int &y)
{
	if(! now) {x = y = 0; return ;}
	if(t[now].val <= Val) x = now, Split(t[now].son[1], Val, t[now].son[1], y);
	else y = now, Split(t[now].son[0], Val, x, t[now].son[0]);
	Update(now);
}

合并

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

特别注意:合并的两棵树 一定是 Split 分裂获得的两棵树
即 Merge 操作的出现, 一定是下列形式的:

Split(Root, Val, tmp1, tmp2);
Root = Merge(tmp1, tmp2);

经过 Split 后,得到的两棵 treap 是有序的,左树任一点权值 必然小于右树任一点。
只需要考虑按关键值,确定父子关系即可。

  • 若左树根的关键值小于右树根关键值,则将左树根作为新树根。
    新树继承 左树的左子树
    递归,将 右树 与 左树的右子树 合并,作为新树的右子树。
  • 否则,将右树根作为新树根,新树继承 右树的右子树
    递归,将 左树 与 右树的左子树 合并,作为新树的左子树。
  • 当左 / 右子树不存在时退出。
int Merge(int x, int y)
{
	if(! x || ! y) return x + y;
	if(t[x].Rand < t[y].Rand)
	{
	  t[x].son[1] = Merge(t[x].son[1], y); Update(x);
	  return x;
	}
	t[y].son[0] = Merge(x, t[y].son[0]); Update(y);
	return y;
}

操作

基本操作

\(\text{Update}(x)\) 更新以\(x\)为根的子树大小。
\(\text{NewNode}(Val)\) 新建一权值为 \(Val\) 的节点,并返回其编号。

void Update(int x) {t[x].size = t[t[x].son[0]].size + t[t[x].son[1]].size + 1;}
int NewNode(int Val)
{
	NodeNum ++;
	t[NodeNum].size = 1; t[NodeNum].val = Val; t[NodeNum].Rand = rand();
	return NodeNum;
}

插入操作

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

先按照 \(Val\) 进行分裂,得到两棵满足上述条件的treap。
新建一权值为 \(Val\) 的节点,先与左树合并(左树点权值 \(< Val\)),再与右树合并 (右树点权值 \(\ge Val\))。

void Insert(int Val)
{
	Split(Root, Val, tmp1, tmp2);
	Root = Merge(Merge(tmp1, NewNode(Val)), tmp2);
}

删除操作

\(\text{Delete}(Val)\) 删除treap中一个权值为 \(Val\)的节点。

按照代码中方式,先将权值为 \(Val\) 的节点分裂至 tmp2 中。
将 tmp2 根的左右子树合并,即删去权值为 \(Val\) 的根节点,保证了只删除一个节点。
再将 tmp1, tmp2, tmp3 合并即可。

void Delete(int Val)
{
	Split(Root, Val, tmp1, tmp3), Split(tmp1, Val - 1, tmp1, tmp2);
	tmp2 = Merge(t[tmp2].son[0], t[tmp2].son[1]);
	Root = Merge(Merge(tmp1, tmp2), tmp3);
}

查询给定权值的排名

\(\text{QueryRank}(Val)\) 查询权值 \(Val\) 的排名。

按权值\(Val - 1\)分裂,所得左树的大小 + 1即为排名。

void QueryRank(int Val)
{
	Split(Root, Val - 1, tmp1, tmp2);
	printf("%d\n", t[tmp1].size + 1);
	Root = Merge(tmp1, tmp2);
}

查询给定排名的权值

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

根据二叉树搜索树左小右大的性质 和 维护的子树大小可得答案。

  • 若左子树非空 且 剩余排名 \(Rank\) 不大于左子树的 Size,则向左子树查找。
  • 若 $Rank = $ 左子树大小 +1,说明当前节点即为所求,返回其编号。
  • 否则将 \(Rank\) 减去左子树大小 +1,向右子树查找。
int Kth(int now, int Key)
{
	while(1)
	{
	  if(Key <= t[t[now].son[0]].size) {now = t[now].son[0]; continue;}
	  if(Key == t[t[now].son[0]].size + 1) return now;
	  Key -= t[t[now].son[0]].size + 1;
	  now = t[now].son[1];
	}
}

查询前驱后继

\(\text{QueryPrecursor}(Val)\) 查询 \(Val\) 的前驱。
前驱定义为小于 \(Val\) 的最大的数。

按权值\(Val - 1\)分裂,所得左树中节点权值均小于\(Val\)
左树中最大的值,即排名为左树Size的值,即为前驱。

void QueryPrecursor(int Val)
{
	Split(Root, Val - 1, tmp1, tmp2);
	printf("%d\n", t[Kth(tmp1, t[tmp1].size)].val);
	Root = Merge(tmp1, tmp2);
}

\(\text{QuerySuccessor}(Val)\) 查询 \(Val\) 的后继。
后继定义为大于 \(Val\) 的最小的数。

按权值\(Val\)分裂,所得右树中节点权值均大于\(Val\)
右树中最小的值,即排名为1的值,即为后继。

void QuerySuccessor(int Val)
{
	Split(Root, Val, tmp1, tmp2);
	printf("%d\n", t[Kth(tmp2, 1)].val);
	Root = Merge(tmp1, tmp2);
}

完整代码

//fhq-treap
/*
By:Luckyblock
*/
#include <cstdio>
#include <ctype.h>
#include <cstdlib>
#include <algorithm>
#define min std::min
#define max std::max
#define ll long long
const int MARX = 1e5 + 10;
//=============================================================
struct FhqTreapNode
{
	int son[2], val, size, Rand;
} t[MARX];
int N, NodeNum, Root;
int tmp1, tmp2, tmp3;
//=============================================================
inline int read()
{
	int f = 1, w = 0; char ch = getchar();
	for(; !isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
	for(; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
	return f * w;
}
void Update(int x) {t[x].size = t[t[x].son[0]].size + t[t[x].son[1]].size + 1;}
int NewNode(int Val)
{
	NodeNum ++;
	t[NodeNum].size = 1; t[NodeNum].val = Val; t[NodeNum].Rand = rand();
	return NodeNum;
}
int Merge(int x, int y)
{
	if(! x || ! y) return x + y;
	if(t[x].Rand < t[y].Rand)
	{
	  t[x].son[1] = Merge(t[x].son[1], y); Update(x);
	  return x;
	}
	t[y].son[0] = Merge(x, t[y].son[0]); Update(y);
	return y;
}
void Split(int now, int Val, int &x, int &y)
{
	if(! now) {x = y = 0; return ;}
	if(t[now].val <= Val) x = now, Split(t[now].son[1], Val, t[now].son[1], y);
	else y = now, Split(t[now].son[0], Val, x, t[now].son[0]);
	Update(now);
}
int Kth(int now, int Rank)
{
	while(1)
	{
	  if(Rank <= t[t[now].son[0]].size) {now = t[now].son[0]; continue;}
	  if(Rank == t[t[now].son[0]].size + 1) return now;
	  Rank -= t[t[now].son[0]].size + 1, now = t[now].son[1];
	}
}
void Insert(int Val)
{
	Split(Root, Val, tmp1, tmp2);
	Root = Merge(Merge(tmp1, NewNode(Val)), tmp2);
}
void Delete(int Val)
{
	Split(Root, Val, tmp1, tmp3), Split(tmp1, Val - 1, tmp1, tmp2);
	tmp2 = Merge(t[tmp2].son[0], t[tmp2].son[1]);
	Root = Merge(Merge(tmp1, tmp2), tmp3);
}
void QueryRank(int Val)
{
	Split(Root, Val - 1, tmp1, tmp2);
	printf("%d\n", t[tmp1].size + 1);
	Root = Merge(tmp1, tmp2);
}
void QueryPrecursor(int Val)
{
	Split(Root, Val - 1, tmp1, tmp2);
	printf("%d\n", t[Kth(tmp1, t[tmp1].size)].val);
	Root = Merge(tmp1, tmp2);
}
void QuerySuccessor(int Val)
{
	Split(Root, Val, tmp1, tmp2);
	printf("%d\n", t[Kth(tmp2, 1)].val);
	Root = Merge(tmp1, tmp2);
}
//=============================================================
int main()
{
	srand(114514);
	N = read();
	for(int i = 1; i <= N; ++ i)
	{
	  int opt = read(), x = read();
	  if(opt == 1) Insert(x);
	  if(opt == 2) Delete(x);
	  if(opt == 3) QueryRank(x);
	  if(opt == 4) printf("%d\n", t[Kth(Root, x)].val);
	  if(opt == 5) QueryPrecursor(x);
	  if(opt == 6) QuerySuccessor(x);
	}
	return 0;
}

写在最后

参考资料
\(\text{Oi-Wiki-Treap}\)
题解 P3369 【【模板】普通平衡树】 - ARFA - 洛谷博客 (图解超详细建议阅读)

posted @ 2020-04-30 11:32  Luckyblock  阅读(696)  评论(2编辑  收藏  举报