析合树学习笔记
析合树
对于一个 \(n\) 阶排列 \(p\),定义其一个区间 \([l,r]\) 是连续的,当且仅当 \(p_{l},p_{l+1},\cdots,p_r\) 值域连续。我们用下标区间 \([l,r]\) 来代表这个连续段,用 \(f(l,r)\) 表示这个连续段的值域区间。
我们称排列 \(p\) 的所有连续段构成的集合为 \(I_p\)。
连续段有几个显而易见的性质:
-
对于任意两个有交且互不包含的连续段 \(A,B\in I_p\):
-
\(A\cup B\in I_p\)。
-
\(A\setminus B,A\cap B,B\setminus A \in I_p\),即 \(A,B\) 相交而划分出的三个子区间都是连续段。
而且可以看出这三个子区间的值域是连续的。
-
-
\([i,i],[1,n]\in I_p\)。
定义 \(I_p\) 中的一个连续段 \(A\) 为本原连续段,当且仅当对于 \(I_p\) 中任何一个其他连续段 \(B\),要么 \(A,B\) 相互包含,要么 \(A,B\) 不交。
显然 \([1,n]\) 和 \([i,i]\) 都是本原连续段,而且任意两个本原连续段要么不交,要么包含。
那么所有本原连续段就构成一棵树形结构(可以理解为将 \(n\) 个平凡本原连续段 \([i,i]\) 不断向上 merge 的结果。),树上每一个点 \(u\) 对应一个本原连续段 \([l,r]\)(所以下文可能会用树上一个节点代表一个连续段),设其值域为 \(f(l,r)\)。对于树上的非叶子节点 \(u\),设其儿子构成的有序序列为 \(S_u\),从中选出一段称作儿子区间。
有两种平凡的儿子区间是连续的:单个儿子组成的儿子区间和所有儿子组成的儿子区间。
将树上的点分为两类点。对于点 \(u\):
- 若 \(u\) 的任何非平凡儿子区间都不是连续段,则称 \(u\) 为析点。
- 否则 \(u\) 的任何非平凡儿子区间都是连续段,则称 \(u\) 为合点。
证明:
考虑一个非平凡儿子区间,如果它是连续段,那么由于它不是本原的,所以它应该和另一个连续段相交。
那么对于一个非析点,考虑它的某一个是连续段的非平凡儿子区间,这个区间肯定要和另一个区间相交,可知交出来后的三个小区间是有序的,然后再考虑这两个区间的并,它肯定也是一个连续段,要再和另一个区间相交……如此反复下去可以得到覆盖整个儿子序列的许多个区间,他们的交划出来的小区间都是有序的。而且这些小区间肯定也是连续段,对它们再执行上述操作,即可得到整个儿子序列是有序的。然后就可知任何一个非平凡儿子区间都是连续段。
特别地,只有两个儿子的节点和叶子节点我们也把他们当作合点。
建出来这棵树有什么用呢?
对于排列 \(p\) 中的任意一个连续段 \(A\),若它不是树上的一个节点,那么它也肯定能被表示成树上某个节点的某个儿子区间。
首先 \(A\) 肯定能被表示成若干个本原连续段的并。然后再结合本原连续段的定义即可证明。
而你发现只有合点的儿子区间才有可能是连续段(准确地说,析点的平凡儿子区间也是连续段,不过它们本来就是树上的节点),于是得出结论:树上的节点和合点的非平凡儿子区间一一对应了序列中的所有连续段。
析合树还有一些其他性质:
- 对于一个析点,它的儿子个数一定大于等于 \(4\),因为小于小于等于 \(3\) 个儿子一定会产生一个非平凡儿子区间。而对于任意 \(n\geq 4\),可以构造出有 \(n\) 个儿子的析点。对于 \(n\) 为偶数可以构造儿子分别为 \(2,4,\cdots,n,1,3,\cdots,n-1\),对于 \(n\) 为奇数可以构造儿子分别为 \(4,6,\cdots,n-1,1,3,\cdots,n,2\)。
- 不同的连续段情况一一对应着不同的析合树,且任意一棵有 \(n\) 个叶子,析点儿子个数 \(\geq 4\),合点儿子个数 \(\geq 2\) 的析合树都能找到至少一个排列与其对应(按第一条性质方法构造即可),那么我们可以通过计数析合树来计数所有排列中不同的连续段情况数。
一些个人瞎扯
析合树上一个点和它的儿子有什么关系?如何自上而下直观地找到每个点的所有儿子?
对于一个非叶子节点 \(u\),设其对应本原连续段 \([l,r]\),考虑把其划分成尽量多的段 \([l_1,r_1],\cdots,[l_k,r_k]\),使得它们任意一个前缀在值域上也是 \(f(l,r)\) 的前缀,即对于任意的 \(i\in [1,k]\),\(\bigcup_{j=1}^i f(l_j,r_j)\) 连续且是 \(f(l,r)\) 的一个前缀,换言之 \([l_1,r_1],\cdots,[l_k,r_k]\) 是升序的。
同样地,我们考虑把 \([l,r]\) 划分成尽量多的段 \([l_1',r_1'],\cdots,[l_{k'}',r_{k'}']\),使得它们任意一个前缀在值域上是 \(f(l,r)\) 的后缀,即对于任意的 \(i\in [1,k]\),\(\bigcup_{j=1}^i f(l_j,r_j)\) 连续且是 \(f(l,r)\) 的一个后缀,换言之 \([l_1',r_1'],\cdots,[l_{k'}',r_{k'}']\) 是降序的。
那么 \(k,k'\) 中至少有一个 \(1\)。因为若 \(k\) 不为 \(1\),则 \(k'\) 必须为 \(1\);若 \(k'\) 不为 \(1\),则 \(k\) 必须为 \(1\)。
不妨设 \(k\geq 1,k'=1\)(反过来情况类似)。
- 若 \(k\geq 2\):
考虑证明 \([l_1,r_2],\cdots,[l_k,r_k]\) 就是 \([l,r]\) 的儿子。
首先可以证明 \([l_i,r_i]\) 肯定是本原连续段,否则如果有任何一个区间与其相交的话,就能划分出更多的段。
然后发现 \([l_1,r_1],\cdots,[l_k,r_k]\) 这 \(k\) 个段中任意连续的若干个段拼起来都是连续段。这也就证明了这 \(k\) 个本原连续段中任意选连续的若干个拼起来(全选除外)都不可能是本原连续段,因为总能找到另一个连续段与它相交。
这也就证明了 \([l,r]\) 和 \([l_i,r_i]\) 之间不可能存在其他本原连续段,那么 \([l_i,r_i]\) 就是 \([l,r]\) 的儿子。
这样的 \(u\) 点就是我们上述所说的合点。
- 若 \(k=1\):
此时通过 \(k\geq 2\) 的方法无法找到 \([l,r]\) 的儿子。
考虑把 \([l,r]\) 划分成尽量多的段 \([l_1,r_1],,\cdots,[l_b,r_b]\),使得 \([l,r]\) 中除 \([l,r]\) 外的任何一个连续段都只在唯一一个段内。
或者可以理解为将 \([l,r]\) 中除 \([l,r]\) 外的所有的连续段放在一个集合内,被包含的合并到包含它的,最后集合内剩下的东西即为 \([l_1,r_1],\cdots,[l_b,r_b]\)。
显然 \(b\geq 2\),否则存在若干相交的连续段覆盖满 \([l,r]\),此时 \(k,k'\) 中一定有一个大于等于 \(2\)。
此时 \([l_1,r_1],\cdots,[l_b,r_b]\) 就是 \([l,r]\) 的儿子,因为根据生成方式可知 \([l_i,r_i]\) 肯定是本原连续段,而且他们都不被其他连续段包含。
这样的 \(u\) 点就是我们上述所说的析点。
其实你发现 \(k\geq 2\iff b=1\),\(k=1\iff b\geq 2\)。印证了每个点都满足非析即合性质。
构建析合树
个人认为构建析合树这部分讲得会比其他博客严谨一点。
考虑增量法,假设我们已经维护好了排列前 \(i-1\) 位的析合森林 \(T\)(由于前 \(i-1\) 位整一段不一定值域连续,所以是森林),且用栈 \(sta\) 依次维护了 \(T\) 中每棵树的根。现在考虑新加入第 \(i\) 位。
我们要考虑的是:原本前 \(i-1\) 位中的哪些本原段会被打断(指出现了某一个右端点为 \(i\) 的连续段与它相交)、加入第 \(i\) 位之后会新出现哪些本原段(显然新出现的本原段右端点也一定为 \(i\)),以及这两者带来的对析合森林形态的变化。
对于两者,我们都给出几点观察:
-
若在 \(T\) 中,\(u\) 已经有了父亲,那么 \(u\) 不可能被打断。
证明:若 \(u\) 被打断,则 \(u\) 的父亲 \(f\) 也会被打断,此时 \(f\) 能被表示成两个连续段 \(\overline{AB}\) 的形式,那么 \(A,B\) 中必有一者能打断 \(u\),与 \(u\) 在前 \(i-1\) 位中为本原段矛盾。
推论:我们只需考虑栈中的点被打断的情况。
-
新出现的本原段必定形如 \([sta[k],\cdots,sta[top],i]\)。
证明:若出现了新的本原段左端点不是栈中某个点的左端点的情况,它一定能被栈中的某个点打断,矛盾。
-
若 \([sta[k],\cdots,sta[top],i]\) 为连续段,则其一定是本原段。
证明:根据假设,我们知道前 \(i-1\) 位中是不存在跨过栈中的根的连续段的,所以若出现了 \([sta[k],\cdots,sta[top],i]\) 为连续段,那么它一定不会被打断,那么它就是本原段。
增量后的整个大致思路是按长度从小到大不断找到包含 \(i\) 的本原段(即新增的本原段,它们在新的树上会构成一条祖先后代链),并以此维护出新的析合森林的形态,过程中也会考虑 \(sta\) 中哪些点被打断了。
现在给出整个算法流程:
初始时令 \(u=[i,i]\),现在我们要找到包含 \(u\) 的最小的本原段,即 \(u\) 的父亲。根据上面的观察,我们只需考虑栈顶一段点与 \(u\) 的拼接,并判断拼接后是否连续段即可。
-
若 \([sta[top],u]\) 为一连续段,那么新增节点 \(rt\) 代表它。现在考虑 \(sta[top]\) 是否会被打断:
- 若 \(u\) 能打断 \(sta[top]\)。发现这等价于原本 \(sta[top]\) 为合点,且 \(u\) 能接上 \(sta[top]\) 儿子组成的值域单调序列。此时用 \(rt\) 替换 \(sta[top]\) 即可。此时 \(rt\) 仍为合点。
- 若 \(u\) 不能打断 \(sta[top]\),那么把 \(rt\) 设为 \(sta[top],u\) 的父亲即可。此时 \(rt\) 为合点。
-
若 \([sta[top],u]\) 不为连续段。那么找到包含 \(u\) 的最小连续段,设为 \([sta[l],\cdots,sta[top],u]\)。
发现此时 \(sta[l],\cdots,sta[top]\) 一定不会被打断(仍为本原段)——否则假设 \(sta[j]\) 被打断了:
- 若 \(j<top\),那么 \([sta[j+1],\cdots,sta[top],u]\) 为连续段,矛盾。
- 若 \(j=top\),那么 \([sta[top],u]\) 为连续段,矛盾。
于是我们只需新增点 \(rt\) 表示 \([sta[l],\cdots,sta[top],u]\),并令它为 \(sta[l],\cdots,sta[top],u\) 的父亲即可。
显然此时 \(rt\) 应为析点,因为 \(sta[l],\cdots,sta[top]\) 之间不会出现连续段,而且根据假设也不会出现 \(l'>l\) 使得 \([sta[l'],\cdots,sta[top],u]\) 为连续段 。
-
若找不到合法的 \(l\),那么当前 \(u\) 就不存在父亲了,于是 \(u\) 为新析合森林的一棵树的根,把 \(u\) 加入栈顶即可。
此时 \(u\) 也不可能把栈内剩下的点给打断,否则就会出现连续段,矛盾。
对于前两种情况,我们还需将 \(rt\) 作为 \(u\) 再重新加入和讨论。
至此,我们阐述完毕了增量法的整个算法流程。
实现上,我们要优化一下第三种情况的判断,否则时间复杂度会劣化为 \(O(n^2)\)。
设 \(u=[x,i]\),我们只需判断是否存在一个左端点在 \([1,x)\)、右端点在 \(i\) 的连续段即可。
根据 “对于连续段有 \(\max-\min=r-l\)” 以及 “对于任意段有 \(\max-\min\geq r-l\)”,我们只需用单调栈和线段树维护区间 \(\max-\min-r+l\) 最小值即可。
总时间复杂度 \(O(n\log n)\)。
算法实现以及 \(O(n)\) 做法还没有,省选后再补吧。
还不知道有没有机会补……