简单写一写,以免忘记
FHQ-treap是一种神奇的数据结构,支持很多操作。包括但不限于查排名,删数,查找前驱后继
首先我们来学习FHQ-treap的前置知识:二叉搜索树(BST)
二叉搜索树
二叉搜索树显然是一棵二叉树,但这个二叉树有特殊性质:他的左子树中所有点权值均小于他,他的右子树中所有点的权值都大于他(大部分时候,二叉搜索树一个节点中会存储若干个一样权值的数)
我们可以发现,二叉搜索树本身就可以支持很多操作。之后FHQ-treap的很多操作也和二叉搜索树的性质息息相关。这样的操作理想情况下一次只需要花费 $O(logn)$ 的时间
但二叉搜索树极易脱离理想状态。最简单的状况,将数从小到大依次插入,这时树会退化成一条链。
为了克服这个确定,前辈们发明了众多算法。较为知名的有splay,treap,替罪羊树,fhq-treap。
(博主曾经写过一篇splay的总结,但她神秘地消失在了博主的电脑里)
所以我们来学习FHQ-treap
FHQ-treap
FHQ是一位大佬的名字,treap是一种数据结构
根据题目我们就知道,这个数据结构是FHQ大佬对treap的魔改。
先来了解一下treap的核心思想:treap给每个节点都赋了一个额外的随机权值,使这棵树在原有权值满足二叉搜索树的性质的同时,新增权值满足堆的性质(小根堆大根堆无所谓,反正值是随机赋的。但我们这里采用小根堆)
看起来这似乎很不靠谱,因为跟随机化有关。那么我来告诉你一个方法:打开明日方舟,往池子里扔两个十连(一个十连会保五星),如果第二个十连不是十个三星的话
你就可以继续学下去了
讲真,这个东西被卡的概率实在太小了。要所有随机的值都达到一种精妙的平衡,使树的深度比logn大很多。这时数据还要正好卡在这一条长链上,出题人还不能故意卡你,因为你赋的值是随机的
当然,卡treap的方法还是有的,只要出题人献祭双亲造出能使treap的稍大常数与splay区分开来的数据,那么就把treap卡掉了。但双亲还是很宝贵的,所以没多少人这么做
treap有什么问题呢?
当然是有的,最大的一个问题就是难写。左右旋分分钟让人丧失对生活的信心,虽然说很模块化但理解起来也很费劲啊。而且treap和splay的一个大区别是splay的旋转包含在splay操作中了,实际上单独使用旋转的操作并不多。但treap又不一样
那么FHQ大佬就不乐意了,于是造出来了FHQ-treap。
FHQ-treap用分裂与合并两个小清新操作代替了繁琐的旋转。我们来一次看看:
分裂操作
常见的分裂操作有两种,按权值分裂与按排名分裂。我们先讲按权值分裂
我们传进去原来的根t,与用于分裂的权值k。我们现在想要小于等于k的权值放一棵树里,大于k的放另一棵树里。
现在我们到了一个节点p,那么我们分类讨论一下
- val[p] <= k
这种时候p连同她的左子树肯定都小于等于k,都要归到小于等于k的树里。
那么她的右子树呢?我们是不知道的。所以要向右子树遍历。左子树就不用管了
那么我们也可以推出p在新树里的位置了。她是之前节点的右子树的一部分,这就意味着p此时权值为这棵树中的最大值。直接放到最右边的儿子上就行了。
- val[p] > k
em...这个就不用讲了吧,把前几行左右互换,最大值变成最小值就行了
在构造新树时,我们有一个小寄巧:把函数设成这个样子split(int now, int k, int &x, int &y),这样如果我们使用split(son[now]1, k, sonnow, y)的话,sonnow的值是会随x变化的。
然后我们可以构造出以下代码:
void split(int now, int k, int &x, int &y)
{
if(now == 0)
{
x = y = 0;
return ;
}
if(val[now] <= k)
{
x = now;//这样子就把now新树中的父亲的右儿子设为了now
split(s[now][1], k, s[now][1], y);//为什么这里变成了s[now][1]了呢?因为这时now已经是新树中的一个节点了,她的左子树会跟着她一起到新树中。此时新树中now的右儿子还不能确定是哪个节点。所以在下一次有节点加入新树时执行上一行的操作就行了
//其实我觉得先看第二行再看第一行好理解些。。。
}
else
{
y = now;
split(s[now][0], k, x, s[now][0]);
}
push_up(now);
}
用了这个技巧,在构造新树时左右儿子的设置就变简单许多了~
排名操作与之类似,将k与左儿子子树节点个数相比,只不过如果k大于左子树的节点个数就要减去个数再减1(减去自己)再遍历右子树
void split(int now, int k, int &x, int &y)
{
if(now == 0)
{
x = y = 0;
return ;
}
if(lt[now] == 1) push_down(now);
if(si[s[now][0]] < k)
{
x = now;
split(s[now][1], k - si[s[now][0]] - 1, s[now][1], y);
}
else
{
y = now;
split(s[now][0], k, x, s[now][0]);
}
push_up(now);
}
合并
我们既然要合并,首先就要先分裂。这样产生的两个新树,其中一个树节点的最大值必定小于另一个树节点的最小值。设小树中合并到的节点为x,大树中合并到的节点为y.显然,根据随机的权值大小,一共只有两种情况,一种合并后y在x的右子树中,一种合并后x在y的左子树中
为什么我不说y是x的右儿子呢?万一x原来的右儿子也小于y就不满足堆的性质了(假设我们写的是小根堆)
所以我们还要把y与原来的x的右儿子作比较,然后返回现在x的右儿子
代码很简单:
int merge(int x, int y)
{
if(x * y == 0) return x + y;//当只有x,y至少有一个为0时,说明只有一个子树。直接返回那个子树的根节点(如果都为0则返回0,不影响)
if(num[x] > num[y])//num为随机权值
{
s[x][1] = merge(s[x][1], y);
push_up(x);
return x;
}
else
{
s[y][0] = merge(x, s[y][0]);
push_up(y);
return y;
}
}
讲完这两个操作,就能开始倒腾平衡树操作了~
插入
设插入的数权值为k
把原树按k分裂,然后新建一个节点,把这三东西合并就行了
删除
设要删除的数为k,
把原树按k - 1分裂,再把k~max的那颗树按k分裂。这时我们得到了一颗权值全为k的树。把这棵树的左右儿子合并,根节点丢掉不管,然后再把这三东西合一起就行了
依数查排名
设给的数为k
把原树按k - 1分裂,k的排名即为小的树的大小+1
记得合并回去
依排名查数
设给的排名为k,
从根节点开始遍历,设当前节点为u,如果k等于u的左子树大小+1,那么说明k代表的就是根节点
如果小于,直接跳到左儿子
如果大于,将u -= (左子树大小+1)再跳到右儿子
很好理解
查前驱
设给的数为k
按u - 1分裂,找小的树的最大值(一直往右儿子跳,直到没有右儿子)
查后继
设给的数为k
按u分裂,找大的树的最小值(一直往右儿子跳,直到没有右儿子)
注意,以上操作,一旦合并,根就要更新
例题:P3369 普通平衡树
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int root;
int cnt, val[maxn], s[maxn][2], num[maxn], si[maxn];//val为题目给的权值,num为随机取的权值
void push_up(int u)
{
si[u] = 1;
if(s[u][0]) si[u] += si[s[u][0]];
if(s[u][1]) si[u] += si[s[u][1]];
}
void split(int now, int k, int &x, int &y)
{
if(now == 0)
{
x = y = 0;
return ;
}
if(val[now] <= k)
{
x = now;
split(s[now][1], k, s[now][1], y);
}
else
{
y = now;
split(s[now][0], k, x, s[now][0]);
}
push_up(now);
}
int merge(int x, int y)
{
if(x * y == 0) return x + y;
if(num[x] > num[y])
{
s[x][1] = merge(s[x][1], y);
push_up(x);
return x;
}
else
{
s[y][0] = merge(x, s[y][0]);
push_up(y);
return y;
}
}
int make_new(int v)
{
val[++ cnt] = v;
num[cnt] = rand();
si[cnt] = 1;
return cnt;
}
void ins(int u)
{
int x = 0, y = 0;
split(root, u - 1, x, y);
root = merge(merge(x, make_new(u)), y);
}
void del(int u)
{
int x = 0, y = 0, z = 0;
split(root, u - 1, x, y);
split(y, u, y, z);
y = merge(s[y][0], s[y][1]);
root = merge(x, merge(y, z));
}
int find_rank(int u)
{
int x = 0, y = 0;
split(root, u - 1, x, y);
int ans = si[x] + 1;
root = merge(x, y);
return ans;
}
int find_num(int x)
{
int u = root;
while(1)
{
if(si[s[u][0]] + 1 == x) break;
if(si[s[u][0]] + 1 > x) u = s[u][0];
else x -= (si[s[u][0]] + 1), u = s[u][1];
}
return val[u];
}
int find_res(int u)
{
int x = 0, y = 0;
split(root, u - 1, x, y);
int ans = x;
while(s[ans][1]) ans = s[ans][1];
ans = val[ans];
root = merge(x, y);
return ans;
}
int find_pre(int u)
{
int x = 0, y = 0;
split(root, u, x, y);
int ans = y;
while(s[ans][0]) ans = s[ans][0];
ans = val[ans];
root = merge(x, y);
return ans;
}
int main(){
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
int t;
cin >> t;
int opt, x;
while(t --)
{
cin >> opt >> x;
if(opt == 1) ins(x);
if(opt == 2) del(x);
if(opt == 3) cout << find_rank(x) << endl;
if(opt == 4) cout << find_num(x) << endl;
if(opt == 5) cout << find_res(x) << endl;
if(opt == 6) cout << find_pre(x) << endl;
}
return 0;
}
fhq-treap的区间操作
典型例题:P3391 文艺平衡树
设要操作的区间为l~r
没有权值了,我们按排名分裂。将1~(l - 1)项,l~r项,r + 1 ~ n项分开,给l~r的根节点打上类似于线段树的懒标记即可。输出就是中序输出
懒标记什么时候下传呢?当我们需要用到儿子信息时就要下传了。所以分裂,合并,输出时都要下传一下
分裂时k减去的数一定要+1.警钟撅烂
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int root;
int cnt, val[maxn], s[maxn][2], num[maxn], si[maxn], lt[maxn];
void push_up(int u)
{
si[u] = 1;
if(s[u][0]) si[u] += si[s[u][0]];
if(s[u][1]) si[u] += si[s[u][1]];
}
void push_down(int u)
{
// cout << u << endl;
swap(s[u][0], s[u][1]);
if(s[u][0]) lt[s[u][0]] ^= 1;
if(s[u][1]) lt[s[u][1]] ^= 1;
lt[u] = 0;
}
void split(int now, int k, int &x, int &y)
{
if(now == 0)
{
x = y = 0;
return ;
}
if(lt[now] == 1) push_down(now);
if(si[s[now][0]] < k)
{
x = now;
split(s[now][1], k - si[s[now][0]] - 1, s[now][1], y);
}
else
{
y = now;
split(s[now][0], k, x, s[now][0]);
}
push_up(now);
}
int merge(int x, int y)
{
if(x * y == 0) return x + y;
if(num[x] > num[y])
{
if(lt[x]) push_down(x);
s[x][1] = merge(s[x][1], y);
push_up(x);
return x;
}
else
{
if(lt[y]) push_down(y);
s[y][0] = merge(x, s[y][0]);
push_up(y);
return y;
}
}
int make_new(int v)
{
val[++ cnt] = v;
num[cnt] = rand();
si[cnt] = 1;
return cnt;
}
void ins(int u)
{
root = merge(root, make_new(u));
}
void p(int u)
{
if(lt[u] == 1) push_down(u);
if(s[u][0]) p(s[u][0]);
cout << val[u] << ' ';
if(s[u][1]) p(s[u][1]);
}
int main(){
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
int n, m, l, r;
cin >> n >> m;
for(int i = 1;i <= n;i ++) ins(i);
while(m --)
{
cin >> l >> r;
int x, y, z;
split(root, l - 1, x, y);
split(y, r - l + 1, y, z);
lt[y] ^= 1;
root = merge(x, merge(y, z));
}
p(root);
return 0;
}
典型例题讲解
懒得写了,看心情补
最后的话
明日方舟里三星的出率是比四星还低的,如果你在bilibili上搜抽卡视频, 出3个六星的都比出10个三星的视频多。实际上博主只看到过一个10三星的视频
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。