[数据结构]FHQ-Treap

前言(个人评价FHQ-Treap)

这是一个巨佬(名叫范浩强)在冬令营交流的时候提出的数据结构(FHQ:\(\text{你干嘛非要旋转呢?Think Functional!}\))(可以看出FHQ大佬英语可能不太过关)

其实就是非旋Treap. 由于不用复杂的旋转, 所以支持可持久化, 且代码简单易懂(只要您熟悉函数式编程的思想).

由于本人思维怠惰所以觉得这种数据结构很符合我的胃口, 唯一的不足之处在于维护LCT的时候, 要多一个\(\log\)(由于蒟蒻我没有学LCT所以不确定是否正确).

核心思想

FHQ-Treap依然借鉴了Treap的依靠随机权值维护树高的方法, 但在操作时, 从不\(\text{rotate}\), 而是依靠两个核心操作\(\text{split}\)\(\text{merge}\).
在写代码的时候要时刻记得, 一个根就是一棵树.

基本函数

1.\(\text{split}\)

将一棵平衡树按照权值或排名"分裂"成两棵树, 保证一棵树的权值小于另一棵. 代码中, 以\(\text{x}\)为根的树是权值较小的那一棵.
根据Treap的性质, 每次能直接把一半的节点直接扔进一个分裂出来的树中, 所以时间复杂度\(O(\log{n})\)

void split(int cur, int k, int &x, int &y)
{
	if(!cur)	x = y = 0;
	else
	{
		if(tr[cur].val <= k)	x = cur, split(tr[cur].rs, k, tr[cur].rs, y);   //若这个节点的权值<=k, 那么这个节点的左子树都<=k, 直接递归右子树即可.
		else y = cur, split(tr[cur].ls, k, x, tr[cur].ls); //否则右子树都>k, 递归左子树即可.
		update(cur);
	}
}

2.\(\text{merge}\)

有分裂就要有合并. 同样的, 在合并两棵平衡树的时候, 保证一颗树的权值小于另一棵. 代码中, 以\(\text{x}\)为根的树是权值较小的那一棵.
合并就要显然一些了. 只需要考虑是\(\text{x}\)合到\(\text{y}\)的左子树还是\(\text{y}\)合到\(\text{x}\)的右子树. 这个通过随机权值来决定, 递归下去就好了.

inline int merge(int x, int y)
{
	if(!x || !y)	return x + y;
	int ret;
	if(tr[x].pri < tr[y].pri)	ret = x, tr[x].rs = merge(tr[x].rs, y);
	else ret = y, tr[y].ls = merge(x, tr[y].ls);
	update(ret);
	return ret;
}

没错, 基本函数就这两个, 简单吧! 下面我们来看靠这两个函数可以实现什么.

功能实现

在下面的代码中, 我们定义\(\text{x}\), \(\text{y}\), \(\text{z}\)是一棵分裂出来的子树.

1.\(\text{insert}\)(插入一个数, 权值为\(v\))

极其简单的代码实现. 比Splay简单多了.
从代码明显看到, 插入一个数的思路是, 按照\(v\)\(\text{split}\), 再\(\text{merge}\)回去.

split(rt, v, x, y);
rt = merge(merge(x, new_Node(v)), y);

2.\(\text{delete}\)(删除一个权值为\(\text{v}\)的数)

极其简单的代码实现. 比Splay简单多了.
\(\text{delete}\)操作的方法是, 先按照\(v\)把原树分裂成\(\text{y}\)\(\text{z}\), 再按照\(v-1\)\(\text{y}\)分裂成\(\text{x}\)\(\text{y}\), 这样\(\text{y}\)中就只剩下了权值为\(v\)的节点.
若删除一个, 那么就合并\(\text{y}\)的左儿子和右儿子, 再并回去, 若删除全部, 则直接合并\(\text{x}\)\(\text{z}\)即可.

split(rt, v, x, z);
split(x, v - 1, x, y);
rt = merge(merge(x, merge(tr[y].ls, tr[y].rs)), z);

3.\(\text{rank}\)(求一个数的排名)

显然把原树按照\(v\)分裂成\(\text{x}\)\(\text{y}\)之后, 答案就是\(\text{x}\)的节点数.
别忘了最后\(\text{merge}\)回去.

split(rt, v - 1, x, y);
printf("%d\n", tr[x].sz + 1);
rt = merge(x, y);

4.\(k\text{th}\)(求第\(k\)大的数)

类似所有二叉平衡树的方法, 不多讲了( Q:为什么就这个操作写成一个函数呢? A:之后求前驱后继还需要用...
代码中\(\text{x}\)是这棵树的根.

inline int kth(int x, int k)
{
	if(k > tr[x].sz)	k = tr[x].sz;
	while(1)
	{
		if(k <= tr[tr[x].ls].sz)	x = tr[x].ls;
		else if(k == tr[tr[x].ls].sz + 1)	return x;
		else k -= (tr[tr[x].ls].sz + 1), x = tr[x].rs;
	}
}

主程序中:

printf("%d\n", tr[kth(rt, v)].val);

5.\(\text{precursor}\)(求一个数的前驱(小于\(v\)的最大数))

显然按照\(v-1\)分裂\(\text{x}\)\(\text{y}\)后, 答案就是\(\text{x}\)中最大的那个数.
最大的那个怎么求呢? 那就是跟\(\text{x}\)的节点个数一样大呗..

split(rt, v - 1, x, y);
printf("%d\n", tr[kth(x, tr[x].sz)].val);
rt = merge(x, y);

6.\(\text{succesor}\)(求一个数的后继(大于\(v\)的最小数))

类比\(\text{precursor}\)的做法, 按照\(v\)分裂\(\text{x}\)\(\text{y}\)后, 答案就是\(\text{y}\)中的最小值.

split(rt, v, x, y);
printf("%d\n", tr[kth(y, 1)].val);
rt = merge(x, y);

以下是洛谷P3369 【模板】普通平衡树的完整AC代码:

#include <ctime>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#define N 100010
#define init(a, b) memset(a, b, sizeof(a))
#define fo(i, a, b) for(int i = (a); i <= (b); ++i)
#define fd(i, a, b) for(int i = (a); i >= (b); --i)
using namespace std;
inline int read() // notice : 1. long long ? 2. negative ?
{
	int x = 0; char ch = getchar(); bool ne = 0;
	while(ch < '0' || ch > '9')	ne |= (ch == '-'), ch = getchar();
	while(ch >= '0' && ch <= '9')	x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();
	return ne ? -x : x;
}
struct Node
{
	int sz, ls, rs, val, pri;
	Node(){}
	Node(int _v){sz = 1, ls = rs = 0, val = _v, pri = rand();}
}tr[N];
int n, rt, tot;
inline int new_Node(int v)
{
	tr[++tot] = Node(v);
	return tot;
}
inline void update(int x){tr[x].sz = tr[tr[x].ls].sz + tr[tr[x].rs].sz + 1;}
inline int merge(int x, int y)
{
	if(!x || !y)	return x + y;
	int ret;
	if(tr[x].pri < tr[y].pri)	ret = x, tr[x].rs = merge(tr[x].rs, y);
	else ret = y, tr[y].ls = merge(x, tr[y].ls);
	update(ret);
	return ret;
}
inline void split(int cur, int k, int &x, int &y)
{
	if(!cur)	x = y = 0;
	else
	{
		if(tr[cur].val <= k)	x = cur, split(tr[cur].rs, k, tr[cur].rs, y);
		else y = cur, split(tr[cur].ls, k, x, tr[cur].ls);
		update(cur);
	}
}
inline int kth(int x, int k)
{
	if(k > tr[x].sz)	k = tr[x].sz;
	while(1)
	{
		if(k <= tr[tr[x].ls].sz)	x = tr[x].ls;
		else if(k == tr[tr[x].ls].sz + 1)	return x;
		else k -= (tr[tr[x].ls].sz + 1), x = tr[x].rs;
	}
}
int main()
{
	freopen("treap.in", "r", stdin);
	freopen("treap.out", "w", stdout);
	srand(time(NULL));
	for(int q = read(); q; --q)
	{
		int op = read(), v = read(), x, y, z;
		if(op == 1) //insert
		{
			split(rt, v, x, y);
			rt = merge(merge(x, new_Node(v)), y);
		}
		else if(op == 2) //delete
		{
			split(rt, v, x, z);
			split(x, v - 1, x, y);
			rt = merge(merge(x, merge(tr[y].ls, tr[y].rs)), z);
		}
		else if(op == 3) //rank
		{
			split(rt, v - 1, x, y);
			printf("%d\n", tr[x].sz + 1);
			rt = merge(x, y);
		}
		else if(op == 4) //kth
			printf("%d\n", tr[kth(rt, v)].val);
		else if(op == 5) //precursor
		{
			split(rt, v - 1, x, y);
			printf("%d\n", tr[kth(x, tr[x].sz)].val);
			rt = merge(x, y);
		}
		else //succesor
		{
			split(rt, v, x, y);
			printf("%d\n", tr[kth(y, 1)].val);
			rt = merge(x, y);
		}
	}
	return 0;
}

后记

虽然本人怠惰, 但是Splay 不能不学, 这就去学Splay.

posted @ 2021-01-21 10:27  Martin_MHT  阅读(180)  评论(0)    收藏  举报