treap【来自蒟蒻的整理】
(最后更新时间2021/11/21 11:04:49,博客查询操作未写完,见谅)
蒟蒻yzh粉丝有点少,看文章前先点个关注呗,qwq,萌新的日常treap学习,技术欠缺,见谅
前言:
众所周知平衡树两大算法splay和treap,听某谷的大佬说treap要快 (其实是写起来快),所以我只学了treap 。
treap:“树堆” “Tree + Heap”
性质:每个点随机分配一个权值,使treap同时满足堆性质和二叉搜索树性质复杂度:期望O( logn )
作用:在logn的复杂度求出一个数的排名和排名为x的数或求一个数的前驱和后驱
其中会有一个 \(key\) 来维护treap的平衡
正片开始:
以下关于treap的常识借鉴lxl的课件
设每个节点的关键字是key,随机权值是rand
1.如果v是u的左儿子,则key[v] < key[u]
2.如果v是u的右儿子,则key[v] > key[u]
3.如果v是u的子节点,则rand[u] > rand[v]
Treap维护权值的时候一般会把相同的权值放在同一个节点上
• 所以一个treap节点需要维护以下信息:
• 左右儿子
• 关键字
• 关键字出现次数
• 堆随机值
• 节点大小(即子树大小)
显然lxl的课件是个人都读不懂(除非你学过treap)
接下来是我的理解:
首先我们针对一个数x,将这个数看做二叉树上的一个节点i它的关键字key为x,rand为一个随机数(为了维护treap的平衡性,后面会讲)。对于一个数列cnt[i]记录key[i]出现的次数,siz[i]记录以节点i为根的树的大小。
son[i][0]记录i节点的左子树根编号,son[i][1]记录i节点右子树的根节点编号。treap必须满足左子树中所有节点的key都小于当前树根的key,右子树中所有的key都大于当前树根的key。
对于一个数列中的数x,x的排名为小于它的数的个数+1,则对于treap上的一个节点i它的排名为上面的树+左子树大小(这个代码中详细,这里不做过多叙述),前驱,后驱后面详细讲。
好了treap大概的思路就说完了yzh表达能力又nb了
代码讲解部分:
注:treap中的一些操作关系到树根节点编号的改变请务必加上&符号
push_up:
首先对于任何维护的树中push_up不可或缺:
void push_up(int x) {
siz[x] = siz[son[x][0]] + siz[son[x][1]] + cnt[x];//以x节点为根的树的大小为左子树大小+右子树大小+当前key出现的次数
}
神奇的旋转:
然后就是对于treap的旋转操作:
许多oier很疑惑为什么要旋转?什么是旋转?
如下图
这是一个以4节点为目标节点进行的右旋操作,反之就是左旋操作,不难发现这么旋转过后treap的性质并没有改变,反而可以利用这种旋转操作来进行一些方便我们的操作
下面是代码:
void rotate(int& x, int y) {//x为要进行旋转的节点,y为1时右旋y为0时左旋
int ii = son[x][y ^ 1];
son[x][y ^ 1] = son[ii][y];
son[ii][y] = x;
push_up(x);
push_up(ii);
x = ii;
}
这段代码不用我过多解释了吧,多仔细考虑考虑就能懂,学treap的oier这些应该都能理解,有不明白的自行百度。
接下来是添加节点操作:
思路:按照treap中一个节点的左子树key都比当前key小,右子树key都比当前key大的规律找到一个合适的叶节点位置将当前要插入的x添加,然后按照上面说的各个变量进行复制,并生产一个随机数用来维护平衡(后面会讲)。
例如在下面这个图中插入一个key为0的节点:
我们从根节点按照规律往下找知道发现一个可以放的叶子节点空位(为1的左子树根)。
如下图:
那如果插入的数之前已经有过怎么办,这就好说了,如果数中已经存在要插入的树x,那么在按treap规律往下找节点的过程中必然会经过key为x的节点
这时只需要将当前节点cnt++就可以了(此思路比较简单),直接上代码:
注:在此代码中x为当前节点编号,y为要插入的数值,不要与上面的思路描述搞混
void ins(int& x, int y) {
if (!x) {//按照treap规律找到最低端符合条件的叶子节点
x = ++sz;
key[x] = y;
cnt[x] = siz[x] = 1;
rd[x] = rand();
return;
}
if (key[x] == y) {//若要插入的树以存在
cnt[x]++;
siz[x]++;
return;
}
int t = (y > key[x]);//若要插入的数大于当前key则从右子树找,反之
ins(son[x][t], y);
if (rd[x] > rd[son[x][t]])//随机数维护操作(看下面的解释)
rotate(x, t ^ 1);
push_up(x);
}
有些oier对于rd随机数的维护还是有点疑惑,那我就说一下我对于随机数维护的看法:
前面我们讲过对于treap中的任意一个节点进行旋转操作treap的性质都不会改变,然而在输入、删除和查询操作中频繁调用旋转操作,可能会导致treap变成一条链,复杂度单次会退化到 \(O(n)\),这样就不能称为平衡树了,所以我们这里就用随机数维护,认真观察的oier不难发现这里的维护是用的小顶堆维护(当然大顶堆也可以),因为随机数具有随机性所以这样维护起来的是完全没有问题的,这样就可以使treap保持基本稳定状态,不难想到退化到一条链的可能是n!,几乎不可能退化成链,不过毕竟是随机数,依旧改变不了弱平衡的事实,所以速度在log算法中也是非常慢的(也有可能是因为常数很大)。
(以上只是我对与随机数维护的个人看法,不一定对,若有疑问可百度或评论留言)
下面是lxl对ins的看法(有兴趣的可以看看,我不认为他讲的比我细 ):
-
先给这个节点分配一个随机的堆权值
-
然后把这个节点按照bst的规则插入到一个叶子上:
-
从根节点开始,逐个判断当前节点的值与插入值的大小关系。如
果插入值小于当前节点值,则递归至左儿子;大于则递归至右儿
子;
- 然后通过旋转来调整,使得treap满足堆性质
delete 删除操作:
这个和添加有点相似
其实就是按照treap的定义往下查找找到要删除的数后一直将这个节点用旋转操作一直往下探,直到这个节点为叶子节点时即可进行删除操作,这里往下探的时候用的是贪心策略,注意维护小顶堆。
有的人会问为什么要将这个节点旋转到叶子节点呢?
因为如果在中间进行旋转操作那么可能会导致中间的节点消失,从而造成树裂开的情况。
由于思路比较简单这里yzh就不做过多说明若有不懂的看参考lxl的思路:
• 和普通的BST删除一样:
• 如果删除值小于当前节点值,则递归至左儿子;大于则递归至右
儿子
• 若当前节点数值的出现次数大于 1 ,则减一(通常将同一个权
值缩掉)
• 若当前节点数值的出现次数等于 1 :
• 若当前节点没有左儿子与右儿子,则直接删除该节点(置 0);
• 若当前节点没有左儿子或右儿子,则将左儿子或右儿子替代该节
点;
• 若当前节点有左儿子与右儿子,则不断旋转当前节点,并走到当
前节点新的对应位置,直到没有左儿子或右儿子为止。
代码:
void del(int& x, int y) {
if (!x) {
return;
}
if (key[x] != y) {
del(son[x][y > key[x]], y);
}
else if (key[x] == y) {
if (!son[x][0] && !son[x][1]) {
cnt[x]--;
siz[x]--;
if (!cnt[x]) {
x = 0;
}
}
else if (son[x][0] && !son[x][1]) {
rotate(x, 1);
del(son[x][1], y);
}
else if (!son[x][0] && son[x][1]) {
rotate(x, 0);
del(son[x][0], y);
}
else {
int t = (rd[son[x][0]] < rd[son[x][1]]);//小顶堆维护
rotate(x, t);
del(son[x][t], y);
}
}
push_up(x);
}
这么详细的讲解再听不懂就得remake
注:以下任何查询操作中都没有更改根节点的操作所以不能再加&
查询操作lxl写的不详细所以就不再展示lxl的思路了
查询排名
常规先从treap的树根出发按照treap左边小右边大的规律往下找,直到找到需要查询的树时,返回排名的累计值,如果查到叶子节点还是没有找到要查询的数的话就更具题目具体要求输出(比如-1),重点在于怎么求累计值+1(一个数的排名是小于这个数的个数+1)。
当走到一个点i的时候若要查询的数x小于key[i]那么不累计并往左子树找x,若x大于key[i]那么将排名加上左子树的大小与cnt[i]的值(因为左子树的数一定都是小于x,key[i]也小于x,这个操作就是文章开头所说的i节点上面的树的大小)并往右边找。
此思路比较简单,直接上代码:
int get_rank(int x, int y) {
if (!x) return 0;
if (key[x] == y) {
return siz[son[x][0]] + 1;
}
if (key[x] < y) {
return siz[son[x][0]] + cnt[x] + get_rank(son[x][1], y);
}
return get_rank(son[x][0], y);
}
查询排名为x的数
这里查排名就不能按照常规了,查排名是按照排名累计值来找。
走到节点i时判断要查找的排名x是否<=siz[son[i][0]] 的值
若成立则证明排名为x的值在以i节点为根节点树的左子树
若不成立且siz[son[i][0]] + cnt[i] < x则证明在右子树,那么我们就将x减去siz[son[i][0]] + cnt[i]并往右子树找,这步减的操作是将要查询的x排名减去当前累计排名值(思考片刻遍能理解)
若发现上面两个条件都不满足,则确定siz[son[i][0]] < x <= siz[son[i][0]] +cnt[i],就证明排名为x的数就是key[i],直接返回key[i]
若按照以上规律找到叶子节点还是没找到排名为x的数,则不存在排名为x的数,此情况按照题目给定要求输出(如-1)
代码:
int find(int x, int y) {//x为当前节点编号,y表示查询排名为y的值,与上文有所不同
if (!x) return 0;
if (siz[son[x][0]] >= y) {
return find(son[x][0], y);
}
else if (siz[son[x][0]] + cnt[x] < y) {
return find(son[x][1], y - siz[son[x][0]] - cnt[x]);
}
else return key[x];
}
剩下的查询操作就比较简单了,自己看看就能读懂(其实是我懒得写了) 。
这里粘一道题
洛谷P3369 模版题
可对照模版体自行学习下面代码,日后有时间yzh会再进行补充
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#pragma warning(disable:4996)
using namespace std;
int T,son[100010][4],sz,cnt[100010],siz[100010],key[100010],rd[1000010],rt,tot;
void push_up(int x) {
siz[x] = siz[son[x][0]] + siz[son[x][1]] + cnt[x];//以x节点为根的树的大小为左子树大小+右子树大小+当前key出现的次数
}
void rotate(int& x, int y) {
int ii = son[x][y ^ 1];
son[x][y ^ 1] = son[ii][y];
son[ii][y] = x;
push_up(x);
push_up(ii);
x = ii;
}
void ins(int& x, int y) {
if (!x) {//按照treap规律找到最低端符合条件的叶子节点
x = ++sz;
key[x] = y;
cnt[x] = siz[x] = 1;
rd[x] = rand();
return;
}
if (key[x] == y) {//若要插入的树以存在
cnt[x]++;
siz[x]++;
return;
}
int t = (y > key[x]);//若要插入的数大于当前key则从右子树找,反之
ins(son[x][t], y);
if (rd[x] > rd[son[x][t]])//随机数维护操作
rotate(x, t ^ 1);
push_up(x);
}
void del(int& x, int y) {
if (!x) {
return;
}
if (key[x] != y) {
del(son[x][y > key[x]], y);
}
else if (key[x] == y) {
if (!son[x][0] && !son[x][1]) {
cnt[x]--;
siz[x]--;
if (!cnt[x]) {
x = 0;
}
}
else if (son[x][0] && !son[x][1]) {
rotate(x, 1);
del(son[x][1], y);
}
else if (!son[x][0] && son[x][1]) {
rotate(x, 0);
del(son[x][0], y);
}
else {
int t = (rd[son[x][0]] < rd[son[x][1]]);//小顶堆维护
rotate(x, t);
del(son[x][t], y);
}
}
push_up(x);
}
int get_rank(int x, int y) {
if (!x) return 0;
if (key[x] == y) {
return siz[son[x][0]] + 1;
}
if (key[x] < y) {
return siz[son[x][0]] + cnt[x] + get_rank(son[x][1], y);
}
return get_rank(son[x][0], y);
}
int find(int x, int y) {
if (!x) return 0;
if (siz[son[x][0]] >= y) {
return find(son[x][0], y);
}
else if (siz[son[x][0]] + cnt[x] < y) {
return find(son[x][1], y - siz[son[x][0]] - cnt[x]);
}
else return key[x];
}
int pre(int x, int y) {
if (!x) return -0x3f3f3f3f;
if (key[x] >= y) {
return pre(son[x][0], y);
}
else {
return max(key[x], pre(son[x][1], y));
}
}
int nxt(int x, int y) {
if (!x) return 0x3f3f3f3f;
if (key[x] <= y) {
return nxt(son[x][1], y);
}
else {
return min(key[x], nxt(son[x][0], y));
}
}
int main() {
scanf("%d", &T);
while (T--) {
int opt, v;
scanf("%d%d", &opt, &v);
if (opt == 1) {
ins(rt, v);
}
else if (opt == 2) {
del(rt, v);
}
else if (opt == 3) {
printf("%d\n", get_rank(rt, v));
}
else if (opt == 4) {
printf("%d\n", find(rt, v));
}
else if(opt==5){
printf("%d\n", pre(rt, v));
}
else if (opt == 6) {
printf("%d\n", nxt(rt, v));
}
}
return 0;
}