学习析合树小记

前言

心态崩了
这玩意不仅比较猎奇,而且网上有关资料还是参差不齐的。
看WC2019LCA的ppt也是一头雾水(尤其是那个定义)
最后在三个地方终于找到了有价值的东西。(自己看到下面的参考资料)
然后想想还是写写博客加强印象以免忘记吧。

正题

引入

我们看一道题:
在这里插入图片描述
让我们来想想这题怎么做?
有一个十分方便的方法——
对于每次询问,我们考虑选出询问区间内的最大值与最小值。
然后再找出选取最大到最小区间所需要的最远左端点与右端点。
重复直到达到要求即可。

然鹅这种做法可以被卡掉,有没有更优秀的做法呢?
可以考虑分块,但是分块是根号的,依然不是很优秀。
这时有可以利用一个神奇的数据结构——析合树
(话说这个东西是最近才在中国的OI界传播开来的,外国方面不清楚,利用百度翻译可知这玩意英文名简称为AC-Tree手动滑稽

定义

我们定义连续段表示:上面题目中的连续区间。
我们定义本原连续段表示:几个互不相交且互不互不包含的连续段。
可以发现,连续段可以由几个本原连续段连在一起。

回来正题:
这个析合树是个什么东西呢?
其实也是类似于线段树一样,每个节点表示一段区间的树。
但是它不是二叉树,而且每个节点所代表的都是一个连续段。
并且,当前节点所代表的连续段是由其儿子的本原连续段拼接而成的。
利用OI WiKi的一幅图理解理解:
在这里插入图片描述
然后,析合树中析合是什么呢?

值域区间看到上面的图很容易知道是什么。
我们再定义儿子排列表示:对于析合树上的一个结点 ,把它儿子的区间顺序重标号。
很难理解?
看到上面的图就是一个例子。
那么,
对于一个析点就是儿子排列为无序的节点。
对于一个合点就是儿子排列为顺序或是倒序的节点
在这里插入图片描述
至此,一些奇妙的定义就差不多完了。

性质

我们来发掘发掘析合树的神奇性质。
我们可以看到,对于析合树的每个节点都分为析点或是合点(叶子节点看做合点)
然后,
对于合点来说,任意选出n个(1<n<=sum_son(也就是儿子总数))连续的儿子出来,它们的区间组合起来是一个连续段
对于析点来说,任意选出n个(1<n<=sum_son)连续的儿子出来,它们的区间组合起来都是一个连续段
证明?
合点显然,因为他的儿子排序都是有序的。
对于析点:
如果任意选出n个儿子区间组合起来是连续段,那么之前肯定建出一个析点或合点包含这些儿子了。
所以说不存在于这种情况。
在这里插入图片描述
那么性质就大致发掘至此。

构造

构造其实有两种方法,一种是便于理解且思路清晰的 O ( n   l o g 2   n ) O(n\ log_2\ n) O(n log2 n)方法,而另一种是 O ( n ) O(n) O(n)的高级方法。
前者正如上面所述,很好搞,容易入手。然鹅后者是比较神仙的方法,我没弄懂。
我是这么安慰自己的——这玩意只是建树 O ( n ) O(n) O(n),查询还不是要带log?
所以没学。
有兴趣的可以去看LCA的ppt或是看CC的博客:
https://blog.csdn.net/Cold_Chair/article/details/91358311

增量法

我们对于建造析合树,我们可以考虑一个一个点地加入到树中。
首先,我们先弄一个栈,栈里面储存着1——>i-1这些点所构成的析合森林的根。
这些根要么是析点,要么是合点(废话)
为了方便打字,我们设栈顶的为h,当前要加入i这个点。

那么现在,如果要加入一个点,一个直观的方法就是把这个点考虑与栈里的点合并。
这就是增量法(名字还不错听)。

至于怎么增量呢?有以下3种情况:

  • 1、i可以变成h的一个儿子,然后这个h必须是合点且合并以后同样也是合点。
    具体:如果type[h]=1(合点)且lastson[h](h的最右边的儿子)与i合并形成了连续段。
    问题来了,为什么h必须是合点呢?如果是析点,原来析点的儿子们已经可以合并成连续段了。再往后面加一个点的话就使得析点的性质不满足了。

  • 2、如果i不能变成h的一个儿子,但是可以接在h后面变成一个新的连续段,那么就新增一个合点z,把z的儿子附成h,再拿i去做第一步即可。

  • 3、如果上述两种方法不能合并i,那么我们考虑新建一个析点z,然后从右往左把栈里面尽量少的一段点与i合并起来,附成析点z的儿子。

最后上面三种情况都不能合并,则表示i无法合并,直接把i丢在栈顶即可。
简单清晰明了,但问题是如何去快速判断3种情况并合并呢?

暴力的方法

暴力即优美
对于情况1、2,我们可以很轻松很快速地算出答案。
既然是连续段,那么就少不了一个非常有用的判定柿子:
m a x ( l 到 r ) − m i n ( l 到 r ) − ( r − l ) = 0 max(l到r)-min(l到r)-(r-l)=0 max(lr)min(lr)(rl)=0
那么我们利用ST表来维护上面的即可 O ( 1 ) O(1) O(1)解决。

但是现在问题是情况3。
暴力的方法就是从栈顶一直找回去找到满足情况的为止。
但是有一个问题,这种方法很容易被数据构造使得要每次遍历一遍栈。
时间复杂度退化成 O ( n 2 ) O(n^2) O(n2)
这很不优秀。
在这里插入图片描述
怎么办?

神奇的优化

我们考虑优化这个遍历过程。
先设一个数组last[i]表示以i为右端点,最远向左拓展到last[i]这个位置且满足last[i]~i这是一个连续段
于是每次往回找,如果超过了last[i],那么就退出不找了。
自己画一下数轴就发现这个小小的优化神奇地优化到了 O ( n ) O(n) O(n)级别的。

那么问题来了,这个L数组怎么求呢?
还记得上面的柿子吗?
当这个柿子满足之时,将是出现一个连续段之时。
ST表显然不太行了,因为每次都是往右边加入一个值。
所以考虑利用某些高强手段维护这个值。
由于本人大脑的问题,只会利用线段树去维护这个值。
但是由于每次往后面拓展一个数字时,max与min都可能会相对应地发现变化。
因此,我们只需要利用两个单调队列分别维护max与min。
每次加入一个数值,相对应会影响单调队列,那么每次退出一个值的时候,在线段树上区间修改即可。

时间复杂度差不多就是 O ( n   l o g   n ) O(n\ log\ n) O(n log n)的了

流程图

还是从OI-Wiki转来的。
(其实很鸡肋)
在这里插入图片描述

至此,构造已经讲完了。

应用

我们回到上面“引入”所利用到的题目。
在那道题目中,我们要求包含[x,y]这段区间的最小连续段。
由于我们已经建出析合树了,那么我们可以来看看怎么利用。
首先,我们考虑找到x,y两个点在析合树上的lca。
找到之后,我们判断其是否为析点,如果是析点,那么答案就是它的区间。
否则,就是当前点距离x,距离y最近的两个儿子节点形成的区间。

证明?
自己画画图就发现这个东西很显然了。
(其实是我懒得画图讲了)

参考资料

OI WiKi
大米饼
Cold_Chair

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值