平衡树这种东西,我只会splay。splay比较好理解,并且好打,操作方便。

我以前学过SBT,但并不是很理解,所以就忘了怎么打了。

许多用平衡树的问题其实可以用线段树来解决,我们真正打平衡树的时候一般都是维护序列之类的。

维护序列时,splay特别方便,所以一般情况下打splay就好了。其它的平衡树也可以,可是如果见到翻转操作的时候,那些平衡树就会崩(至少我不知道那些平衡树有什么可以翻转的做法)。

但是splay的常数很大(常数最大的平衡树),并且有的时候需要可持久化。

这个时候就要用到非旋Treap来代替splay了。

但有的时候不能代替,比如LCT,据说只有splay才能让它达到lg⁡\lglg级别,用非旋Treap会变成lg⁡2\lg^2lg2。(splaysplaysplay常数大得要死,所以或许也没关系吧)

话说splay和LCT的博客我都没打,懒得打了


Treap?

Treap=Tree+Heap

Treap上每个节点有两个值(key,val)(key,val)(key,val),keykeykey满足BST的性质,valvalval满足堆的性质

valvalval的取值是个随机数,所以说Treap的复杂度是期望的。

Treap有带旋Treap和非旋Treap,带旋Treap似乎没什么卵用,所以直接说非旋Treap。


非旋Treap

(下面的Treap以普通平衡树为例子,和维护序列的Treap不一样)

核心操作

sqlit(x,p)sqlit(x,p)sqlit(x,p):将以xxx为根的树的前ppp个与后面的分开,形成两棵树,返回两棵树的根节点(有序)。

merge(a,b)merge(a,b)merge(a,b):将以aaa和以bbb为根的子树合并,返回合并后的根节点。(保证aaa子树中所有)

sqlit(x,p)sqlit(x,p)sqlit(x,p)

判断第ppp个点在xxx的左边还是右边,然后递归下去。

回溯的时候在左边或者右边接上。

具体见代码:

inline pair<Node*,Node*> sqlit(Node *t,int p){
pair<Node*,Node*> res(null,null);
if (t==null)
return res;
if (p<=t->l->siz){
res=sqlit(t->l,p);
t->l=res.second;
t->update();
res.second=t;
}
else{
res=sqlit(t->r,p-t->l->siz-1);
t->r=res.first;
t->update();
res.first=t;
}
return res;
}

由于每次ttt必定往下递归,所以时间是lg⁡\lglg级别的。

merge(a,b)merge(a,b)merge(a,b)

首先判断如果一个为空,就返回另一个。

接下来比较aaa和bbb的valvalval的大小,选取小的那个作为根,然后将儿子与另一个进行合并。

具体见代码:

inline Node *merge(Node *a,Node *b){
if (a==null)
return b;
if (b==null)
return a;
if (a->val<b->val){
a->r=merge(a->r,b);
a->update();
return a;
}
b->l=merge(a,b->l);
b->update();
return b;
}

由于每次aaa或bbb有一个要向下走,所以时间也是lg⁡\lglg级别的。

其它操作

其它操作就比较简单了。

rank(t,key)rank(t,key)rank(t,key):找出keykeykey的位置(小于它的数的个数+1+1+1)。

insert(t,key)insert(t,key)insert(t,key):插入keykeykey。

通过rankrankrank找出它要插入的位置。

把前面和后面分开,将其插在中间,三个合并起来。

remove(t,key)remove(t,key)remove(t,key):删除keykeykey。。

通过rankrankrank找出它的位置。

把前面、它、后面分开,然后前面和后面合并。

kth(t,k)kth(t,k)kth(t,k):找第kkk小

pred(t,key)pred(t,key)pred(t,key):找keykeykey的前驱

succ(t,key)succ(t,key)succ(t,key):找keykeykey的后继

容易发现这些都是在lg⁡\lglg级别的时间内完成的。

buildbuildbuild

这个操作或许有点特殊,可以用笛卡尔树实现,时间是线性的(当keykeykey有序的时候)。

笛卡尔树是什么自己上网查去

考虑到我们有其它操作,所以时间还是O(nlg⁡n)O(n\lg n)O(nlgn)级别的,所以这样建树费码量还没有多大意义,还不如一个一个插入,时间O(nlg⁡n)O(n\lg n)O(nlgn)。

但有的时候常数折磨人,所以还是说一下吧。

建立笛卡尔树的时候,我们维护一个栈,表示最右边的那一条链。

栈底到栈顶的valvalval值递增。

将新的节点插在栈顶的右儿子处,但这可能会破坏堆的性质,所以不行。

不断弹栈,直到栈顶小于这个点valvalval。将点插在它的右儿子处,它原来的右儿子变成这个点的左儿子。

区间操作

首先,在维护区间的时候,所谓的keykeykey实际上并不存在,它具体指它们在平衡树中的位置(排名)。

区间操作的大体思想和splay是差不多的,就是截取一段区间,将它们集中在一棵子树里面,然后进行各种操作。

我们可以通过sqlitsqlitsqlit将这个区间分离出来,然后打上一个标记,再放回去。

当然标记会下传。

这样的时间复杂度显然是lg⁡\lglg级别的。

用这种思想来搞区间操作就可以做到翻转操作。如果用别的平衡树,可以在每个节点上维护它子树的信息,在计算的时候像线段树那样合并。可是这样搞不了翻转啊!

代码

题目是洛谷上的普通平衡树。

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
#include <ctime>
#define N 100010
#define irand (rand()*32768+rand())
int n;
struct Node{
int key,val;
Node *l,*r;
int siz;
inline void update(){
siz=l->siz+r->siz+1;
}
} d[N];
Node *null=d;
inline pair<Node*,Node*> sqlit(Node *t,int p){
pair<Node*,Node*> res(null,null);
if (t==null)
return res;
if (p<=t->l->siz){
res=sqlit(t->l,p);
t->l=res.second;
t->update();
res.second=t;
}
else{
res=sqlit(t->r,p-t->l->siz-1);
t->r=res.first;
t->update();
res.first=t;
}
return res;
}
inline Node *merge(Node *a,Node *b){
if (a==null)
return b;
if (b==null)
return a;
if (a->val<b->val){
a->r=merge(a->r,b);
a->update();
return a;
}
b->l=merge(a,b->l);
b->update();
return b;
}
int rank(Node *t,int key){
if (t==null)
return 1;
if (key<=t->key)
return rank(t->l,key);
return t->l->siz+1+rank(t->r,key);
}
Node *kth(Node *t,int k){
if (k<=t->l->siz)
return kth(t->l,k);
if (k>t->l->siz+1)
return kth(t->r,k-t->l->siz-1);
return t;
}
Node *pred(Node *t,Node *res,int key){
if (t==null)
return res;
if (t->key<key)
return pred(t->r,t,key);
return pred(t->l,res,key);
}
Node *succ(Node *t,Node *res,int key){
if (t==null)
return res;
if (t->key>key)
return succ(t->l,t,key);
return succ(t->r,res,key);
}
int cnt;
Node *root;
int main(){
srand(time(0));
null->val=2147483647;
root=null;
scanf("%d",&n);
for (int i=1;i<=n;++i){
int op,x;
scanf("%d%d",&op,&x);
if (op==1){
int k=rank(root,x);
pair<Node*,Node*> a=sqlit(root,k-1);
d[++cnt]={x,irand,null,null,1};
root=merge(merge(a.first,&d[cnt]),a.second);
}
else if (op==2){
int k=rank(root,x);
pair<Node*,Node*> a=sqlit(root,k-1),b=sqlit(a.second,1);
root=merge(a.first,b.second);
}
else if (op==3)
printf("%d\n",rank(root,x));
else if (op==4)
printf("%d\n",kth(root,x)->key);
else if (op==5)
printf("%d\n",pred(root,null,x)->key);
else
printf("%d\n",succ(root,null,x)->key);
}
return 0;
}

很短是不是?


可持久化

可以参考一下上面的标程,我们发现那些节点不需要记录父亲。

这就是可持久化的特征啊!就像可持久化线段树一样,每个点都不需要记录父亲,在修改的时候把修改变成新建,然后新建之后照样把儿子指针连过去。

这就是非旋Treap最伟大的地方:可持久化!

在非旋Treap上面改一改,把所有的修改操作改为新建操作,就可以了。

然后你就会发现所需要的空间特别大……

以下是代码,为洛谷上的可持久化文艺平衡树:

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
#include <ctime>
#define N 200010
#define irand (rand()*32768+rand())
int n;
struct Node{
int val,v;
Node *c[2];
int siz;
long long sum;
bool rev;
inline void update(){
sum=c[0]->sum+c[1]->sum+v;
siz=c[0]->siz+c[1]->siz+1;
}
inline void pushdown();
} d[N*100];
Node *null;
int cnt;
#define clone(t) (&(d[++cnt]=*(t)))
inline void Node::pushdown(){
if (rev){
if (c[0]!=null)
c[0]=clone(c[0]);
if (c[1]!=null)
c[1]=clone(c[1]);
swap(c[0],c[1]);
c[0]->rev^=1;
c[1]->rev^=1;
rev=0;
}
}
pair<Node*,Node*> sqlit(Node *t,int k){
pair<Node*,Node*> res(null,null);
if (t==null)
return res;
t->pushdown();
if (k<=t->c[0]->siz){
res=sqlit(t->c[0],k);
Node *newt=clone(t);
newt->c[0]=res.second;
newt->update();
res.second=newt;
}
else{
res=sqlit(t->c[1],k-t->c[0]->siz-1);
Node *newt=clone(t);
newt->c[1]=res.first;
newt->update();
res.first=newt;
}
return res;
}
Node *merge(Node *a,Node *b){
if (a==null)
return b;
if (b==null)
return a;
if (a->val<b->val){
a->pushdown();
Node *newa=clone(a);
newa->c[1]=merge(a->c[1],b);
newa->update();
return newa;
}
b->pushdown();
Node *newb=clone(b);
newb->c[0]=merge(a,b->c[0]);
newb->update();
return newb;
}
Node *root[N+1];
int main(){
srand(time(0));
null=d;
null->val=2147483647;
root[0]=null;
scanf("%d",&n);
long long lastans=0;
for (int i=1;i<=n;++i){
int pre,op;
scanf("%d%d",&pre,&op);
if (op==1){
int p,x;
scanf("%d%d",&p,&x),p^=lastans,x^=lastans;
Node *t=&(d[++cnt]={irand,x,null,null,1,x,0});
pair<Node*,Node*> a=sqlit(root[pre],p);
root[i]=merge(merge(a.first,t),a.second);
}
else if (op==2){
int p;
scanf("%d",&p),p^=lastans;
pair<Node*,Node*> a=sqlit(root[pre],p-1),b=sqlit(a.second,1);
root[i]=merge(a.first,b.second);
}
else if (op==3){
int l,r;
scanf("%d%d",&l,&r),l^=lastans,r^=lastans;
pair<Node*,Node*> a=sqlit(root[pre],l-1),b=sqlit(a.second,r-l+1);
b.first->rev^=1;
root[i]=merge(a.first,merge(b.first,b.second));
}
else{
int l,r;
scanf("%d%d",&l,&r),l^=lastans,r^=lastans;
pair<Node*,Node*> a=sqlit(root[pre],l-1),b=sqlit(a.second,r-l+1);
printf("%lld\n",lastans=b.first->sum);
root[i]=root[pre];//很容易发现合并回去和之前是一模一样的,所以直接用之前那个。
}
}
return 0;
}

说实在的,这个程序的常数特别大。

在洛谷上跑得贼慢,而且还有点听天由命的味道……因为Treap是随机的,运气不好就会相差个几千毫秒。

无用的空间特别多,比如,插入操作中,从原来的树中拆出两棵树,然后合并。在上面的程序中,这两棵拆出的树也会被保存,然而我们只需要保存合并之后的树就好了。如何解决?其实可以在拆出来的时候用新建的方法,然后在合并的时候就不用新建了,像普通的非旋Treap一样。这样子可以大大地节省空间,应该也能节省时间。

只不过,不想打了啊!都已经快3000多byte了……

然后就是一个值得深思的问题,可不可以做到不下传翻转标记?上面的程序中,下传标记就要暴力地新开节点,消耗很多空间。我们能不能用标记永久化之类的思想来搞一下?我一开始就是那样打的,可不知道为什么错了,也许是我自己的方法有漏洞。

还有,询问的时候,我考虑过使用类似于线段树的操作,每个节点都可以表示成一个区间,询问区间就是几个节点的答案合并起来,显然节点的个数是lg⁡\lglg级别的。然后我打了,本来以为会快,结果……更慢了。

我觉得我很有必要重新学习一下卡常数。

非旋Treap及其可持久化的更多相关文章

  1. [模板] 平衡树: Splay, 非旋Treap, 替罪羊树

    简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...

  2. 平衡树简单教程及模板(splay, 替罪羊树, 非旋treap)

    原文链接https://www.cnblogs.com/zhouzhendong/p/Balanced-Binary-Tree.html 注意是简单教程,不是入门教程. splay 1. 旋转: 假设 ...

  3. 非旋 treap 结构体数组版(无指针)详解,有图有真相

    非旋  $treap$ (FHQ treap)的简单入门 前置技能 建议在掌握普通 treap 以及 左偏堆(也就是可并堆)食用本blog 原理 以随机数维护平衡,使树高期望为logn级别, FHQ  ...

  4. 2827: 千山鸟飞绝 非旋treap

    国际惯例的题面:看起来很不可做的样子,我们先来整理一下题意吧.就是,维护每个点曾经拥有过的最大的两个属性值,支持把点的位置移动.我们用map对每个位置进行离散化,对每个位置建立一个平衡树.为了方便分离 ...

  5. 2081.09.22 Kuma(非旋treap)

    描述 有N张卡片,编号从0到n-1, 刚开始从0到n-1按顺序排好. 现有一个操作, 对于p. l,表示从第p张卡片之后的l张卡片拿到 最前面. 例如n=7的时候, 刚开始卡片序列为0 1 2 3 4 ...

  6. 2018.08.27 rollcall(非旋treap)

    描述 初始有一个空集,依次插入N个数Ai.有M次询问Bj,表示询问第Bj个数加入集合后的排名为j的数是多少 输入 第一行是两个整数N,M 接下来一行有N个整数,Ai 接下来一行有M个整数Bj,保证数据 ...

  7. 2018.08.06 bzoj1500: [NOI2005]维修数列(非旋treap)

    传送门 平衡树好题. 我仍然是用的fhqtreap,感觉速度还行. 维护也比线段树splay什么的写起来简单. %%%非旋treap大法好. 代码: #include<bits/stdc++.h ...

  8. 2018.08.05 bzoj3223: Tyvj 1729 文艺平衡树(非旋treap)

    传送门 经典的平衡树问题,之前已经用splay写过一次了,今天我突发奇想,写了一发非旋treap的版本,发现挺好写的(虽然跑不过splay). 代码: #include<bits/stdc++. ...

  9. 2018.07.24 loj#107. 维护全序集(非旋treap)

    传送门 就是普通平衡树,可以拿来练非旋treap" role="presentation" style="position: relative;"&g ...

随机推荐

  1. c++-文件分离

    实现文件分离 1.头文件中不要使用using namespace,由于c++编译的特性,由于初学还没深入了解,不做具体编译的解释 2.由于没有了命名空间,所以string定义要写成std::strin ...

  2. hexo next主题深度优化(四),自定义一个share功能,share.js。

    文章目录 背景: 开始: 引入资源: 代码 关键的一步 附:方便学习的小demo 一次成功后还出现上面的bug 结束 2018.12.23发现bug(读者可忽略) 个人博客:https://mmmmm ...

  3. HDU1285-确定比赛名次-拓扑排序板子题

    有N个比赛队(1<=N<=500),编号依次为1,2,3,....,N进行比赛,比赛结束后,裁判委员会要将所有参赛队伍从前往后依次排名,但现在裁判委员会不能直接获得每个队的比赛成绩,只知道 ...

  4. day 91 Django学习之django自带的contentType表

      Django学习之django自带的contentType表   通过django的contentType表来搞定一个表里面有多个外键的简单处理: 摘自:https://blog.csdn.net ...

  5. vue多文件上传进度条 进度不更新问题

    转自 hhttp://www.cnblogs.com/muge10/p/6767493.html 感谢这位兄弟的文章,之前因为这个问题 ,我连续在sgmentflow上提问过多次,完全没人能回答.谢谢 ...

  6. startup_stm32f10x_xx.s 启动代码文件选择

    引用http://blog.csdn.net/gasbi/article/details/7545568 整体感觉stm32给的库文件太琐碎了,正如它的芯片型号一样繁多,例如启动文件: 网上查到的各个 ...

  7. FaceNet pre-trained模型以及FaceNet源码使用方法和讲解

    Pre-trained models Model name LFW accuracy Training dataset Architecture 20180408-102900 0.9905 CASI ...

  8. protobuf文档翻译-安装,数据格式及编码规范

    Install Download protobuf: https://github.com/protocolbuffers/protobuf/releases unzip protoc-3.8.0-l ...

  9. 【JZOJ3422】水叮当的舞步

    description 水叮当得到了一块五颜六色的格子形地毯作为生日礼物,更加特别的是,地毯上格子的颜色还能随着踩踏而改变. 为了讨好她的偶像虹猫,水叮当决定在地毯上跳一支轻盈的舞来卖萌~~~ 地毯上 ...

  10. 把云数据库带回家!阿里云发布POLARDB Box数据库一体机

    9月26日,2019杭州云栖大会上,阿里云宣布正式推出高性能数据库一体机——POLARDB Box,用户部署在自有数据中心即可享受云数据库的便捷体验,同时还为Oracle等传统数据库用户提供一键迁移功 ...