SLT学习——leafes tree扩展 【文艺平衡树】
这是一个全新的数据结构
md,别看这篇文章了,这篇已经废了。
百折不饶,再交一次,更新复杂度证明
这里是HYF,蒟蒻一只,最近因某些原因开始学数据结构了,然后就写了这篇题解。
下面给大家介绍一个全新的数据结构,暂且称作IST(Immortal segment tree),你们也可以称作YYC Segment Tree(像FHQ_Treap 一样)
本人是这数据结构的第一弹使用者,对这个数据结构起到几乎没用的作用。这篇题解也算是这个数据结构的一个预告,给广大OIer了解一下这个数据结构。
由于我们学校信息学团实在是太菜了(除了YYC巨佬,就是他发明了这个数据结构;还有ZLH巨佬,他初二LCT)所以这个数据结构还没有被完全研究透彻,只能做一些比较正常的操作,例如:
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、分裂
把整颗树分成三个部分,左,中,右,最后中部分就是我们要操作的部分。例如这道题的旋转操作,我们像线段树一样递归下去,如果要此时的区间完全在要操作的内,那么就分到中区间;如果在完全在要操作的区间左部,那就分到左区间;右区间同理。
最后把分裂出的节点删掉,因为那已经没用了,删掉的节点放入垃圾回收,以便于下次可以重复利用该节点,节省空间。
看下代码吧:
void spilt(int node,int begin,int end,int x,int y){
if(end<x){Left[++le]=node;return;} //如果查询区间在要修改区间左部,并入左数组
if(x<=begin&&end<=y){middle[++mi]=node;return;} //如果查询区间在要修改区间中,并入中数组
if(y<begin){Right[++ri]=node;return;} //如果查询区间在要修改区间右部,并入右数组
pushdown(node); //pushdown更新tag标记
int mid=begin+tree[tree[node].l].size-1; //注意,因为经过修改后的树不像线段树那么平衡,所以mid值有所变动
spilt(tree[node].l,begin,mid,x,y); //向左子树递归分裂
spilt(tree[node].r,mid+1,end,x,y); //向左子树递归分裂
rub[++tot]=node; //这里是垃圾回收
}
2、合并操作
我们先把分裂好的三个部分:左,中,右.合并在一个数组里,直接三个for赋值就可以了。像这样:
int cnt=0;
for(int i=1;i<=le;++i)t[++cnt]=Left[i];
for(int i=1;i<=mi;++i)t[++cnt]=middle[i];
for(int i=1;i<=ri;++i)t[++cnt]=Right[i];
然后我们就对合并好的数组进行操作,就是把他们两两合并,每一次新建一个节点存他们合并后的数据,然后把这个新节点的左儿子和右儿子分别指向要合并的两个节点,最后再pushup就可以了。
我觉得这里可能大家会比较难理解,其实就是一个重新建树的一个过程(我们也可以理解为一个石子合并的过程,只不过是相邻两个合并而已)。下面看看完整代码比较好理解:
void marge(){ //合并
int cnt=0; //记得数组清0,要按顺序合并哦
for(int i=1;i<=le;++i)t[++cnt]=Left[i]; //把左部分合并
for(int i=1;i<=mi;++i)t[++cnt]=middle[i]; //把中部分合并
for(int i=1;i<=ri;++i)t[++cnt]=Right[i]; //把右部分合并
while(cnt>1){ //重新建树过程
int mid=(cnt+1)/2; //取mid值
for(int i=1;i<=mid;++i){ //将线段树底部节点一一合并
int x=t[i*2-1],y=t[i*2];
if(!x||!y)t[i]=x+y; //如果有一个是空,特判
else{
int node=New(); //新建一个节点
tree[node].l=x;tree[node].r=y; //新节点指儿子
pushup(node); //pushup
t[i]=node; //把新节点并入数组里
}
}
for(int i=mid+1;i<=cnt;++i)t[i]=0;//记得把剩下节点清0,原因是他们已经在上面合并过了,而且避免了奇偶的错误
cnt=(cnt+1)/2; //到下一层继续建树
}root=t[1]; //根更新
}
3、新建节点
在这里我们的垃圾回收就起到作用了,我们的垃圾会收就是把一些被删除的节点记在垃圾堆里,那么现在我们就可以把他们重新取出来,作为新节点的编号。嗯~,还有就是记得给新节点数据清0哦。
int New(){
int pos=rub[tot--];
tree[pos].l=tree[pos].r=tree[pos].size=tree[pos].x=tree[pos].tag=0;
return pos;
}
细心的读者可能发现tree[node].size也清0了,这是为什么了,一个节点的大小怎么会是0呢?
因为它每一次新建是新建一个虚节点,虚节点刚开始是什么都没有的,只有在连向其他节点时才能有信息。
我们可以发现新建节点的操作只在合并出现,而新建节点后有个pushup操作……
4、建树
哎呀,这个操作和线段树差不多的啦,看注释吧:
void build(int node,int l,int r){
if(l==r){
tree[node].x=l; //初值是i嘛
tree[node].size=1; //树大小是1
return;
}int mid=(l+r)>>1; //和线段树一毛一样
tree[node].l=++top; //新建左子树的编号
build(top,l,mid); //递归左子树建树
tree[node].r=++top; //新建右子树的编号
build(top,mid+1,r); //递归右子树建树
pushup(node); //pushup
}
旋转
分三步,一分裂,二操作,三合并
主要讲一下操作部分
就是把中部分取出来,然后打标记,再翻过来,交换左右子树。
没了……
void rotate(int x,int y){
le=0,ri=0,mi=0; //记得左,中,右部分数组要清0
spilt(root,1,tree[root].size,x,y); //分裂
for(int i=1;i<=mi;++i)tmp[i]=middle[i]; //把中部分提取出来
for(int i=1;i<=mi;++i){
middle[i]=tmp[mi-i+1]; //翻过来,就是直接赋值嘛
tree[middle[i]].tag^=1; //打标记
swap(tree[middle[i]].l,tree[middle[i]].r); //交换左右子树
}marge(); //合并
}
下面再扯一下pushup和pushdown吧,怕广大读者不懂
pushup and pushdown
其实pushup只是更新树大小而已,就这样:
void pushup(int node){
tree[node].size=tree[tree[node].l].size+tree[tree[node].r].size;
}
至于pushdown下传标记,和文艺平衡树广大算法一样,我也不知道怎么讲好,看代码吧:
void pushdown(int node){
if(tree[node].tag){ //如果有标记就操作
tree[node].tag=0; //原标记清0,和线段树一样
tree[tree[node].l].tag^=1; //左标记取相反值,就是0变成1,1变成0
swap(tree[tree[node].l].l,tree[tree[node].l].r); //交换左子树的左右孩子,因为要翻转嘛
tree[tree[node].r].tag^=1; //右标记取相反值
swap(tree[tree[node].r].l,tree[tree[node].r].r); //交换右子树的左右孩子
}
}
哦,对了,忘记讲怎么输出了。
输出
直接递归最后的那颗树,然后如果是叶子节点就输出它的权值。
void print(int node){
pushdown(node); //pushdown
if(tree[node].size==1){
printf("%d ",tree[node].x); //输出
return ;
}print(tree[node].l);print(tree[node].r); //递归左右子树
}
辣么,你已经学会了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都会是比之前更优或相等,所以不可能被卡。
下面是第二种合并方式的代码:
int _marge(int x,int y){
if(!x||!y)return x+y; //如果有一个是空,特判
else{
int node=New(); //新建一个节点
tree[node].l=x;tree[node].r=y; //新节点指儿子
pushup(node); //pushup
return node;
}
}
void marge()
{
int cnt=0;
for(int i=1;i<=le;i++)t[++cnt]=Left[i];
for(int i=1;i<=mi;i++)t[++cnt]=middle[i];
for(int i=1;i<=ri;i++)t[++cnt]=Right[i];
int minn,pos;
while(cnt>1){
minn=1<<30;
for(int i=1;i<cnt;i++){
if (tree[t[i]].size+tree[t[i+1]].size<minn){
minn=tree[t[i]].size+tree[t[i+1]].size;
pos=i;
}
}t[pos]=_marge(t[pos],t[pos+1]);
for(int i=pos+1;i<cnt;i++)
t[i]=t[i+1];
cnt--;
}root=t[1];
}
由于文艺平衡树没有卡的数据,所以我写了第一种写法
最后,代码一贴,马上走人。
总的关于这个数据结构的可以看YYC的blog,所有的更新和优化都会在那里声明。
后记
新的一个数据结构需要大家的鼓励呢,希望大家了解这个数据结构后为它点赞,让它让更多人知道。如果有什么疑问可以在讨论中询问,我们将为您解答。