这是一个全新的数据结构

md,别看这篇文章了,这篇已经废了。

百折不饶,再交一次,更新复杂度证明

这里是HYF,蒟蒻一只,最近因某些原因开始学数据结构了,然后就写了这篇题解。


下面给大家介绍一个全新的数据结构,暂且称作IST(Immortal segment tree),你们也可以称作YYC Segment Tree(像FHQ_Treap 一样)

本人是这数据结构的第一弹使用者,对这个数据结构起到几乎没用的作用。这篇题解也算是这个数据结构的一个预告,给广大OIer了解一下这个数据结构。

由于我们学校信息学团实在是太菜了(除了YYC巨佬,就是他发明了这个数据结构;还有ZLH巨佬,他初二LCT)所以这个数据结构还没有被完全研究透彻,只能做一些比较正常的操作,例如:

1、P3391 【模板】文艺平衡树

2、P5055 【模板】可持久化文艺平衡树

3、全部线段树

4、全部平衡树,额,只不过这毕竟是线段树,所以用来写平衡树的操作有点复杂,这个数据结构更偏向于像文艺平衡树的区间树。

而且这个数据结构有几个十分优秀的特点:

1、它思想简单!!!你只需要会线段树就可以了。

2、实现简单,代码不超过100行

3、跑得飞快,不卡常大概300多ms。主要还是因为才刚发明,没有特殊数据去卡。

4、易于可持久化,额,我还没有掌握,反正就是很简单很简单啦,具体可以看YYC巨佬的题解

(其实这个数据结构并没有怎么强大)

YYC Segment Tree简介

辣么,下面给大家介绍YYC Segment Tree的基本思想。

由名可知,这个数据结构是基于线段树进行改造的,作为一个线段树,他却可以做一些平衡树的操作,这究竟是为什么呢?

因为他虽然是一个线段树,但是却基于区间划分,也就是分裂和合并操作。但是悲伤的是,经过区间划分后,这个线段树就没有那么平衡了,不过他还是有很多线段树的性质。所以我们还是可以当线段树来写的QAQ。

有很多人(至少有一个审题解的管理员)认为这是一个FHQ_Treap的改造版。这也是这篇题解第一次被拒了的原因,我有点不开心,因为这和FHQ_Treap根本是两回事,虽然IST也是基于区间划分。但是我们把IST建好树后会发现,真正有用的也就是最底下的那一层节点,他们存的是真实数据,而其他的只不过是虚节点,存的只是存储了标记与上传的数据。而FHQ_Treap每一个点都存的是实节点,存的是实在的数据,所以它是平衡树,而IST却是一棵线段树。

IST长这样!!!:

操作介绍

1、分裂

把整颗树分成三个部分,左,中,右,最后中部分就是我们要操作的部分。例如这道题的旋转操作,我们像线段树一样递归下去,如果要此时的区间完全在要操作的内,那么就分到中区间;如果在完全在要操作的区间左部,那就分到左区间;右区间同理。

最后把分裂出的节点删掉,因为那已经没用了,删掉的节点放入垃圾回收,以便于下次可以重复利用该节点,节省空间。

看下代码吧:

  1. void spilt(int node,int begin,int end,int x,int y){
  2. if(end<x){Left[++le]=node;return;} //如果查询区间在要修改区间左部,并入左数组
  3. if(x<=begin&&end<=y){middle[++mi]=node;return;} //如果查询区间在要修改区间中,并入中数组
  4. if(y<begin){Right[++ri]=node;return;} //如果查询区间在要修改区间右部,并入右数组
  5. pushdown(node); //pushdown更新tag标记
  6. int mid=begin+tree[tree[node].l].size-1; //注意,因为经过修改后的树不像线段树那么平衡,所以mid值有所变动
  7. spilt(tree[node].l,begin,mid,x,y); //向左子树递归分裂
  8. spilt(tree[node].r,mid+1,end,x,y); //向左子树递归分裂
  9. rub[++tot]=node; //这里是垃圾回收
  10. }

2、合并操作

我们先把分裂好的三个部分:左,中,右.合并在一个数组里,直接三个for赋值就可以了。像这样:

  1. int cnt=0;
  2. for(int i=1;i<=le;++i)t[++cnt]=Left[i];
  3. for(int i=1;i<=mi;++i)t[++cnt]=middle[i];
  4. for(int i=1;i<=ri;++i)t[++cnt]=Right[i];

然后我们就对合并好的数组进行操作,就是把他们两两合并,每一次新建一个节点存他们合并后的数据,然后把这个新节点的左儿子和右儿子分别指向要合并的两个节点,最后再pushup就可以了。

我觉得这里可能大家会比较难理解,其实就是一个重新建树的一个过程(我们也可以理解为一个石子合并的过程,只不过是相邻两个合并而已)。下面看看完整代码比较好理解:

  1. void marge(){ //合并
  2. int cnt=0; //记得数组清0,要按顺序合并哦
  3. for(int i=1;i<=le;++i)t[++cnt]=Left[i]; //把左部分合并
  4. for(int i=1;i<=mi;++i)t[++cnt]=middle[i]; //把中部分合并
  5. for(int i=1;i<=ri;++i)t[++cnt]=Right[i]; //把右部分合并
  6. while(cnt>1){ //重新建树过程
  7. int mid=(cnt+1)/2; //取mid值
  8. for(int i=1;i<=mid;++i){ //将线段树底部节点一一合并
  9. int x=t[i*2-1],y=t[i*2];
  10. if(!x||!y)t[i]=x+y; //如果有一个是空,特判
  11. else{
  12. int node=New(); //新建一个节点
  13. tree[node].l=x;tree[node].r=y; //新节点指儿子
  14. pushup(node); //pushup
  15. t[i]=node; //把新节点并入数组里
  16. }
  17. }
  18. for(int i=mid+1;i<=cnt;++i)t[i]=0;//记得把剩下节点清0,原因是他们已经在上面合并过了,而且避免了奇偶的错误
  19. cnt=(cnt+1)/2; //到下一层继续建树
  20. }root=t[1]; //根更新
  21. }

3、新建节点

在这里我们的垃圾回收就起到作用了,我们的垃圾会收就是把一些被删除的节点记在垃圾堆里,那么现在我们就可以把他们重新取出来,作为新节点的编号。嗯~,还有就是记得给新节点数据清0哦。

  1. int New(){
  2. int pos=rub[tot--];
  3. tree[pos].l=tree[pos].r=tree[pos].size=tree[pos].x=tree[pos].tag=0;
  4. return pos;
  5. }

细心的读者可能发现tree[node].size也清0了,这是为什么了,一个节点的大小怎么会是0呢?

因为它每一次新建是新建一个虚节点,虚节点刚开始是什么都没有的,只有在连向其他节点时才能有信息。

我们可以发现新建节点的操作只在合并出现,而新建节点后有个pushup操作……

4、建树

哎呀,这个操作和线段树差不多的啦,看注释吧:

  1. void build(int node,int l,int r){
  2. if(l==r){
  3. tree[node].x=l; //初值是i嘛
  4. tree[node].size=1; //树大小是1
  5. return;
  6. }int mid=(l+r)>>1; //和线段树一毛一样
  7. tree[node].l=++top; //新建左子树的编号
  8. build(top,l,mid); //递归左子树建树
  9. tree[node].r=++top; //新建右子树的编号
  10. build(top,mid+1,r); //递归右子树建树
  11. pushup(node); //pushup
  12. }

旋转

分三步,一分裂,二操作,三合并

主要讲一下操作部分

就是把中部分取出来,然后打标记,再翻过来,交换左右子树。

没了……

  1. void rotate(int x,int y){
  2. le=0,ri=0,mi=0; //记得左,中,右部分数组要清0
  3. spilt(root,1,tree[root].size,x,y); //分裂
  4. for(int i=1;i<=mi;++i)tmp[i]=middle[i]; //把中部分提取出来
  5. for(int i=1;i<=mi;++i){
  6. middle[i]=tmp[mi-i+1]; //翻过来,就是直接赋值嘛
  7. tree[middle[i]].tag^=1; //打标记
  8. swap(tree[middle[i]].l,tree[middle[i]].r); //交换左右子树
  9. }marge(); //合并
  10. }

下面再扯一下pushup和pushdown吧,怕广大读者不懂

pushup and pushdown

其实pushup只是更新树大小而已,就这样:

  1. void pushup(int node){
  2. tree[node].size=tree[tree[node].l].size+tree[tree[node].r].size;
  3. }

至于pushdown下传标记,和文艺平衡树广大算法一样,我也不知道怎么讲好,看代码吧:

  1. void pushdown(int node){
  2. if(tree[node].tag){ //如果有标记就操作
  3. tree[node].tag=0; //原标记清0,和线段树一样
  4. tree[tree[node].l].tag^=1; //左标记取相反值,就是0变成1,1变成0
  5. swap(tree[tree[node].l].l,tree[tree[node].l].r); //交换左子树的左右孩子,因为要翻转嘛
  6. tree[tree[node].r].tag^=1; //右标记取相反值
  7. swap(tree[tree[node].r].l,tree[tree[node].r].r); //交换右子树的左右孩子
  8. }
  9. }

哦,对了,忘记讲怎么输出了。

输出

直接递归最后的那颗树,然后如果是叶子节点就输出它的权值。

  1. void print(int node){
  2. pushdown(node); //pushdown
  3. if(tree[node].size==1){
  4. printf("%d ",tree[node].x); //输出
  5. return ;
  6. }print(tree[node].l);print(tree[node].r); //递归左右子树
  7. }

辣么,你已经学会了YYC Segment Tree的基本操作。

复杂度证明

这里引用的是YYC's blog

注:EX线段树值IST

当然EX线段树也有好有劣,深度为O(log(n))的线段树当然是最好的了。

(下文的“线段树”大多指EX线段树)

深度为O(log(n)) 的线段树,我们称之为平衡线段树。

能发现,平衡线段树满足线段树的不少性质。

如查询时最多分成O(log(n)) 几个区间。

证明十分简单,请读者自己研究。

其他性质:

我们把叶节点称为实节点,因为它存储了实际的数据。

非叶节点称为虚节点,因为它只是存储了标记与上传的数据。

(1)容易发现,虚节点都一定有两个孩子。

虚节点的分布,影响着IST的复杂度,虚节点越 平衡 ,跑的就越快。

至于如何分布,对于正确性来说无关紧要。

平衡树的每个节点必须都是存储原值的节点,所以维护不方便。

这里的虚节点可以随便删除新建(没有懒标记的情况下)。

(2)一棵树有n个实节点,那么就有(n-1)个虚节点。

很好理解,不多讲。

所以我们有一种神奇的操作。

把两颗IST合并可以O(1)。即建一个新的虚节点然后指一下。

很明显FHQ_Treap就要O(logn)

最后在说一下

建树复杂度O(n)

分裂复杂度O(树高)

合并复杂度O(树高)

复杂度讨论:

这里引用的是YYC's blog

由于本人太菜,不会严格证明

“方式1”的marge可以卡。

只要询问[1,1],[1,2],[1,3]...[1,n][1,1],[1,2],[1,3]...[1,n]可以使IST退化成链,但是在某一次询问后又会变成完全二叉树,所以复杂度应该是均摊的。

而且在随机数据中,IST每次操作都是期望logn的。

向大家提出疑问1:“方式1”的marge可以卡吗(不考虑可持久化,卡栈)?

由于现在主流的区间树都不依赖数据随机性,所以所有的区间树题目都是随机造的数据。

IST可以在其中大显身手。

而且,如果人为地在其中加入随机询问,理论上是卡不掉的.

向大家提出疑问2:“方式1”的marge加入随机询问之后可以卡吗(不考虑可持久化卡空间)?

我们定义一颗IST的“深度和”为所有实节点的深度总和。

可见如果“深度和”在O(nlogn)左右是最好的。

先把一整颗IST,split变为森林,则这个森林的“深度和”小于原来IST的深度和(具体的减少大概是O(n)~O(nloglogn)(大家可以证明一下很好证的))。

“方式1”的marge,是直接build完全二叉树。

森林中每棵树的深度增加了O(loglogn),“深度和”增加了O(nloglogn)

由于O(nloglogn)>O(n),“方式1”的marge有或没有被卡掉的可能。

当然还有第二种marge方式。

先看道题:

给出n堆果子,每次把相邻的两堆合并,需要消耗这两堆的重量总和的体力,问把这些果子合成一堆最少需要多少体力。

marge过程就可以抽象为上述题目,最少需要多少体力就是最少增加的“深度和”。

我们直接选取目前相邻的和最小的两堆合并即可。

marge变为O(log^2n)。

明显,每一次marge都会是比之前更优或相等,所以不可能被卡。

下面是第二种合并方式的代码:

  1. int _marge(int x,int y){
  2. if(!x||!y)return x+y; //如果有一个是空,特判
  3. else{
  4. int node=New(); //新建一个节点
  5. tree[node].l=x;tree[node].r=y; //新节点指儿子
  6. pushup(node); //pushup
  7. return node;
  8. }
  9. }
  10. void marge()
  11. {
  12. int cnt=0;
  13. for(int i=1;i<=le;i++)t[++cnt]=Left[i];
  14. for(int i=1;i<=mi;i++)t[++cnt]=middle[i];
  15. for(int i=1;i<=ri;i++)t[++cnt]=Right[i];
  16. int minn,pos;
  17. while(cnt>1){
  18. minn=1<<30;
  19. for(int i=1;i<cnt;i++){
  20. if (tree[t[i]].size+tree[t[i+1]].size<minn){
  21. minn=tree[t[i]].size+tree[t[i+1]].size;
  22. pos=i;
  23. }
  24. }t[pos]=_marge(t[pos],t[pos+1]);
  25. for(int i=pos+1;i<cnt;i++)
  26. t[i]=t[i+1];
  27. cnt--;
  28. }root=t[1];
  29. }

由于文艺平衡树没有卡的数据,所以我写了第一种写法


最后,代码一贴,马上走人。

总的关于这个数据结构的可以看YYC的blog,所有的更新和优化都会在那里声明。

后记

新的一个数据结构需要大家的鼓励呢,希望大家了解这个数据结构后为它点赞,让它让更多人知道。如果有什么疑问可以在讨论中询问,我们将为您解答。

完结

SLT学习——leafes tree扩展 【文艺平衡树】的更多相关文章

  1. bzoj3223 Tyvj 1729 文艺平衡树(Splay Tree+区间翻转)

    3223: Tyvj 1729 文艺平衡树 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 2202  Solved: 1226[Submit][Sta ...

  2. 【阶梯报告】洛谷P3391【模板】文艺平衡树 splay

    [阶梯报告]洛谷P3391[模板]文艺平衡树 splay 题目链接在这里[链接](https://www.luogu.org/problemnew/show/P3391)最近在学习splay,终于做对 ...

  3. BZOJ 3223: Tyvj 1729 文艺平衡树

    3223: Tyvj 1729 文艺平衡树 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 3628  Solved: 2052[Submit][Sta ...

  4. bzoj3223 文艺平衡树 (treap or splay分裂+合并)

    3223: Tyvj 1729 文艺平衡树 Time Limit: 10 Sec  Memory Limit: 128 MB Submit: 3313  Solved: 1883 [Submit][S ...

  5. [bzoj3224]普通平衡树/3223文艺平衡树

    这是一道很普通的题.. 最近花了很多时间来想要去干什么,感觉自己还是太拿衣服 做这道题是因为偶尔看到了lavender的blog和她的bzoj早期AC记录,就被题目深深地吸引到了,原因有二: 自己sp ...

  6. Tyvj P1729 文艺平衡树 Splay

    题目: http://tyvj.cn/p/1729 P1729 文艺平衡树 时间: 1000ms / 空间: 131072KiB / Java类名: Main 背景 此为平衡树系列第二道:文艺平衡树 ...

  7. 3223: Tyvj 1729 文艺平衡树

    3223: Tyvj 1729 文艺平衡树 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 1347  Solved: 724[Submit][Stat ...

  8. 洛谷 P3391 【模板】文艺平衡树(Splay)

    题目背景 这是一道经典的Splay模板题——文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1, ...

  9. 洛谷 P3391【模板】文艺平衡树(Splay)

    题目背景 这是一道经典的Splay模板题--文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1, ...

随机推荐

  1. Jenkins+Maven+Github+Springboot实现可持续自动部署(非常详细)

    目前公司开发的项目已经部署到服务器上,部署项目的测试环境和生产环境,加上每个项目n个服务,于是我就 , 骚就是骚,但是就是太累了,于是花点时间研究了一下Jenkins. Jenkins的作用和它的lo ...

  2. P3378 (模板)并查集

    使用带路径压缩的并查集,不然会TLE AC代码: #include <bits/stdc++.h> #define MP make_pair #define PB push_back #d ...

  3. Linux虚拟机(CentOS)安装gcc, g++

    1.    确保自己的虚拟机联网 点击那个三角形可以选择连接网络 如果还是连不了网,参考https://www.cnblogs.com/xingbo/p/6100554.html 2.联网后,使用命令 ...

  4. sql语句查询指定月份数据

    要求:查询出emp表中1981年2月份入职的员工 emp表 常用的两种方式: 1.YEAR查询年,MONTH查询月 SELECT * FROM emp WHERE ' 2.date_format (使 ...

  5. 「JSOI2013」贪心的导游

    「JSOI2013」贪心的导游 传送门 多次询问区间内%一个数的最大值 我们不妨设这个数为M_sea 值域比较小所以考虑分块维护. 我们观察到对于给定的一个 \(p\) ,函数 \(y = x \% ...

  6. 转载--php 7.2 安装 mcrypt 扩展

    在 php 官网下载 mcrypt 包,php 扩展官网 # wget http://pecl.php.net/get/mcrypt-1.0.1.tgz # tar xf mcrypt-1.0.1.t ...

  7. Windows 查看并关闭占用指定端口的程序

    windows关闭端口的小工具: 链接:https://pan.baidu.com/s/1ZGL4cdSluy0lbi3tDERUvA 提取码:spxy 查看指定端口的使用情况 netstat -an ...

  8. Bugku-CTF加密篇之easy_crypto(0010 0100 01 110 1111011 11 11111 010 000 0 001101 1010 111 100 0 001101 01111 000 001101 00 10 1 0 010 0 000 1 01111 10 11110 101011 1111101)

    easy_crypto 0010 0100 01 110 1111011 11 11111 010 000 0 001101 1010 111 100 0 001101 01111 000 00110 ...

  9. pta谁先倒

    传送门 #include <stdio.h> int main() { int x,y;//酒量 scanf("%d%d",&x,&y); int n; ...

  10. Executor、Executors、ExecutorService多线程操作

    Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command),该方法接收一个Runable实例,它用来执行一 ...