无旋转Treap是一个神奇的数据结构,能够支持插入,删除,查询k大,查询某个数的排名,查询前驱后继,支持各种区间操作和持久化。基于旋转的Treap无法实现区间反转等操作,但是无旋Treap可以轻易地支持区间操作。那为什么区间操作不用Splay而要去学无旋转Treap?原因很简单,Splay的时间复杂度是均摊的不能可持久化,而且无旋转Treap的代码量少得起飞(明明是yyf的Splay太丑了),而且无旋转Treap不容易写(翻)挂(车)。

节点

  对于节点,我们通常情况下需要维护它的子树大小、随机优先级和键值(简单地说就是排序的关键字)。

  由于没有旋转操作,所以不用维护父节点指针(终于可以把这个可恶的家伙扔掉了,这样至少少了10句特判),只用维护左右儿子指针就好了。

 typedef class TreapNode {
public:
int val;
int pri;
int s;
TreapNode *l, *r; TreapNode():val() { }
TreapNode(int val):val(val), pri(rand()), s() { } void maintain() {
s = ;
if(l != NULL) s += l->s;
if(r != NULL) s += r->s;
}
}TreapNode;

  为了偷懒,我通常还会在之前加上3句话:

 #define pnn pair<TreapNode*, TreapNode*>
#define fi first
#define sc second

分裂与合并

  无旋转Treap几乎一切操作都是基于这两个操作。

  首先来说说分裂操作

  分裂通常分为按权值分裂(权值小于等于x的拆成一堆,剩余的拆成另一堆),或者按排名拆分(前k大拆成一堆,其余的拆成一堆)。

  很快就会出现疑问,没有像Splay一样的伸展操作,如何保证分裂出来的两堆各是一颗Treap?

  虽然不能保证,但是,可以在分裂的过程中对零散的子树进行合并,最后保证两堆各是一颗Treap。

  假设当前考虑到点node。我们现在只需要考虑node会和左子树一起拆成一堆还是和右子树一起拆成一堆。

  现在考虑递归处理不和node拆成一堆的那颗子树。将它拆分完成后返回一个pair型变量 (lrt, rrt) ,其中 lrt 表示拆分后较小一堆的根节点和 rrt 边拆分后较大的一堆的根节点。

  为了更好地说明如何合并,还是来举个例子。

  我们现在要把Treap拆成权值小于等于x的两颗树,现在递归到节点node,很不幸,它的权值小于x,所以它和左子树应该被加入较小的一堆,然后我们要把权值小于等于x都拆出来,所以去递归它的右子树并返回了 (lrt, rrt) 。

  现在就将node的右子树赋值为lrt,然后将lrt设为node。

  对于另一种情况和按排名拆分同理。

  记得在分裂的过程中维护子树大小。

 pnn split(TreapNode* node, int val) {
if(node == NULL) return pnn(NULL, NULL);
pnn rt;
if(node->val <= val) {
rt = split(node->r, val);
node->r = rt.fi, rt.fi = node;
} else {
rt = split(node->l, val);
node->l = rt.sc, rt.sc = node;
}
node->maintain();
return rt;
}

  然后来考虑合并操作

请记住这个合并操作的前提是其中一颗Treap上的所有键值小于另一颗Treap上的键值,否则你只能启发式合并了。

毕竟它比可并堆多了二叉查找树的性质的限制。

  在学习无旋转Treap合并操作之前,先来看看左偏树的合并

  现在来效仿左偏树的合并。假设现在维护的是大根堆。加入现在考虑的两颗子树的根节点分别为a和b。

  那么考虑谁的随机优先级大,谁留在当前位置,然后把其中一颗子树(至于是哪颗要根据大小来判断)和另一颗树合并,递归处理。

  注意合并后返回一个根节点,要把它和当前的点连接好,然后维护子树大小。

 TreapNode* merge(TreapNode* a, TreapNode* b) {
if(a == NULL) return b;
if(b == NULL) return a;
if(a->pri >= b->pri) {
a->r = merge(a->r, b), a->maintain();
return a;
}
b->l = merge(a, b->l), b->maintain();
return b;
}

插入与删除

  对于插入操作。假设要在Treap内插入一个键值为x的点。

  首先给它分配随机优先级。

  然后按权值将Treap拆成键值小于等于x和大于x的两堆。

  然后将插入的这个点,看成独立的一颗Treap,分别和这两颗树合并。

 void insert(int x) {
TreapNode* pn = newnode();
pn->val = x;
pnn pa = split(rt, x);
pa.fi = merge(pa.fi, pn);
rt = merge(pa.fi, pa.sc);
}

  对于删除操作。假设要在Treap内删除一个键值为x的点。

  根据上面的套路,然后按权值将Treap拆成键值小于等于x和大于x的两堆,然后再按照小于等于x - 1把前者拆成两堆,假设这三个Treap分别为\(T_{1}\),\(T_{2}\)和\(T_{3}\)。

  显然\(T_{2}\)中的点的键值全是x,那么将\(T_{2}\)的左右子树合并,然后再和\(T_{1}\)和\(T_{3}\)合并,然后我们就成功删掉了一个键值为x的点。

 void remove(int x) {
pnn pa = split(rt, x);
pnn pb = split(pa.fi, x - );
pb.sc = merge(pb.sc->l, pb.sc->r);
pb.fi = merge(pb.fi, pb.sc);
rt = merge(pb.fi, pa.sc);
}

其他操作

  名次查询

    考虑统计树中有多少个键值比查询的x小。答案是这个个数加1。

    如何统计?假装要去查找这个数,如果找到了就递归左子树,每次访问右子树时计算当前点和它的左子树对答案的贡献。

 int rank(int x) {
TreapNode* p = rt;
int rs = ;
while(p) {
int ls = (p->l) ? (p->l->s) : ();
if(x > p->val) rs += ls + , p = p->r;
else p = p->l;
}
return rs + ;
}

  第k小值查询

    考虑要在当前子树内查询第k小值,然后递归处理,边界就是第k小值刚好是当前点或者访问到空节点(k小值不存在)

    (代码详见完整代码)

  各种前驱后继

    和有序序列二分查找是一样的,只是二分变到了二叉搜索树上。

    (代码详见完整代码)

建树

  什么?建树?不是一个数一个数地往里插吗?

  假如给定一个已经从小到大排好序的序列让你建树,这么做显然没有利用性质。

Solution 1 笛卡尔树式建树

  考虑首先给每个点附一个随机优先级。

  然后用单调栈维护最右链(现在约定它是指从根节点,一直访问右节点直到空节点形成的一条链):

  考虑在最右链末端插入下一个点,这样可能会导致出现一些点破坏堆的性质,所以我们向上找到第一个使得没有破坏堆的性质的点(由于没有记录父节点,实质上就是单调然暴力回溯),然后将它的右子树变为插入点的左子树然后将它的右子树变为插入点。

  这是一个构造一个数组\(\{a_{i} = i\}\)的Treap的代码: 

 TreapNode *now, *p;
for(int i = , x; i <= n; i++) {
p = newnode();
p->val = i;
now = NULL;
while(!s.empty() && s.top()->pri <= p->pri) {
now = s.top();
s.pop();
}
if(!s.empty())
s.top()->r = p;
p->l = now;
s.push(p);
}
p = NULL;
while(!s.empty()) now = s.top(), now->r = p, p = now, s.pop();
tr.rt = now;

  由于用这个方法在构造时不好维护子树的大小,所以要维护子树的大小还需要写一个后序遍历:

 void travel(TreapNode *p) {
if(!p) return;
travel(p->l);
travel(p->r);
p->maintain();
}

  这个方法的主旨在于装逼,没有什么特别大的作用,因为下面有个很简(智)单(障)的方法就可以完成

Solution 2 替罪羊式建树

  首先可以参考替罪羊树的建树方法

  然后我们考虑如何让它满足大根堆的性质?

  就让子节点的随机优先级等于父节点的随机优先级减去某个数。

  或者维护小根堆,让子节点的随机优先级等于父节点的加上某个数。

  上文提到的某个数是可以rand的。

  虽然感觉这么做会导致一些小问题(比如某个节点一定在根),不过应该可以忽略。

  感谢Doggu提供了这个方法

完整代码

 /**
* bzoj
* Problem#3224
* Accepted
* Time: 528ms
* Memory: 5204k
*/
#include <bits/stdc++.h>
using namespace std;
typedef bool boolean;
const signed int inf = (signed) (~0u >> );
#define pnn pair<TreapNode*, TreapNode*>
#define fi first
#define sc second typedef class TreapNode {
public:
int val;
int pri;
int s;
TreapNode *l, *r; TreapNode():val() { }
TreapNode(int val):val(val), pri(rand()), s() { } void maintain() {
s = ;
if(l != NULL) s += l->s;
if(r != NULL) s += r->s;
}
}TreapNode; #define Limit 200000
TreapNode pool[Limit];
TreapNode* top = pool;
TreapNode* newnode() {
top->l = top->r = NULL;
top->s = ;
top->pri = rand();
return top++;
} typedef class Treap {
public:
TreapNode* rt; Treap():rt(NULL) {} pnn split(TreapNode* node, int val) {
if(node == NULL) return pnn(NULL, NULL);
pnn rt;
if(node->val <= val) {
rt = split(node->r, val);
node->r = rt.fi, rt.fi = node;
} else {
rt = split(node->l, val);
node->l = rt.sc, rt.sc = node;
}
node->maintain();
return rt;
} TreapNode* merge(TreapNode* a, TreapNode* b) {
if(a == NULL) return b;
if(b == NULL) return a;
if(a->pri >= b->pri) {
a->r = merge(a->r, b), a->maintain();
return a;
}
b->l = merge(a, b->l), b->maintain();
return b;
} TreapNode* find(int val) {
TreapNode* p = rt;
while(p != NULL && val != p->val) {
if(val < p->val) p = p->l;
else p = p->r;
}
return p;
} void insert(int x) {
TreapNode* pn = newnode();
pn->val = x;
pnn pa = split(rt, x);
pa.fi = merge(pa.fi, pn);
rt = merge(pa.fi, pa.sc);
} void remove(int x) {
pnn pa = split(rt, x);
pnn pb = split(pa.fi, x - );
pb.sc = merge(pb.sc->l, pb.sc->r);
pb.fi = merge(pb.fi, pb.sc);
rt = merge(pb.fi, pa.sc);
} int rank(int x) {
TreapNode* p = rt;
int rs = ;
while(p) {
int ls = (p->l) ? (p->l->s) : ();
if(x > p->val) rs += ls + , p = p->r;
else p = p->l;
}
return rs + ;
} int getkth(int r) {
TreapNode* p = rt;
while(r) {
int ls = (p->l) ? (p->l->s) : ();
if(r == ls + ) return p->val;
if(r > ls) r -= ls + , p = p->r;
else p = p->l;
}
return p->val;
} int getPre(int x) {
TreapNode* p = rt;
int rs = -inf;
while(p) {
if(p->val < x && p->val > rs) rs = p->val;
if(x <= p->val) p = p->l;
else p = p->r;
}
return rs;
} int getSuf(int x) {
TreapNode* p = rt;
int rs = inf;
while(p) {
if(p->val > x && p->val < rs) rs = p->val;
if(x < p->val) p = p->l;
else p = p->r;
}
return rs;
} void debug(TreapNode* p) {
if(!p) return;
cerr << "(" << p->val << "," << p->pri << ")" << "{";
debug(p->l);
cerr << ",";
debug(p->r);
cerr << "}";
}
}Treap; int m;
Treap tr; inline void init() {
scanf("%d", &m);
} inline void solve() {
int opt, x;
while(m--) {
scanf("%d%d", &opt, &x);
switch(opt) {
case :
tr.insert(x);
// tr.debug(tr.rt);
break;
case :
tr.remove(x);
break;
case :
printf("%d\n", tr.rank(x));
break;
case :
printf("%d\n", tr.getkth(x));
break;
case :
printf("%d\n", tr.getPre(x));
break;
case :
printf("%d\n", tr.getSuf(x));
break;
}
}
} int main() {
srand(233u);
init();
solve();
return ;
}

bzoj 3224

无旋转Treap简介的更多相关文章

  1. 【数据结构】【平衡树】无旋转treap

    最近在研究平衡树,看起来这种东西又丧水又很深,感觉很难搞清楚.在Ditoly学长的建议下,我先学习了正常的treap,个人感觉这应该是平衡树当中比较好懂的而且比较好写的一种. 然而,发现带旋treap ...

  2. Treap + 无旋转Treap 学习笔记

    普通的Treap模板 今天自己实现成功 /* * @Author: chenkexing * @Date: 2019-08-02 20:30:39 * @Last Modified by: chenk ...

  3. 浅谈无旋treap(fhq_treap)

    一.简介 无旋Treap(fhq_treap),是一种不用旋转的treap,其代码复杂度不高,应用范围广(能代替普通treap和splay的所有功能),是一种极其强大的平衡树. 无旋Treap是一个叫 ...

  4. [转载]无旋treap:从好奇到入门(例题:bzoj3224 普通平衡树)

    转载自ZZH大佬,原文:http://www.cnblogs.com/LadyLex/p/7182491.html 今天我们来学习一种新的数据结构:无旋treap.它和splay一样支持区间操作,和t ...

  5. [您有新的未分配科技点]无旋treap:从好奇到入门(例题:bzoj3224 普通平衡树)

    今天我们来学习一种新的数据结构:无旋treap.它和splay一样支持区间操作,和treap一样简单易懂,同时还支持可持久化. 无旋treap的节点定义和treap一样,都要同时满足树性质和堆性质,我 ...

  6. 【算法学习】Fhq-Treap(无旋Treap)

    Treap——大名鼎鼎的随机二叉查找树,以优异的性能和简单的实现在OIer们中广泛流传. 这篇blog介绍一种不需要旋转操作来维护的Treap,即无旋Treap,也称Fhq-Treap. 它的巧妙之处 ...

  7. 无旋treap的简单思想以及模板

    因为学了treap,不想弃坑去学splay,终于理解了无旋treap... 好像普通treap没卵用...(再次大雾) 简单说一下思想免得以后忘记.普通treap因为带旋转操作似乎没卵用,而无旋tre ...

  8. 【bzoj3224】Tyvj 1728 普通平衡树 01Trie姿势+平衡树的四种姿势 :splay,旋转Treap,非旋转Treap,替罪羊树

    直接上代码 正所谓 人傻自带大常数 平衡树的几种姿势:  AVL Red&Black_Tree 码量爆炸,不常用:SBT 出于各种原因,不常用. 常用: Treap 旋转 基于旋转操作和随机数 ...

  9. 无旋Treap - BZOJ1014火星人 & 可持久化版文艺平衡树

    !前置技能&概念! 二叉搜索树 一棵二叉树,对于任意子树,满足左子树中的任意节点对应元素小于根的对应元素,右子树中的任意节点对应元素大于根对应元素.换言之,就是满足中序遍历为依次访问节点对应元 ...

随机推荐

  1. echarts实现全国地图

    1.首先我没有按需引入echarts,我是全局引入的,所以说在node_modules中有 这个china,你只需要在你的页面引入即可 但是按需引入echarts 的 项目中node_modules中 ...

  2. mysql 命令一套

    MySQL mysql -h主机地址 -u用户名 -p用户密码 首先打开DOS窗口,然后进入目录mysql\bin,再键入命令mysql -u root  -p,回车后提示你输密码.注意用户名前可以有 ...

  3. SQL Server 创建索引(index)

    索引的简介: 索引分为聚集索引和非聚集索引,数据库中的索引类似于一本书的目录,在一本书中通过目录可以快速找到你想要的信息,而不需要读完全书. 索引主要目的是提高了SQL Server系统的性能,加快数 ...

  4. oracle中实现自增id

    在一些数据库(例如mysql)中,实现自增id只要在建表的时候指定一下即可, 但是在oracle中要借助sequence来实现自增id, 要用上自增id,有几种方式: 1.直接在insert语句中使用 ...

  5. OWASP top 10

    OWASP Top 10 A1: InjectionSolution+Validate User Input+Never concatenate queries and date+Parameteri ...

  6. python读取大文件

    最近在学习python的过程中接触到了python对文件的读取.python读取文件一般情况是利用open()函数以及read()函数来完成: f = open(filename,'r') f.rea ...

  7. <6>Cocos Creator​​​​​​​调试

    高手在于调试,下面来谈Cocos Creator调试! 1. 网页平台调试 调试常见的三种形式为调试打印.运行时报错与断点调试,这里主要利用编辑器"VS Code"与"C ...

  8. Vue系列之 => 动画

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. linux命令-查找所有文件中包含某个字符串

    查找目录下的所有文件中是否含有某个字符串 find .|xargs grep -ri "IBM" 查找目录下的所有文件中是否含有某个字符串,并且只打印出文件名 find .|xar ...

  10. 51Nod 1069 Nim游戏 (位运算)

    题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1069 有N堆石子.A B两个人轮流拿,A先拿.每次只能从一堆 ...