2022.6 杂题
P6105 [Ynoi2010] y-fast trie
维护一个多重集 \(S\),表示当前集合中的元素 \(\bmod C\) 的值。
设最优的那两个元素是 \(x,y\),有两种情况:\(x+y\ge C\) 和 \(x+y<C\)。对于第一种情况,把 \(S\) 中的最大值和次大值加起来即可。
对于第二种情况,考虑对每个数 \(a\) 维护 \(\operatorname{best}(a)=b\),表示最大的 \(b\in S\) 使得 \(a+b<C\) 且 \(a\) 和 \(b\) 不是同一元素。那么这种情况的答案就是 \(\max_{a\in S} \{a+\operatorname{best}(a)\}\)。用另一个多重集 \(S'\) 维护所有的 \(a+\operatorname{best}(a)\)。
但这种做法会导致加入、删除一个元素时,可能改变 \(\mathcal{O}(n)\) 个元素的 \(\operatorname{best}\)。考虑只维护双向的最优匹配,也就是说,仅当 \(\operatorname{best}(\operatorname{best}(a))=a\) 时,才把 \(a+\operatorname{best}(a)\) 加入 \(S'\),这样对于每个数只存在至多一个匹配。时间复杂度 \(\mathcal{O}(n\log n)\)。
思路挺简单的,但代码细节比较多。代码。
P3215 [HNOI2011] 括号修复 / [JSOI2011] 括号序列
我的做法比较麻烦。在一段括号序列中删除掉匹配的括号后,剩下的一定是若干 )
后面接着若干 (
。那么用平衡树大力维护原来剩下的 )
和 (
个数、Swap 之后的个数、Invert 之后的个数、既 Swap 又 Invert 之后的个数,一共 \(4\times 2=8\) 个值。然后直接做。设剩下了 \(a\) 个 (
和 \(b\) 个 )
,那么答案就是 \(\lceil a/2 \rceil+\lceil b/2 \rceil\)。
写错的地方:Swap 和 Invert 的标记是 ^=
而不是 =
。
更简单的做法是把 (
设为 \(-1\),把 )
设为 \(1\),那么剩下的 )
个数就是前缀 \(\max\),剩下的 (
个数就是后缀 \(\min\) 的绝对值。这样只需要记录前缀 \(\min/\max\) 和后缀 \(\min/\max\) 即可。代码。
P4097 [HEOI2013] Segment
李超线段树。我以前居然没写过,不过挺好写。
开一棵线段树 \(T\),在每个节点上记录它的“主线段”,也就是包含这个区间、并且在最高折线中的长度最长的线段。加入一条线段 \(l'\) 时,对于被它完全包含的一个线段树节点,设这个节点上原来的主线段是 \(l\),有若干情况:
- 在左端点、右端点处,\(l'\) 均比 \(l\) 低:啥也不干;
- 在左端点、右端点处,\(l'\) 均比 \(l\) 高:把主线段换成 \(l'\);
- 在中点处,\(l'\) 比 \(l\) 高:把主线段换成 \(l'\),同时在线段树的某一侧把 \(l\) 递归下去;
- 在中点处,\(l'\) 比 \(l\) 低:主线段不变,同时在线段树的某一侧把 \(l'\) 递归下去。
这样,我们在 \(\mathcal{O}(\log V)\) 个线段树节点上插入线段,每个线段树节点最多递归 \(\mathcal{O}(\log V)\) 层,总复杂度是 \(\mathcal{O}(n\log^2 V)\),并且跑得很快。代码。
还可以扩展到区间查询:再开一棵线段树 \(T'\),插入线段时就把线段的两个端点放到 \(T'\) 里。查询时,在 \(T\) 上查询区间的两个端点处的值,并且在 \(T'\) 上查询整个区间的最大值,然后把这两个答案取个 \(\max\)。
P5610 [Ynoi2013] 大学
很厉害的并查集用法。
首先,假如 \(1\) 不作为除数,那么一个数最多被除 \(\log\) 次。要做的是快速找到所有需要被除的位置,考虑开 \(V\) 个并查集,第 \(x\) 个并查集中的 \(u\) 号点,它的根指向 \(u\) 后面第一个能被 \(x\) 整除的位置。这样,除的时候只需要二分一下找到并查集中的节点,然后遍历,并且每个数 \(x\) 都最多只会被插入到 \(\operatorname{d}(x)\) 个并查集中。并查集用路径压缩实现。
时间复杂度算起来挺麻烦,反正不超过 \(\mathcal{O}(n\sqrt{V}+nd\alpha(n))\),其中 \(d\) 是最大约数个数。代码。
HDU 6315 Naive Operations
给定序列 \(\{a_n\}\) 和 \(1\sim n\) 的排列 \(\{b_n\}\),需要支持两种操作:
- \(a\) 区间加 \(1\);
- 询问某个区间 \([l,r]\) 内的 \(\sum_{k=l}^r \lfloor\frac{a_k}{b_k}\rfloor\)。
在最坏情况下答案是 \(n\ln n\) 级别的,所以只需要考虑维护出,在每次操作后,哪些位置的答案加了一。
用线段树维护所有位置距离下一次 \(+1\) 的差值,也就是 \(b_i-(a_i\bmod b_i)\)。维护区间最小值,假如最小值变到了 \(0\),说明存在一些会 \(+1\) 的位置,递归下去找到这些位置,然后更新答案、并且把这些位置的值重新设置成 \(b_i\) 即可。时间复杂度 \(\mathcal{O}((n+m)\log^2 n)\)。
LOJ 6029 [雅礼集训 2017 Day1] 市场
对于一次区间除 \(d\) 并下取整的操作,设区间最大值为 \(a\)、最小值为 \(b\),假如 \(a-\lfloor a/d\rfloor=b-\lfloor b/d\rfloor\),那么直接在这个区间上打一个减法标记即可。否则递归到两边。
这样的话,每次递归到两边都会导致 \(a,b\) 均至少减半,最多只会减半 \(\log (a-b)\) 次。
对于一次区间 \([l,r]\) 加一个数的操作,被 \([l,r]\) 完全包含的线段树节点不会受到影响,只有那些与 \([l,r]\) 有交、但不完全被包含的节点才会受到影响。而一次的影响是极差加上了一个数,新增的代价就是 \(\log\) 级别的。
所以总时间复杂度是 \(\mathcal{O}(q\log n\log |C|+n\log V)\)。代码。
P5068 [Ynoi2015] 我回来了
https://www.luogu.com.cn/blog/alan-zhao/solution-p5068。
P5586/P5350 序列
可持久化平衡树。我写的是无旋 Treap。
除了“区间复制”操作,其他操作普通平衡树都能做到。对于区间复制,我们将 Split 和 Merge 操作可持久化即可。
Pushdown 时,我们也需要复制子节点,因为当前的儿子可能是另外某个子树的。但是,如果当前节点上没有 tag,或者左、右儿子为空,那就不需要复制子节点了。
此外,可持久化平衡树的空间复杂度是 \(O(m\log n)\) 的,我们需要每 \(m/\log(n)\) 次操作重构一次平衡树,这样空间复杂度就是线性了。
代码。