析合树
本篇文章内的图片全部来自于析合树 OIWIKI。
析合树
考虑这样一个问题,给定一个1到n的排列P,如果Pl,Pl+1,…,Pr排序后是一段连续的数值,或者说maxl≤i≤rPi−minl≤i≤rPi=r−l,那么我们称(P,[l,r])是一个连续段。
可以发现连续段拥有如下性质:
- 如果1≤a≤l≤b≤r≤n,且(P,[a,b])和(P,[l,r])都是连续段,那么它们的交集(P,[l,b]),并集(P,[a,r]),差集(P,[a,l))和(P,(b,r])都是连续段。
可以发现连续段的数目是O(n2)的,这有点太多了。现在我们引入本原段的定义:
- 本原段:一个段是本原段,当且仅当不存在另外一个连续段与其部分相交。
可以发现本原段之间只有包含关系,而不存在相交关系。因此我们可以将所有本原段组织为一颗树。考虑到树的叶子结点是n个单一元素的连续段,且每个非叶子顶点下至少拥有两个孩子,因此可以证明本原段的数目是最多有2n−1个。
在本原段组成的树中,一个顶点拥有多个孩子。我们可以将孩子按照它们的下标顺序从小到大排序成一个列表,这样的列表我们称为该顶点的儿子序列。之后我们根据孩子序列中每个孩子中的值进行离散化,得到一个新的排列,这样的排列叫做儿子排列。
下面我们引入析点和合点的定义:
- 合点:如果一个顶点的儿子排列是递增或递减的,则这个顶点为合点。
- 析点:一个顶点是析点当且仅当这个顶点不是合点
有了这两个定义,我们就可以将所有本原段划分为两类:析点和合点。
当一个顶点是合点的时候,那么它的儿子序列中任意连续的几个孩子都可以组成一个连续段。而当一个顶点是析点的时候,它的儿子序列中连续的至少两个孩子组成的序列都不是连续段。
根据上面这个性质还可以推出析点至少拥有四个孩子。否则第二大的孩子可以和左右结合。
根据析合树的定义,我们可以发现所有连续段要么是一个本原段,这时候对应析合树上的一个顶点,要么不是本原段,这时候它对应某个孩子序列中连续的至少两个孩子组成的序列。
上面这个性质允许我们通过析合树高效处理所有的连续段。
下面讲讲如何建立一颗析合树。
我们可以利用增量的方式来构建一颗析合树。假设我们已经根据前k个数值构建了一个析合树森林,我们将森林的根结点按照处理顺序保存在一个栈中。接下来考虑设第k+1个数。首先我们可以构建一个值域为[Pk+1,Pk+1]的本原段,记这个本原段为x,记栈顶部的顶点为y,我们需要做下面操作:
- 如果y是一个合点且拥有至少两个孩子,且x可以作为y的新儿子,那么将x加入到y的儿子序列中,之后y出栈并令x=y。
- 如果第1步没有成立,则尝试创建一个新合点z,它的孩子为y和x,之后将y出栈并令x=z。
- 如果前面几步都不成立,创建一个析点z,并将x和栈顶部尽可能少的顶点作为z的孩子并出栈,最后令x=z。
- 如果前面几步都不成立,将x入栈,并结束流程。
上面的算法瓶颈实际上在第三步,因为很可能第三步没有满足,但是我们被迫需要扫描整个栈。因此如果就使用naive的算法的话,时间复杂度会达到O(n2)。
要优化上面的算法,我们必须减少第3步需要的扫描次数(出栈入栈其总的时间复杂度为O(n)可以无视)。
假设给定区间[a,b],记[l,r]为包含所有值落在[mina≤i≤bPi,maxa≤i≤bPi]范围内的最小区间。如果r>b,则可以发现以b作为右端点,以小于等于a的数作为区间左端点的区间一定不是一个连续段。因此在第三步的时候,如果我们倒着扫描栈,一旦发现当前合并的区间中,此时算出的r>k+1,则可以直接退出。当然这个过程是微不足道的,但是如果我们结合fail链的技术,比如说新插入的顶点x的fail链指向第一个遇到的r>k+1的顶点。这时候我们可以y.fail解读为要合并y和前面一个顶点必须先同时合并掉y.fail。这样的话扫描的时候就可以沿着fail链来做操作,第三步的总的时间复杂度就优化到O(n)了。
然后我们还需要O(1)计算区间最小最大值的技术,我们可以通过结合笛卡尔树+Schieber Vishkin算法实现。
上面讨论的这个算法的时间复杂度为O(n)。
题目1:给定一个长度为n的置换P,要求回答其中有多少连续段。这里1≤n≤106。
求出析合树后,可以发现每个本原段(顶点)都对结果贡献1,而每个拥有k个孩子的合点,对结果贡献(k2)−1。
时间复杂度为O(n)
题目2:给定一个长度为n的置换P,之后回答m个请求。每个请求给定l,r,要求找出最小的连续段(P,[a,b]),满足a≤l≤r≤b,结果保证唯一。这里1≤n≤106
首先求出析合树,可以发现[l,l]和[r,r]对应的是析合树上的叶子结点。找到二者的LCA,如果LCA是析点,则结果就是LCA代表的本原段,否则我们可以取二者在LCA孩子列表中二者之间的所有孩子组成的连续段。
这里我们可以在树上做倍增,时间复杂度为O(nlog2n)。
提供一道题目。
题目3:给定一个长度为n的置换P,之后回答m个请求。每个请求给定l,r,要求计算Pl,Pl+1,…,Pr的子序列中有多少是连续段。这里1≤n,m≤106
提供一道题目。
首先我们可以在P上建立一颗析合树。
之后对于每个请求,我们实际上可以遍历一遍析合树就可以找出所有落在区间的连续段的数目,因此对于每个请求的时间复杂度为O(n)。
我们考虑从L=[Pl−1,Pl−1]和R=[Pr+1,Pr+1]这两个叶子之间的路径。可以发现路径上的所有顶点都不处于区间中。路径将树划分为两部分,路径内部的顶点都是属于区间的,外部的顶点都是属于区间外的。
我们可以为每个顶点u赋予一个c属性,即u.c表示的是以u为根的子树存在多少连续段。记u.a表示以u所在儿子列表中以u开始的后缀中顶点集合所对应的子树所存在的连续段数目,同时记u.b表示以u所在儿子列表中以u开始的后缀中顶点集合所对应的子树所存在的连续段数目。
之后我们记A为L和R的LCA,记LA为A的孩子中包含L的顶点,同理记RA为A的孩子中包含R的顶点。于是我们可以发现从L到LA的路径上我们取顶点的a属性,从R到RA的路径上我们取顶点的b属性,而对于LA和RA我们需要特殊判断。
要处理路径,我们可以使用LCT、树链剖分。但是由于不存在修改操作,我们可以使用更加简单的树上前缀和的技术,利用直链剖分快速找到LA和RA。
这样预处理的时间复杂度为O(nlog2n)。之后每个请求都可以O(1)回答。