无旋转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. 从零开始一起学习SLAM | 相机成像模型

    上一篇文章<从零开始一起学习SLAM | 为啥需要李群与李代数?>以小白和师兄的对话展开,受到了很多读者的好评.本文继续采用对话的方式来学习一下相机成像模型,这个是SLAM中极其重要的内容 ...

  2. jQuery-手风琴效果-2

    动画 高级函数:基于底层函数又进行了封装 两大块:简化版的动画函数和万能动画函数 简化版动画函数 显示/隐藏$().show; $(...).hide(); 强调:无参数的show()/hide()使 ...

  3. 不用ajax实现异步请求:XmlHttpRequest 小记

    视图页面代码 控制器代码

  4. phpcs

    phpcs(代码规范) https://juejin.im/post/5b18fdeb6fb9a01e573c3cb3 https://laravel-china.org/docs/psr/psr-2 ...

  5. Yii Restful api认证

  6. Sitecore CMS中配置项目图标

    在Sitecore中,图标通常用于通过各种不同的模板类型快速区分项目.文章可能使用红色图标,而列表页面可能使用蓝色.项目上设置的图标可以在内容树中看到,也可以在选择项目时在内容编辑器的顶部看到. 从功 ...

  7. 如何登录Sitecore CMS

    这是关于学习如何使用和开发Sitecore CMS的系列文章中的第一篇. 在使用Sitecore CMS之前,必须先登录.新Sitecore开发人员常见的一个问题是“我该在哪里登录?” 安装任何版本的 ...

  8. 关于git上的一些错误信息

    如果输入$ Git remote add origin git@github.com:djqiang(github帐号名)/gitdemo(项目名).git 提示出错信息:fatal: remote ...

  9. 【Redis学习之十一】Java客户端实现redis集群操作

    客户端:jedis-2.7.2.jar 配置文件两种方式: properties: redis.cluster.nodes1=192.168.1.117 redis.cluster.port1=700 ...

  10. C/C++笔试题(基础题)

    为了便于温故而知新,特于此整理 C/C++ 方面相关面试题.分享,共勉. (备注:各题的重要程度与先后顺序无关.不断更新中......欢迎补充) (1)分析下面程序的输出(* 与 -- 运算符优先级问 ...