前言
心态崩了
这玩意不仅比较猎奇,而且网上有关资料还是参差不齐的。
看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(l到r)−min(l到r)−(r−l)=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最近的两个儿子节点形成的区间。
证明?
自己画画图就发现这个东西很显然了。
(其实是我懒得画图讲了)