析合树
解释一下本文可能用到的符号:
关于段的问题
我们由一个小清新的问题引入:
对于一个
的排列,我们称一个值域连续的区间为段。问一个排列的段的个数。比如, 的段有: 。
看到这个东西,感觉要维护区间的值域集合,复杂度好像挺不友好的。线段树可以查询某个区间是否为段,但不太能统计段的个数。
这里我们引入这个神奇的数据结构——析合树!
连续段
在介绍析合树之前,我们先做一些前提条件的限定。鉴于 LCA 的课件中给出的定义不易理解,为方便读者理解,这里给出一些不太严谨(但更容易理解)的定义。
排列与连续段
排列:定义一个
. . .连续段:对于排列
,定义连续段 表示一个区间 ,要求 值域是连续的。说得更形式化一点,对于排列 ,连续段表示一个区间 满足:
特别地,当
我们称排列
连续段的运算
连续段是依赖区间和值域定义的,于是我们可以定义连续段的交并差的运算。
定义
. . . . .
其实这些运算就是普通的集合交并差放在区间上而已。
连续段的性质
连续段的一些显而易见的性质。我们定义
证明?证明的本质就是集合的交并差的运算。
析合树
好的,现在讲到重点了。你可能已经猜到了,析合树正是由连续段组成的一棵树。但是要知道一个排列可能有多达
本原段
其实这个定义全称叫作 本原连续段。但笔者认为本原段更为简洁。
对于排列
所有本原段的集合为
显然,本原段之间只有相离或者包含关系。并且你发现 一个连续段可以由几个互不相交的本原段构成。最大的本原段就是整个排列本身,它包含了其他所有本原段,因此我们认为本原段可以构成一个树形结构,我们称这个结构为 析合树。更严格地说,排列
前面干讲这么多的定义,不来点图怎么行。考虑排列
在图中我们没有标明本原段。而图中 每个结点都代表一个本原段。我们只标明了每个本原段的值域。举个例子,结点
析点与合点
这里我们直接给出定义,稍候再来讨论它的正确性。
- 值域区间:对于一个结点
,用 表示该结点的值域区间。 - 儿子序列:对于析合树上的一个结点
,假设它的儿子结点是一个 有序 序列,该序列是以值域区间为元素的(单个的数 可以理解为 的区间)。我们把这个序列称为儿子序列。记作 。 - 儿子排列:对于一个儿子序列
,把它的元素离散化成正整数后形成的排列称为儿子排列。举个例子,对于结点 ,它的儿子序列为 ,那么把区间排序标个号,则它的儿子排列就为 ;类似的,结点 的儿子排列为 。结点 的儿子排列记为 。 - 合点:我们认为,儿子排列为顺序或者逆序的点为合点。形式化地说,满足
或者 的点称为合点。叶子结点没有儿子排列,我们也认为它是合点。 - 析点:不是合点的就是析点。
从图中可以看到,只有
析点与合点的性质
析点与合点的命名来源于他们的性质。首先我们有一个非常显然的性质:对于析合树中任何的结点
对于一个合点
对于一个析点
合点的性质不难证明。因为合点的儿子排列要么是顺序,要么是倒序,而值域区间也是首位相接,因此只要是连续的一段子序列(区间)都是一个连续段。
对于析点的性质可能很多读者就不太能理解了:为什么 任意 长度大于
使用反证法。假设对于一个点
析合树的构造
前面讲了这么多零零散散的东西,现在就来具体地讲如何构造析合树。LCA 大佬的线性构造算法我是没看懂的,今天就讲一下比较好懂的
增量法
我们考虑增量法。用一个栈维护前
- 我们先判断它能否成为栈顶结点的儿子,如果能就变成栈顶的儿子,然后把栈顶取出,作为当前结点。重复上述过程直到栈空或者不能成为栈顶结点的儿子。
- 如果不能成为栈顶的儿子,就看能不能把栈顶的若干个连续的结点都合并成一个结点(判断能否合并的方法在后面),把合并后的点,作为当前结点。
- 重复上述过程直到不能进行为止。然后结束此次增量,直接把当前结点压栈。
接下来我们仔细解释一下。
具体的策略
我们认为,如果当前点能够成为栈顶结点的儿子,那么栈顶结点是一个合点。如果是析点,那么你合并后这个析点就存在一个子连续段,不满足析点的性质。因此一定是合点。
如果无法成为栈顶结点的儿子,那么我们就看栈顶连续的若干个点能否与当前点一起合并。设
- 如果
不存在,那么显然当前结点无法合并; - 如果
,那么这就是两个结点合并,合并后就是一个 合点; - 否则在栈中一定存在一个点
的左端点 ,那么一定可以从当前结点合并到 形成一个 析点;
判断能否合并
最后,我们考虑如何处理
而且由于 P 是一个排列,因此对于任意的区间
于是我们就维护
有了上述思路,不难想到这样的算法。对于增量过程中的当前的
现在我们想知道在
但是当第
那么如果对于一个区间
因此我们对
- 找到最大的
使得 ,那么显然, 这一段数全部小于 ,于是就需要更新 的最大值。由于 是(非严格)单调递增的,因此可以每一段相同的 做相同的更新,即区间加操作。 - 更新
同理。 - 把每一个
都减 。因为区间长度加 。 - 查询
:即查询 的最小值的所在的 下标。
没错,我们可以使用线段树维护
具体的维护方法见代码。
讲这么多干巴巴的想必小伙伴也听得云里雾里的,那么我们就先上图吧。长图警告!
实现
最后放一个实现的代码供参考。代码转自 大米饼的博客,被我加了一些注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
|
参考文献
刘承奥。简单的连续段数据结构。WC2019 营员交流。
build本页面最近更新:2022/2/13 10:36:07,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:Enter-tainer, abc1763613206, countercurrent-time, diauweb, H-J-Granger, Ir1d, ksyx, NachtgeistW, ouuan, QAQAutoMaton, sshwy, StudyingFather, SukkaW, swift-zym, Xeonacid, xyjg, zyouxam
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用