202112-4 磁盘文件操作
25% 数据——直接模拟
我们按照题目要求进行对应操作即可,注意每一个要求执行的条件:
- 写入操作:从左往右依次执行,直到第一个不被自己占用的位置。除了第一个点就被其他程序占用以外,必然会写入。遇到自己占用,则覆盖。
- 删除操作:同时整体进行,要求所有位置都被目前程序占用。要么全删,要么不做任何更改。
- 恢复操作:同时整体进行,要求所有位置都不被占用,且上次占用程序为目前程序。要么全恢复,要么不做任何更改。遇到自己占用,则不做任何更改。
- 读入操作:读取占用程序和数值,若未被占用,则输出 0 0。
100% 数据——离散化+线段树
通过这道题的操作要求等,我们可以大致推测出这道题可能需要使用线段树。
提示
如果没什么思路,可以拿各种数据结构往上套。 例如本题,因为涉及区间修改、单点查询,对于树状数组来说负担太重,我们可以尝试其他数据结构。 如果使用平衡树,则一般是要求出第 k 大数,或者是序列翻转类问题,对于本题来说不太契合。 其他数据结构不再一一列举。综合考虑下,线段树是比较符合要求的。
考虑线段树的做法
先不考虑线段树的内存空间问题,我们分析一下如何用线段树解决这道题目。
考虑我们需要维护的量,目前已知的有磁盘位置的值、目前占用程序 id、上次占用程序 id。
在这里,我们假设一个位置未被占用和被 id 为 0 的程序占用是等价的。
-
写入操作:可以划分为找到写入右边界和直接写入两个操作。
直接写入操作就是直接的线段树区间修改, 而划分操作需要知道该区间被占用的位置是否属于将要写入的 id。 我们不妨将这个量设为 id1。
-
删除操作:可以划分为判断是否可删和直接删除两个操作。
直接删除操作就是直接的线段树区间修改, 而判断是否可删需要知道该区间所有的位置是否属于将要写入的 id。 我们不妨将这个量设为 id2,注意 id1 与 id2 的区别——是否允许包含未被占用的程序。
-
恢复操作:可以划分为判断是否可恢复和直接恢复两个操作。
该操作与删除操作类似,不过需要注意的是判断时需要判断目前占用的 id 和上次被占用的 id。
-
读取操作:可以划分为查询占用程序 id和查询值两个操作。
该操作是相对比较质朴的单点查询,当然也可以处理为区间。
通过以上分析,我们得到了需要维护的量:值、有关目前占用程序 id 的两个量、上次被占用的程序 id。我们考虑每个量针对父子之间的维护。
- 值 val:每个节点代表取值的多少,若左右子节点不同则设为一个不存在的值。因为我们是单点查询,所以不用担心查询到不存在的值的问题。
- 被占用位置程序 id1:
- 若左右子节点都未被占用,则该节点标记为未占用;
- 若左右子节点中存在不唯一节点,则该节点标记为不唯一。
- 若左右子节点中一个节点未占用,则该节点标记为另一个非空节点的标记;
- 若左右子节点都非空且相等,则该节点标记为任意一个节点;
- 若左右子节点都非空且不等,则该节点标记为不唯一;
- 被占用位置程序 id2:为了方便进行讨论,将未被程序占用节点视为被 id 为 0 的程序占用。
- 若左右子节点中存在不唯一节点,则该节点标记为不唯一。
- 若左右子节点相等,则该节点标记为任意一个节点;
- 若左右子节点不等,则该节点标记为不唯一;
- 上一次被占用程序 lid:与 id2 相同。
- 若左右子节点中存在不唯一节点,则该节点标记为不唯一。
- 若左右子节点相等,则该节点标记为任意一个节点;
- 若左右子节点不等,则该节点标记为不唯一;
解决空间问题
理解线段树的解法之后,就会出现另一个问题:空间达到了 1e9 级别,肯定会 MLE。
我们可以从另一个角度考虑:一共有 \(2\times 10^5\) 次询问,每次最多操作涉及一个区间,可以用两个端点表示。 考虑到临界处的影响,一次操作最多会涉及 4 个点 (比如原来的区间是 \([1, 10]\),我们更改了区间 \([3,5]\),那么得到的区间为 \([1,2],[3,5],[6,10]\),多出了 \(2,3,5,6\) 四个点)。 那么总体来看,涉及到的点最多有 \(2\times 10^5\times 4 = 8\times 10^5\) 个。
我们可以维持这些点的相对大小关系,而将其投影到一个值域较小的区域,就可以减少空间占用了。这种方法称为离散化。
离散化
把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。 离散化本质上可以看成是一种哈希,其保证数据在哈希以后仍然保持原来的全/偏序关系。
离散化的一般性步骤:
- 统计所有出现过的数字,在知道确切上界的时候可以用数组,不清楚情况下可以用 vector;
- 对所有的数据排序 (sort)、去重 (unique);
- 对于每个数,其离散化后的对应值即为其在排序去重后数组中的位置,可以通过二分 (lower_bound) 确定。
这道题允许我们提前将所有可能出现的数记录下来(当然不是所有的题目都允许这样),所以这道题就解决了。 线段树节点的个数与询问个数成比例,时间复杂度 \(\mathrm{O}(k\log k)\)。
代码实现
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 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
|
100% 数据——动态开点线段树
在上一题的做法中,我们需要先读入所有的数据并进行离散化处理,之后再执行主要的算法过程。 但不是所有的题目都可以在执行主要的算法过程前得到所有的输入数据。
离线算法
要求在执行算法前输入数据已知的算法称为离线算法。 一般而言,如果没有对输入输出做特殊处理,则可以用离线算法解决该问题。
在线算法
不需要输入数据已知就可以执行的算法称为在线算法。 一般而言,如果对输入输出做特殊处理(如本次的询问需要与上次执行的答案进行异或才能得到真正的询问),则只能用离线算法解决该问题。
对于一道能用离线和在线算法解决的题目,如果出题人对数据进行了加密处理,导致只能使用在线算法,则我们称这道题是强制在线的。
离散化需要事先知道所有可能出现的数,所以是离线算法。如果要强制在线,就需要另一种思路。
同样,从询问涉及的点有限出发,我们考虑最多能涉及线段树上点的个数。 线段树的高度为 \(\mathrm{O}(\log m)\),假设每个涉及查询的点都到达了线段树的叶子结点, 且不考虑根到任意两个结点之间重复的节点,则总共涉及的线段树节点数的个数为 \(\mathrm{O}(k\log m)\)。 所以我们只需要为用到的节点开辟空间即可。
针对一般的线段树,我们是预先建好了整棵线段树(build 函数), 每个线段树节点的左右子节点编号与其本身编号都是对应的(通常一个子节点是父结点的二倍,而另一个子节点则相差 1)。 而对于这种只为需要用到节点开辟空间的线段树,其左右子树只有在需要的时候才会被创建, 所以编号间没有特定关系,需要记录。
考虑什么时候需要开辟新结点:在初始化的时候需要开创一个根节点; 在进行修改及查询的时候,如果区间不是所要的区间,则需要开创新的节点。 有一个技巧是,在修改和查询的时候往往要下传标记(pushdown),可以在此之前检查是否需要开创节点。
代码实现
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 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
|