啃WC课件系列。
LCA讲得很好了(虽然一些奇怪的定义让人摸不着头脑),为了以后复习方便自己再整理下。
析合树是用于连续段问题的比较通用的数据结构。
首先定义一下连续段:对于一个长度为(n)的排列(p),如果对于一个区间([l,r]),如果(p_l,p_{l+1},dots,p_r)排序后可以组成值域连续的一段,即(max_{i=l}^r p_i-min_{i=l}^r p_i+1=r-l+1),那么称([l,r])为(p)的一个连续段。
考虑到连续段有一个非常有用的性质:
对于两个相交但互不包含的连续段(x),(y),总有(xcup y)、(xcap y)、(xsetminus(xcap y))和(ysetminus(xcap y))都是连续段。
证明比较显然。
从这个性质出发,我们定义本原连续段为不存在和它相交且互不包含的其它连续段的连续段。容易发现一个非本原连续段一定可以用若干个相邻的本原连续段的并来表示,并且由于本原连续段互相只有包含关系,因此如果将一个本源连续段看成一个点,那么所有本原连续段实际上可以形成一棵树形结构。非本原连续段都是由某些连续的兄弟本原连续段组成的。
那么,究竟哪些连续的兄弟本原连续段可以组成非本原连续段呢?
由于所有儿子的并是一个连续段,我们不妨将儿子缩成一个点并离散化,这样可以简化为一个排列(不妨叫做儿子排列)。我们只需要知道这个排列的连续段信息即可。
一种合法的情况是,儿子排列的所有非平凡区间都不是连续段,我们将拥有这样的儿子的点成为析点。
接下来考虑至少存在一个非平凡连续段的情况,那么我们枚举所有的非平凡连续段,可以发现所有非平凡连续段的并就是整个排列,同时每一个点都会作为若干个连续段的交出现——否则我们就找到了一个非平凡的本原连续段,这是不可能的。
那么我们可以发现:假设儿子排列的长度为(n),那么对于(1sim n-1)的每个位置,以它开头的非平凡连续段至少存在一个,对于(2sim n)的每个位置,以它结尾的非平凡连续段也至少存在一个。那我们考虑对于其中的任意一个区间([l,r]),将以(l,l+1,dots,n-1)为开头的非平凡连续段取并,可以得到([l,n])是连续段,将以(2,3,dots,r)为结尾的非平凡连续段取并,可以得到([1,r])是连续段,将两个连续段取交就可以得到([l,r])也是连续段。
于是我们可以发现这种情况下,任意一个区间都是一个连续段,我们将拥有这样的儿子的点成为合点。同时不难发现合点的儿子排列只可能是(1sim n)或者(nsim 1)。我们将长度为(1)的连续段也看作合点。
我们还可以发现一些性质:
(1)、对于一个析点,它的儿子个数一定(geq 4),因为(leq 3)的排列都至少存在一个非平凡连续段。而对于任意(ngeq 4),一定存在有(n)个儿子的析点。因为如果(n)是偶数,那么(2,4,6,dots,n,1,3,5,dots,n-1)一定是符合要求的排列,否则(n)是奇数,(4,6,8,dots,n-1,1,3,5,dots,n,2)一定是符合要求的排列。
(2)、对于任意一棵包含(n)个叶子的树,如果指定好每个非叶节点的析合性,并满足析点儿子个数(geq 4),合点儿子个数(geq 2),那么一定存在一个长度为(n)的排列满足其析合树为这棵树。可以通过构造符合要求的儿子排列实现。
(3)、对于所有区间是否为连续段的情况都相同的排列,它们的析合树相同。结合性质(2),我们可以通过计数析合树来计数区间连续段情况的不同个数。
最后的问题是析合树的构造,这里只给出(O(nlog n))的做法,LCA的(O(n))太强了不会。
考虑增量,在前(1sim i-1)项构成的析合树中插入第(i)项。由于接下来的过程保证只会让已经确定是本原连续段的点向父亲连边,因此可以发现如果一个点已经有父亲则其子树不会再改变(由本原连续段的性质得)。于是我们维护一个栈,表示所有尚未有父亲节点的点。
那么我们描述一下增量第(i)项的过程:
首先对(i)单独作为一个点,称为当前点。
接下来不断重复这些步骤:
首先判断一下当前点能否成为栈顶的点的新儿子。如果可以则将当前点的父亲设为栈顶的点,并取出栈顶的点作为新的当前点。不断进行这一步直到栈为空或当前点无法成为栈顶的新儿子。
我们来证明能够合并的当前点一定是一个本原连续段。可以证明栈顶的点一定是合点(因为析点的非平凡儿子前缀一定不是连续段),因此当前点一定是作为儿子排列的最小段或最大段,不妨以是最大段为例,由于之前并未进行合并,因此当前点不存在一个不满的前缀值域是最大段的前缀,等价于不存在一个不满的后缀值域是最大段的后缀。由于最大段的前缀在之前出现过,因此如果想要和当前点的不满后缀拼合得到新的连续段,当前点的后缀的值域必须要是最大段的后缀,而这是不可能的。因此可以合并的当前点一定是本原连续段。
如果当前点不能成为栈顶的儿子,那么先通过某种方法(接下来再具体讲)判断当前点是否可能与栈顶的若干个点合并成一个连续段得到一个新点。如果不可能则直接结束这次增量。否则我们暴力向前扫描直到可以合并成一个连续段为止。
我们来证明合并的这些点一定是本原连续段。可以发现此时“切开”某个儿子的那些前缀的值域不可能是整个连续段值域的前缀(否则会出现新的本原连续段),因此不存在一个切开儿子的后缀是值域的后缀,同理也不存在一个不满的后缀是值域的前缀,因此实际上一个切开儿子的后缀是无法和之后的任何段形成连续段的,可以得证。
考虑除了判断方法之外的部分的复杂度。由于每次花费的时间和栈里减少的元素个数成正比,而每次增量最多使得栈内元素个数(+1),因此这部分的复杂度为(O(n))。
最后考虑如何判断是否可能与之前的点合并。考虑预处理数组(L),其中(L_i)表示以(i)右端点的所有连续段的最小左端点。那么加入栈顶的点左端点(<L_i),那么一定不会合并。否则如果存在一个点的左端点(=L_i),那么一定可以合并到这里。否则一定存在一个不是栈顶的点,其区间为([l,r])且(l<L_ileq r),由连续段性质可知([r+1,i])一定是连续段,因此可以合并。
那么问题在于如何处理(L)。考虑维护从左到右枚举区间右端点,用线段树维护每个区间的(max-min+1-区间长度),那么只需要找到一个区间的最左端的值为(0)的点,由于(0)是可能的最小值,因此实际上是找到最左边的最小值的位置。考虑对于(max)和(min)的处理可以用单调栈做到共计(O(n))次操作,区间长度也只要在每次右端点更新时更新即可。于是复杂度就是(O(nlog n))。
综上所述,构建析合树的复杂度可以做到(O(nlog n))。
(P.S.)文章里许多过程或者证明都是我口胡的,假如存在错误或者不必要的繁琐的地方恳请指正。