[数据结构]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.
浙公网安备 33010602011771号