Pbri

析合树学习笔记

析合树学习笔记

析合树是一种数据结构,事实上能解决的问题并不多,但对于解决“连续段”问题是非常好用的工具,然而事实上全网只能找到三个可以用析合树的题目而且全部可以用线段树解决,但我太弱了不会线段树做法,所以只能学析合树。可能存在别的应用,但我太弱了不会。

先给一些定义

连续段:给一个排列 \(P\) ,定义一个区间 \([L,R]\) 为连续段,当且仅当将这个区间内的元素从小到大排序后,值是连续的。比如 \([3,4,1,2]\) 就是一个连续段, \([3,5,1,2]\) 则不是。

\(I_p\)\(P\) 这个排列中所有连续段的集合

本原连续段:定义区间 \([l_1,r_1]\in I_p\) 是一个本原连续段,当且仅当不存在区间 \([l_2,r_2]\in I_p\) 使得 \(l_1\in [l_2,r_2]\)\(r_1\in [l_2,r_2]\) 中有且仅有一个成立,也就是说任意两个本原连续段要么互相包含要么分离。比如排列 \([1,4,6,2,3,5,10,9,8,7]\) 中的本原连续段就是:所有长度为 \(1\) 的,以及 \([4,5],[2,6],[7,10],[2,10],[1,10]\)\([7,9]\) 则不是,因为与 \([9,10]\) 相交。

析合树:一棵析合树是由所有的本原连续段组成的,每个结点代表一个本原连续段,定义 \(v\)\(u\) 的儿子,当且仅当 \(l_u\le l_v\le r_v\le r_u\) ,即 \(v\) 所代表的本原连续段是 \(u\) 所代表的本原连续段的子区间。

儿子排列:对于结点 \(u\) 来说,他的儿子们所代表的值域区间任意两个都是相离的,而且他们的并是 \(u\) 所代表的值域区间区间,然后我们就可以把他们离散化一下,按照 \(V_l\) 或者 \(V_r\) 都可以,形成的排列就是儿子排列。

合点:合点是析合树上的一个点,我们定义一个点是合点,当且仅当他的儿子排列是递增或递减。

析点:跟合点对应,不是合点就是析点。

特别的,对于叶子结点,根据定义来说它是合点,但实际情况中将其看作析点是一个比较方便的选择。

\(L_u,R_u\)\(u\) 这个结点所对应的本原连续段的左端点和右端点

\(typ_u\)\(u\) 这个结点的类型,如果是合点就是 \(1\) ,析点就是 \(0\)

\(M_u\):只对于合点而言的,代表儿子排列中第2个儿子的左端点(注意是第 \(2\) 个!)

\(cnt\):析合树中结点的数量,同时也是本原连续段的数量

再给一些析点的性质

1、析点中不存在两个相邻儿子是递增或递减的。因为如果有两个相邻的递增或递减,那么这两个合起来也是一个本原连续段,根据析合树定义,每一个本原连续段都在析合树中,所以不满足定义。

2、析点至少有四个儿子。一个两个不用说,对于三个儿子,任意的排列中都至少有两个相邻的是递增或递减,根据性质1不存在。

关于析合树的构造

实际上存在 \(O(n)\) 的方法,不过我不会,而且很难写。

总体思路是增量法:对于一片析合树森林,考虑第 \(i\) 个元素如何加入。

用一个栈存析合树森林中的每一个根,按所代表区间的顺序从小到大,这个按顺序正常插入就可以保持。需要注意的是一条边一旦连上了就再也不会断了,这保证了过程中连边形成的树恰好是这棵析合树而不会有多余的 边。先把第 \(i\) 个元素变成 \([i,i]\) 的结点 \(now\) ,然后加入这个结点,设栈顶元素是 \(T\),按下列方式考虑(只是思路,具体操作后面会讲):

1、如果 \(T\) 是合点,而且插入 \(T\) 这棵树后 \(T\) 仍然是合点,那么将栈顶元素取出,然后直接插入进去,作为 \(T\) 的一个儿子(因为 \(now\) 是第一个到达 \(i\) 这个位置的结点,所以肯定不会是这棵树的其他儿子的儿子),然后再插入回栈,\(now=stack[top],R_{now}=i\)

2、如果 \(T\) 是析点,但是如果 \(now\) 可以和这个结点合并成一个本原连续段,那么 \(cnt+1\) ,因为只有两个点,所以是合点,然后左端点是 \(T\) 的左端点,右端点是 \(i\)\(M_{cnt}\)\(L_{now}\)。然后取出 \(T\)\(now=cnt\),然后把 \(now\) 插入进去。

3、如果前两种情况都不符合,那么一直往后找,一直栈中的一个点,使得从这个点到栈顶连上 \(now\) 可以形成一个连续段,然后 \(cnt+1\),左端点是这个点的左端点,右端点是 \(i\),取出从这个点到栈顶的所有点,\(now=cnt\),把 \(now\) 插入进去。

重复这个过程,直到不可能再继续合并。

关于过程中一些具体的操作

如何判断能否合并?

考虑这样一个事实,对于一个排列, \([L,R]\) 是连续段的充要条件是 \((\max_l^ra_i-\min_l^ra_i)-(r-l)=0\),因为如果不是连续段那么一定有更大或者更小的,那么肯定不是 \(0\) ,所以用普通的 \(st\) 表就可以完成查询这个操作

如何判断插入合点后是否还是合点?

这个问题伴随着另一个问题:\(M\) 数组干什么用的?如果只是单纯为了判断插入之后是否还是一个连续段,实际上我们只需要判断 \([L_T,i]\) 是不是连续段就可以,但并不能保证插入后是否还是一个合点,比如 \([5,4,3]\) 插入一个 \(6\) ,显然插入之后还是一个连续段,然而却不是一个合点了。\(M\) 数组的作用就是取右边一段判断能否和这个结点合并。假设儿子排列是降序,所以如果右边这一段可以和新结点合并成为一个连续段,因为更大的那一边已经有元素了,而且是个排列,所以如果可以合并那么 \(u\) 所代表的值域一定是小的那一边,所以如果和右边一段合并还是一个连续段,那么整体合并后也肯定还是合点了。

如何判断有没有可能继续合并?

对于第一个问题,有一个性质,就是那个式子的最小值就是 \(0\),对于每个位置都有这个式子唯一的值,我们把这些值排成一个序列,那么可以合成的最长的本原连续段就是最左边的 \(0\) 的位置,这个可以用线段树维护。

维护的信息:区间加标记,区间最小值(不是位置!!!)

修改:考虑用单调栈维护最小值最大值,这样每次我们就可以查出这个点改变 \(\max or \min\) 的区间,然后取消原来的最大值最小值的贡献,加上这个位置的贡献。至于 \(r-l\) ,我们每次都对 \([1,i]\) 这个区间减一就好了。

查询:如果左儿子最小值是 \(0\) ,直接查左儿子,否则取右儿子。因为 \(i\) 这个位置一定是个连续段,所以肯定有位置。

构造讲完了。

关于复杂度

构造过程是 \(O(n\log_2n)\)这比较显然,但空间是 \(2n-1\) 的。原因和虚树差不多,用归纳法,考虑假设前面过程中我们是比着最终的析合树构造虚树,然后 \(n=1\) 的时候显然满足条件,然后每加入一个点,要么它的父亲已经存在了,那么增量是 \(1\) ,否则要把父亲加进去再加自己,增量就是 \(2\) ,然后就证完了。

下面是例题

例题实在太缺了,就三个,然后都可以线段树淦过去,析合树跑的又慢写起来又麻烦,大家优先选择线段树解法,想不出来再用析合树日过去。下文中只讲析合树做法。

CF526F

题意:给一个排列,询问有多少个连续段

\(solution\):建出析合树,析点对答案的贡献是 \(1\) ,合点的贡献是儿子的对数,如果有 \(k\) 个儿子,那么对答案的贡献是 \(\frac{k\times(k-1)}{2}\)

\(code\)https://codeforces.com/contest/526/submission/120216052

P4747

题意:给一个排列,每次询问一个区间,问包含这个区间的长度最短的连续段。

\(solution\):求出这两个端点的 \(lca\),然后如果是析点就是析点代表的区间,如果是合点,那么左端点就是 \(ql\)\(lca\) 的那个儿子的左端点,右端点同理。

\(code\):没写

CF997E

题意:给一个排列,每次询问一个区间内有多少个连续段。

\(solution\):首先求每个本原连续段内的答案。如果是析点就是所有儿子的答案之和加一,如果是合点,那么除了儿子的答案之和加一外还有儿子的对数。还是考虑 \(lca\),然后在 \(lca\) 上考虑两个儿子中间的儿子,如果是析点就是每个儿子的答案之和,如果是合点就是儿子之和和对数,后者可以简单容斥做一下:右端点前缀和(不包含)-左端点前缀和(包含)-左边(包含端点)的儿子数量乘以中间的儿子数量。端点都是指的左右端点对应的儿子。

然后考虑到 \(lca\) 的路径上,不可能存在到 \(lca\) 的路径上会有与 \(lca\) 中的儿子造成贡献的情况,那样就不符合本原连续段的定义了。然后这个可以倍增的去算,\(pre_i,j\) 表示从 \(i\) 这个结点往上走 \(1<<j\) 步过程中,不包括自己这个结点的前缀之和,然后 \(qr\) 往上走的时候就一块求出来,然后左端点类似,用 \(suc\) 就好了

\(code\)https://codeforces.com/contest/997/submission/120375560

posted @ 2021-06-24 22:20  Pbri  阅读(333)  评论(0编辑  收藏  举报