非旋转Treap:用运行时间换调试时间的有效手段

    Hello大家好,我们今天来聊一聊非旋转Treap

  相信各位或多或少都做过些序列上的问题。如果水题我们考虑暴力;不强制在线我们可能用过莫队和待修改莫队;不更改序列上的时间戳信息的我们使用线段树或者树状数组,也有可能请出主席树。那如果更大幅度的操作,我们要用到splay或者块链。但是!我们今天介绍一种特殊的平衡树,用时间换取时间的数据结构。

  啥意思?如果写过splay和块链,前者的调试时间和后者的敲版子时间让我们头痛欲裂,但是这个平衡树,好写好调,省去了时间。但是常数相对较大,这便是我们的代价(好一个时间换时间)。

  Treap就是Tree+heap,一个基于堆的二叉搜索树,但是它的维护过程是仰仗于旋转的。而我们今天讲的非旋转Treap,顾名思义,不需要旋转的哦。

  那应该如何维护平衡呢?用两个基本操作即可:撕裂&合并

  撕裂??考虑区间最本质的:如果我们可以将要修改的区间成为一棵单独的树,我们是不是想干什么就干什么!整体操作就打标记,特殊操作就特殊处理。我们也想要这样怎么办?引入一个split操作,就是断开原来BST上的一条边,整个BST变成了两棵树,返回一个pair,pair里存的是两棵新BST的根。如果想要提取一个区间,就split两次,中间的子树就是我们要的区间子树。 附上版子(为了方便理解,我每个语句都是一行):

// split函数的pair存的是两个新BST的两个根,时间戳小的在first,时间戳大的在second
pair split(int pos,int num) //表示在以pos为根节点的BST中,取出前num个作为新的BST,剩下的作为另一棵BST
{
if(num==0)//递归退出条件,如果在一个pos的BST取出前0个,我们就返回一个(0,pos)的pair。
{
return make_pair(0,pos);
}
int lson=a[pos].ls;
int rson=a[pos].rs;
// 这里我们分类讨论
if(num==a[lson].size) // 如果恰好是左子树的大小,我们将左子树断开。
{
a[pos].ls=0;//将左子树和根节点之间的路径断开
pushup(pos);//更新当前根节点信息。
return make_pair(lson,pos);//返回两个新子树的pair,这里返回的是lson而不是a[pos].ls是因为后者已经被更新成0.
}
// 后面的同理
else if(num==a[lson].size+1)
{
a[pos].rs=0;
pushup(pos);
return make_pair(pos,rson);
}
else if(num<a[lson].size)
{
Pair t=split(lson,num);
a[pos].ls=t.second;
pushup(pos);
return make_pair(t.first,pos);
}
else
{
// 这里我们需要注意:如果num>a[lson].size+1,说明我们分开的两棵子树中有一棵包含了pos的左子树和根节点。
Pair t=split(rson,num-a[lson].size-1); // 所以这里我们直接将右子树分成num-a[lson].size-1的两个子树。
a[pos].rs=t.first; // 然后将num-a[lson].size-1大小的子树和前面的树加在一起,这样就得到了大小为num的子树。
pushup(pos);
return make_pair(pos,t.second);
}
}

  合并??如果我们将一棵单独的表示区间的BST处理完了,我们需要将两棵子树合并到一起啊!所以就有了这个merge操作。因为我们本质上维护平衡的办法就是利用随机数的权值来维护一个依据权值的堆,撕裂操作显然不会打破这个局势,但是如果我们瞎合并,就无法保证平衡,时间复杂度也就没有办法保证。所以我们想一个能维护两个子树的方法,即能保证堆的性质,也保留BST应该有的中序遍历时间戳递增。我们请出可并堆!显然,如果我们采用可并堆的合并方式,是可以达到我们预期的效果的。

  可并堆是如何合并的呢?其实也非常简单。这样:因为我们是想维护基于随机权值val的堆!所以对于两棵BST,分别以x和y为根。其中,x的整体时间戳小于y。那么,如果val[x]大于val[y],那么x一定是他们两棵BST合并之后的根。因为val[x]是x所在子树里最大的,y是y所在子树里最大的,x还比y大,所以x是根。又因为,我们要维护Tree,所以我们递归地将x的右儿子和y合并即可。这样合并时候的新BST就满足我们的要求。 附上板子!

// merge 操作返回的是两棵BST合并后的新BST的根。
int merge(int x,int y)
{
if(x==0||y==0)
{
if(x==0) return y;
else return x;
}
// 如果x的权值大于y的权值,显然x是根。我们只需要让x的右儿子和y合并即可。
if(a[x].val_heap>a[y].val_heap)
{
a[x].rs=merge(a[x].rs,y);
pushup(x);
return x;
}
// 反之,如果y的权值大于x,显然y是根。所以我们让x和y的左儿子合并即可。
else
{
a[y].ls=merge(x,a[y].ls);
pushup(y);
return y;
}
}

  说完了非旋转Treap的基本操作,相信各位对于这个神奇的平衡树有了一定的认识。它的其他操作有了split和merge后都变得非常简单,自己手玩即可。

  例题时间

  Bzoj 3223 文艺平衡树 (所有平衡树的入门题)

  这道题非常经典,我们记录一下标记,每次merge和split之前pushdown一下即可。 趁着这个题看一下其他的操作哦。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 100100
#define mp make_pair
using namespace std;
typedef pair<int,int> par;
int ls[N],rs[N],size[N],key[N],val[N];
int n,m,root,tot;
bool lazy[N];
inline void update(int x)
{
size[x]=1;
if(ls[x])size[x]+=size[ls[x]];
if(rs[x])size[x]+=size[rs[x]];
}
inline void pushdown(int x)
{
if(!x||!lazy[x])return;
swap(ls[x],rs[x]);lazy[x]=0;
if(ls[x])lazy[ls[x]]^=1;
if(rs[x])lazy[rs[x]]^=1;
}
int lson,rson;
par split(int x,int k)
{
pushdown(x);
if(!k) return mp(0,x);
lson=ls[x],rson=rs[x];
par t;
if(k==size[ls[x]])
{
ls[x]=0;update(x);
return mp(lson,x);
}
else if(k==size[ls[x]]+1)
{
rs[x]=0;update(x);
return mp(x,rson);
}
else if(k<size[ls[x]])
{
t=split(lson,k);
ls[x]=t.second;update(x);
return mp(t.first,x);
}
t=split(rson,k-size[ls[x]]-1);
rs[x]=t.first;update(x);
return mp(x,t.second);
}
int merge(int x,int y)
{
if(!x||!y) return x|y;
pushdown(x);pushdown(y);
if(key[x]<key[y])
{
ls[y]=merge(x,ls[y]);update(y);
return y;
}
rs[x]=merge(rs[x],y);update(x);
return x;
}
void output(int x,int flag)
{
pushdown(x);
if(ls[x]) output(ls[x],0);
if(!rs[x]&&flag) printf("%d",val[x]);
else printf("%d ",val[x]);
if(rs[x])output(rs[x],flag);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
key[++tot]=rand();
val[tot]=i;
size[tot]=1;
root=merge(root,tot);
}
par t1,t2;
for(int x,y,i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
t2=split(root,y);t1=split(t2.first,x-1);
lazy[t1.second]^=1;
root=merge(merge(t1.first,t1.second),t2.second);
}
output(root,1);
return 0;
}

  Bzoj 1861 书架

  多了两个get_rank和get_id的操作。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 80010
#define mp make_pair
using namespace std;
typedef pair<int,int> par;
int n,m,root;
int ls[N],rs[N],size[N],key[N],fa[N];
inline void pushup(int x)
{
size[x]=size[ls[x]]+size[rs[x]]+1;
}
int merge(int x,int y)
{
if(!x||!y) return x|y;
if(key[x]>key[y])
{
rs[x]=merge(rs[x],y);fa[rs[x]]=x;
pushup(x); return x;
}
ls[y]=merge(x,ls[y]);fa[ls[y]]=y;
pushup(y); return y;
}
par split(int x,int k)
{
if(!k) return mp(0,x);
int lson=ls[x],rson=rs[x];
if(k==size[lson])
{
ls[x]=0;pushup(x);
fa[lson]=0;
return mp(lson,x);
}
if(k==size[lson]+1)
{
rs[x]=0;pushup(x);
fa[rson]=0;
return mp(x,rson);
}
if(k<size[lson])
{
par t=split(lson,k);
ls[x]=t.second;fa[ls[x]]=x;pushup(x);
return mp(t.first,x);
}
par t=split(rson,k-size[ls[x]]-1);
rs[x]=t.first;fa[rs[x]]=x;pushup(x);
return mp(x,t.second);
}
int getid(int x)
{
int flag=1,t=x,ans=0;
while(t)
{
if(flag)ans+=size[ls[t]]+1;
flag=(t==rs[fa[t]]);
t=fa[t];
}
return ans-1;
}
int find(int x)
{
x--;int t=root;
while(1)
{
if(x==size[ls[t]]) return t;
if(x<size[ls[t]]) t=ls[t];
else x-=size[ls[t]]+1,t=rs[t];
}
}
int main()
{
cin >> n >> m ;
for(int x,i=1;i<=n;i++)
{
scanf("%d",&x);
size[x]=1,key[x]=rand()*rand();
root=merge(root,x);
}
char flag[10];
for(int x,y,i=1;i<=m;i++)
{
scanf("%s",flag);
if(flag[0]=='T')
{
scanf("%d",&x);
int id=getid(x);
par t2=split(root,id+1),t1=split(t2.first,id);
root=merge(t1.second,merge(t1.first,t2.second));
}
else if(flag[0]=='B')
{
scanf("%d",&x);
int id=getid(x);
par t2=split(root,id+1),t1=split(t2.first,id);
root=merge(merge(t1.first,t2.second),t1.second);
}
else if(flag[0]=='I')
{
scanf("%d%d",&x,&y);
if(!y)continue;
if(y==-1)
{
int id=getid(x);
par t3=split(root,id+1),t2=split(t3.first,id),t1=split(t2.first,id-1);
root=merge(merge(merge(t1.first,t2.second),t1.second),t3.second);
}
else
{
int id=getid(x);
par t3=split(root,id+2),t2=split(t3.first,id+1),t1=split(t2.first,id);
root=merge(merge(merge(t1.first,t2.second),t1.second),t3.second);
}
}
else if(flag[0]=='A')
{
scanf("%d",&x);
printf("%d\n",getid(x));
}
else
{
scanf("%d",&x);
printf("%d\n",find(x));
}
}
return 0;
}

  小结:好啦,非旋转Treap到这里,基本就说完了。这个平衡树好写,不用好调。常数虽然相较其他平衡树(除了BST)较大,但是时间复杂度是没有问题的。相信用这个fhq大神发明的神奇平衡树能让各位感受到序列的友好吧! 有问题直接评论哦!

非旋转Treap:用运行时间换调试时间的有效手段的更多相关文章

  1. BZOJ1014 JSOI2008 火星人prefix 【非旋转Treap】*

    BZOJ1014 JSOI2008 火星人prefix Description 火星人最近研究了一种操作:求一个字串两个后缀的公共前缀.比方说,有这样一个字符串:madamimadam,我们将这个字符 ...

  2. [bzoj3196][Tyvj1730]二逼平衡树_树套树_位置线段树套非旋转Treap/树状数组套主席树/权值线段树套位置线段树

    二逼平衡树 bzoj-3196 Tyvj-1730 题目大意:请写出一个维护序列的数据结构支持:查询给定权值排名:查询区间k小值:单点修改:查询区间内定值前驱:查询区间内定值后继. 注释:$1\le ...

  3. [bzoj3173]最长上升子序列_非旋转Treap

    最长上升子序列 bzoj-3173 题目大意:有1-n,n个数,第i次操作是将i加入到原有序列中制定的位置,后查询当前序列中最长上升子序列长度. 注释:1<=n<=10,000,开始序列为 ...

  4. 关于非旋转treap的学习

    非旋转treap的操作基于split和merge操作,其余操作和普通平衡树一样,复杂度保证方式与旋转treap差不多,都是基于一个随机的参数,这样构出的树树高为\(logn\) split 作用:将原 ...

  5. [Codeforces702F]T-Shirts——非旋转treap+贪心

    题目链接: Codeforces702F 题目大意:有$n$种T恤,每种有一个价格$c_{i}$和品质$q_{i}$且每种数量无限.现在有$m$个人,第$i$个人有$v_{i}$元,每人每次会买他能买 ...

  6. BZOJ5063旅游——非旋转treap

    题目描述 小奇成功打开了大科学家的电脑. 大科学家打算前往n处景点旅游,他用一个序列来维护它们之间的顺序.初 始时,序列为1,2,...,n. 接着,大科学家进行m次操作来打乱顺序.每次操作有6步: ...

  7. BZOJ3223文艺平衡树——非旋转treap

    此为平衡树系列第二道:文艺平衡树您需要写一种数据结构,来维护一个有序数列,其中需要提供以下操作: 翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 ...

  8. BZOJ3224普通平衡树——非旋转treap

    题目: 此为平衡树系列第一道:普通平衡树您需要写一种数据结构,来维护一些数,其中需要提供以下操作:1. 插入x数2. 删除x数(若有多个相同的数,因只删除一个)3. 查询x数的排名(若有多个相同的数, ...

  9. [NOIP]2017列队——旋转treap/非旋转treap

    Sylvia 是一个热爱学习的女孩子.  前段时间,Sylvia 参加了学校的军训.众所周知,军训的时候需要站方阵. Sylvia所在的方阵中有n × m名学生,方阵的行数为 n,列数为m.  为了便 ...

随机推荐

  1. html中常见符号的代码表示

    HTML中空格的集中代码表示: HTML中空格   不断行的空白(1个字符宽度)     半个空白(1个字符宽度)     一个空白(2个字符宽度)     窄空白(小于1个字符宽度)   其他常见的 ...

  2. shell脚本,配置文件加载顺序,以及什么时候加载。

    在linux系统中,有/etc/profile,/etc/bashrc ,~/.bash_profile,~/bashrc这四个配置文件,这些文件,会自动的在某些时候加载,也就是点一下,一般都是些别名 ...

  3. HDU-1548-奇怪的电梯

    这题的题意就是,如果在k层,该数的序号为k,则在k层上只能去k+a[k]层或者k-a[k],这样的话,就变成了一个单向联通图,对这个Dijkstra算法就可以了. #include <cstdi ...

  4. java代码生成二维码

    java代码生成二维码一般步骤 常用的是Google的Zxing来生成二维码,生成的一般步骤如下: 一.下载zxing-core的jar包: 二.需要创建一个MatrixToImageWriter类, ...

  5. C语言之链接库

    链接库是windows的术语,但对于Linux来说,其概念是一样的.我们通常会把一些相似或相近功能的程序生成链接库,这样的好处是: 1)便于共享,开发软件时如需要相同功能时,不需要将大量重复的代码整合 ...

  6. Python Importlib模块与__import__详解

    Importlib模块与__import__都可以通过过字符串来导入另外一个模块,但在用法上和本质上都有很大的不同. 以一个例子为证: 以下为我的工程目录结构: lib/test.py: name = ...

  7. [LoadRunner]LR性能测试结果样例分析

    R性能测试结果样例分析 测试结果分析 LoadRunner性能测试结果分析是个复杂的过程,通常可以从结果摘要.并发数.平均事务响应时间.每秒点击数.业务成功率.系统资源.网页细分图.Web服务器资源. ...

  8. intellij idea 17 log4j 中文乱码

    先是在intellij idea里设置没有得到解决, 然后在tomcat的server.xml里设置没有得到解决, 再然后在log4j配置文件里配置没有得到解决. 以下是解决方案. C:\Progra ...

  9. [uiautomator篇] bluetooth---接口来做

    package com.softwinner.performance.frameratetest; import android.Manifest; import android.bluetooth. ...

  10. 2017 Wuhan University Programming Contest (Online Round) D. Events,线段树区间更新+最值查询!

    D. Events 线段树区间更新查询区间历史最小值,看似很简单的题意写了两天才写出来. 题意:n个数,Q次操作,每次操作对一个区间[l,r]的数同时加上C,然后输出这段区间的历史最小值. 思路:在线 ...