Ynoi Easy Round 2014~2015
[Ynoi Easy Round 2014] 在太阳西斜的这个世界里
知识点:红黑树,数据结构撤销。
思维难度困难,代码难度困难,卡常难度无。
题目解法
先学会红黑树。期望一眼诈骗换成计数。根据对红黑树的了解或者样例中的图可以知道旋转会发生当且仅当双红修正时子红节点的叔节点为黑点。此时如果两个红节点共线转一次,否则转两次。另外还有黑点变红的情况,很难维护。
考虑在红黑树上维护这个东西。首先维护的肯定是子树内信息,我们把旋转挂到爷爷节点,考虑对每个点维护所有能插入这棵子树内的值造成的旋转次数的和。
我们把插入的红节点视为把儿子变红,那统计旋转的方案数就是对每个子节点为一红一黑的黑点统计其红子节点左右儿子变红的方案数,称为答案。因此,我们对黑点还需要维护它变红的方案数,对红点还需要维护它的左右儿子分别变红的方案数,因为统计贡献时由于不共线转两次一个是 \(1\) 贡献一个是 \(2\) 贡献。
对于红点,pushup 时累加子树答案,然后直接更新左右儿子变红的方案数。特别的,如果是空节点,我们需要记录这个节点的前驱(或后继),作差求出插入的节点的方案数,因为插入相当于变红。前驱可以在插入时在红黑树上走路时顺便维护。
对于黑点,pushup 时累加子树答案。黑点变红当且仅当两个儿子都是红点且孙子变红,可以直接累加子节点左右儿子变红的方案数。如果子节点是一黑一红,那么我们根据红点的左右儿子变红的方案数看是否共线确定累加系数统计到答案。如果子节点是不会产生贡献,不用管它。
这样根节点就是 \([1,10^8]\) 内插入每个数的旋转次数和。为了求一段前缀 \([1,x]\),我们考虑插入一个节点 \(x\),不进行双红修正且不统计这个节点,从这个节点无论黑红只统计左子树的贡献 pushup 到根,这就求出来 \([1,x)\) 的方案数。\(x\) 的方案数我们直接暴力模拟就行了。
记录修改到的节点,查询结束后暴力回滚修改回去即可。时间复杂度 \(O(n\log n)\)。
实现细节
主要是只统计左子树的贡献 pushup 到根这一块。虽然不考虑右子树的贡献,但是黑点 pushup 时依旧需要考虑右子节点的颜色。因为只是不算贡献,那个节点还存在。
只统计左子树的贡献 pushup 还要注意虽然插入一个 \(x\),但是这个节点实际上不应该被统计答案,需要被记为黑空节点。但这里需要更新前驱后继,不然叶子节点可能会出锅。
至于重复元素,要相信自己的红黑树会处理好的,不需要特判。
[Ynoi Easy Round 2014] 置身天上之森
知识点:线段树,序列分块。
思维难度简单,代码难度中等,卡常难度简单。推荐完成。
题目解法
线段树上只有 \(O(n)\) 个区间,因此我们考虑把这些区间全部找出来。
考虑修改的影响。我们发现,完全包含时,一次修改对所有长度相同的区间影响是一样的。于是我们考虑把长度相同的区间按左端点排序,放在一起维护。修改分别修改,查询分别查询后合并。线段树的另一个经典结论是线段树只会有 \(O(\log n)\) 种长度的区间。
修改时,对于某一个长度的区间,修改到的区间的区间一定是一个区间,是区间加操作。查询时也是同理,是区间 \(\le\) 某个数的元素数量。这是一个经典的分块问题,块内排序,二分每个块查询。
复杂度看似是 \(O(n\sqrt{n\log n}\log n)\) 的,但是可以发现线段树节点按长度升序排序每过 \(O(1)\) 层长度翻倍,数量减半,因此实际复杂度其实是 \(O(n\sqrt{n\log n})\)。
实现细节
主要是特判两端未被完全包含的区间以及区间被完全包含的情况。基本不卡常,如果被卡了就调一下块长,不知道为什么我的块长取 \(\sqrt{n}\) 时比较优,猜测是常数原因。
[Ynoi Easy Round 2014] 等这场战争结束之后
知识点:离线操作树,可撤销并查集,值域分块。
思维难度中等,代码难度简单,卡常难度简单。推荐完成。
题目解法
离散化。首先把离线操作树建出来,在离线操作树上搜索回溯,然后我们需要支持的修改操作就变为了合并两个连通块和撤销某次合并。
维护带合并值域信息首选线段树合并和启发式合并,但由于空间限制以及需要支持撤销无法使用。接着想到并查集也可以维护连通块信息,因为需要支持撤销使用可撤销并查集。
接下来考虑维护信息。由于空间限制无法使用数据结构,于是使用经典的值域分块技巧均衡时间空间复杂度。具体的,我们进行值域分块,查询时每次看能不能跳一个完整的块,如果能直接跳,否则暴力遍历块中的每一个元素对应的位置看是否在同一联通块中寻找。合并两个连通块直接逐块合并,撤销也是逐块减去贡献。时间复杂度 \(O(\frac{n^2}{B}+nB)\),空间复杂度 \(O(\frac{n^2}{B})\),其中 \(B\) 为块长。为了优化空间,我们可以把 \(B\) 开大一点,已经可以通过。
另外通过逐块处理技巧可以做到 \(O(n)\) 空间。具体的,我们跑 \(\frac{n}{B}\) 次该算法,从小到大处理每个块,每次只关注一个块内的信息,最后把多次的信息合并。原则上空间 \(O(B)\) 但瓶颈在存初始数组所以空间 \(O(n)\)。
实现细节
比较反常识的,本题块长取 \(30\) 比较优秀。因为数据比较水,我开 short 并没有爆炸,就极限卡过去了。如果之后被叉了就换逐块处理。
[Ynoi Easy Round 2014] 不归之人与望眼欲穿的人们
知识点:线段树二分,支配对。
思维难度中等,代码难度困难,卡常难度困难。
题目解法
注意到,如果区间只往左或只往右拓展,变化 \(\log V\) 次之后区间或每一位都为 \(1\),不会发生变化。联系题目问的是最短长度,自然想到存在支配对。
我们考察支配对的性质。记 \(S(l,r)\) 表示区间 \([l,r]\) 的区间或,对于区间 \([l,r]\),如果满足 \(S(l,r-1)\) 或上 \(a_r\) 不等于 \(S(l,r-1)\) 且 \(S(l+1,r)\) 或上 \(a_l\) 不等于 \(S(l+1,r)\),那么 \((l,r)\) 就是一个支配对。证明的话考虑如果不满足条件可以收缩 \(l\) 或 \(r\)。
回答询问只需要维护支配对需即可。需要一个带删数据结构,还要支持前缀查最大值。于是用可删堆维护每一个区间或的支配对长度的可重集,用线段树维护一段或和或区间的最大值。修改时单点修,查询时线段树二分即可。
先考虑不带修怎么找出支配对。我们顺序遍历每一个位置 \(r\),使用线段树二分找出所有位置 \(r\) 左边满足 \(S(l+1,r)\) 或上 \(a_l\) 不等于 \(S(l+1,r)\) 的位置,然后再判断另一个条件是否满足。
接下来考虑带上修改。我们先找出所有左边满足 \(S(l+1,r)\) 或上 \(a_l\) 不等于 \(S(l+1,r)\) 的位置,再找出所有右边满足 \(S(l,r-1)\) 或上 \(a_r\) 不等于 \(S(l,r-1)\) 的位置。一次修改中只有以这些位置加上修改的位置为端点的区间可能会产生支配对的变化,必要性显然,充分性考虑假设存在一个跨修改位置的区间没有被计算,但它的左右端点一定满足条件,所以会被计算,矛盾。我们两两组合这些位置,再判断这两个条件,在维护支配对的线段树上插入删除。
时间复杂度左右各 \(\log V\) 个位置,加上 \(\log V\) 的线段树,时间复杂度 \(O(q\log^3 V)\)。
实现细节
细节有但不多,主要难度在于卡常,卡着卡着码长就上去了。
主要的卡常有四个:第一个是线段树二分可以一次把所有位置找出来;第二个是考虑线段树二分时记录除初始位置的区间或,这样两次就能共用一次线段树二分的信息;第三个是修改前后相同的区间直接不管,不要删除再插入;第四个是线段树底层分块。
如果还不行就加上较小的区间用桶记录而不是在线段树上维护,两倍空间线段树,尝试手写堆等等。
本题是唯一一道让我卡常破防去看题解代码的题,我觉得这题是这套题中最难的题。
[Ynoi Easy Round 2014] 人人本着正义之名
知识点:平衡树。
思维难度简单,代码难度困难,卡常难度中等。
题目解法
考虑 \(3\sim 6\) 操作对序列的影响,通过简单模拟不难发现 \(3\sim 6\) 操作本质上就是让一段 \(0\) 或 \(1\) 的左右边界扩张或收缩一个位置。而 \(1,2\) 操作也可以转化为修改与覆盖区间相交的连续段的左右边界,再插入一个新的。\(7\) 操作就是先找出被完全包含的连续段,再加上左右相交的连续段分别统计贡献。需要特判被包含。
因此我们考虑维护 \(0\) 或 \(1\) 的极长连续段,用平衡树的节点维护连续段,每个节点维护子树内的和与这个连续段的左右端点与权值。
\(3\sim 6\) 操作需要考虑通过打区间左右扩张收缩懒标记实现。为了打懒标记时更新子树内信息,我们还需要记录子树内 \(0,1\) 的极长连续段个数。这样就可以通过偏移量乘上连续段个数更新子树信息。需要特判被包含。时间复杂度 \(O((n+m)\log(n+m))\)。
此时又出现了一个问题,可能会有一个连续段被不断收缩导致消失,此时不仅需要删除这个连续段,还需合并它前一个和后一个连续段。但每出现一次这种情况,就会使平衡树中减少两个节点。即使有覆盖操作,每次至多增加两个连续段。因此这种删除操作至多只会进行 \(n+m\) 次,每次在 FHQ 上找节点复杂度为 \(O(\log(n+m))\),因此不影响总时间复杂度。
实现细节
本题的边界与细节非常多,基本不是给人瞪眼调的。建议先写出初稿,然后写一个对拍,一次次慢慢调。
可以写两个分裂函数,一个按照小于等于 \(r\) 分裂,另一个按照小于 \(l\) 分裂,这样找完全不交区间比较容易。要特别注意各种操作中被完全包含的情况,需要特判。
删除被操作消失的区间时建议先把这些区间找出来,从小到大依次删除。不然这些消失的区间在平衡树里的位置很奇怪,有可能删除后加入的新区间在平衡树中的位置会出问题。
常数方面我的 Splay 比 FHQ-Treap 慢,因此我选用了 FHQ-Treap。比较显著的优化有 pushdown 的时候如果没有标记直接返回和按照讨论区的写法手写 FHQ-Treap 的随机函数。
[Ynoi Easy Round 2014] 长存不灭的过去、逐渐消逝的未来
知识点:线段树。
思维难度困难,代码难度中等,卡常难度无。
题目解法
主要难点在于发现这玩意真的可以线段树维护。
先不考虑括号,对于线段树的每一个节点,我们维护这个节点对应区间内的表达式化成最简的式子。但运算有优先级问题,且可能出现两端区间中最前面的数和最右边的数拼起来的情况,因此对于区间最左或最右的数字不能化简。
先不考虑减法,只有加法和乘法。注意到,如果出现 \(+x+y+,\times x\times y\times,+x\times y+\) 的情况,中间的一部分就可以化简。而如果出现 \(\times x+y\times\),无论其左右添加哪个运算符,都会导致可以消掉。因此,此时的每个节点的信息是 \(O(1)\) 的。
减法和加法需要从左往右计算,在线段树的区间合并过程中很难做到真正从左往右计算,因此我们考虑把减法转化为加法和乘法。由于是模 \(\text{mod}\) 意义下,所以负数可以用正数表示,因此 \(-x\) 等价于 \(+(\text{mod}-1)\times x\),直接把减号替换掉就行。
然后考虑括号,其实不是问题。遇到左括号不管它,把区间分为两个部分。遇到左右括号如果合法中间一定可以被化成一个数,直接把括号去掉。设括号深度为 \(k\),每个节点的信息是 \(O(k)\) 的。
然后就是判不合法,符号后面接符号和括号后面接数字直接导致所有包含这个区间的区间不合法。如果最后的区间存在单独的左括号也不合法。
总时间复杂度 \(O(nk\log n)\),其中 \(k\) 为括号深度。
实现细节
一个比较好写的区间合并方法是写一个末尾追加字符函数,每次合并的时候就把右儿子的最简的式子从左到右一个一个插到左边。
由于需要处理左右区间的数字拼合,所以需要记录区间前后数字长度,然后乘 \(10\) 的次幂。符号后面接符号和括号后面接数字这种不合法对父节点具有传递性,可以另外开一个变量记录是否有这种不合法。最后计算答案的时候可以不写中缀表达式求值,因为只有加法和乘法,直接扫一遍就行了。
[Ynoi Easy Round 2015] 我回来了
知识点:线段树二分,调和级数。
思维难度中等,代码难度简单,卡常难度简单。推荐完成。
题目解法
期望一眼诈骗,直接维护伤害值域区间和。考虑每一个随从对某一个伤害值的贡献。我们发现,对于一个伤害值 \(d\),我们可以把整个值域分成 \(\lceil\frac{n}{d}\rceil\) 段,每个段中只要有一个血量在这个段之间的随从就有贡献,我们称这种段为激活的。根据调和级数知识,这种段的数量是 \(O(n\log n)\) 的。
添加某一个随从,相当于把所有值域包含这个随从的血量的段激活。显然激活过的段再次激活没有影响,因此激活的总次数是 \(O(n\log n)\)。
对于寻找激活的段,我们考虑线段树二分,以左端点为下标,右端点为值存进线段树。对于每个节点,维护子树内最大值。查询时在值域下标小于等于当前元素值的区间线段树二分,遇到子树内最大值小于当前元素值就直接返回。由于激活的总次数为 \(O(n\log n)\),修改操作的总复杂度是 \(O(n\log^2 n)\) 的。还需要顺便维护是哪个段。
此外还有一个问题。对于每个伤害值,被激活的段的贡献是前缀极长连续激活的段的数量。我们对每个伤害值维护答案,找激活的段时把激活的段属于哪个伤害记录下来,找完修改的段后,把每一个需要更新伤害暴力看下一个段有没有激活。由于只会增加,这部分复杂度为 \(O(n\log n)\)。
最后用树状数组维护一下伤害值域区间和即可。时间复杂度 \(O(n\log^2n+m\log n)\)。
实现细节
本题的卡常都很套路。我用到的卡常有已经加入过的元素值特判,底层的 set 其实可以用 vector 替代,把 vector 换成链式前向星,特判长度较小的段。如果还是被卡了考虑线段树底层分块和两倍空间线段树。
[Ynoi Easy Round 2015] 纵使日薄西山
知识点:线段树。
思维难度简单,代码难度简单,卡常难度无。推荐完成。
题目解法
有一个观察是当我们对位置 \(x\) 进行一次操作后,就不可能对 \(x-1\) 和 \(x+1\) 进行操作,因为一次操作这三个位置相对大小不变,而 \(x-1\) 和 \(x+1\) 在其他操作中只能减小,\(a_x\) 始终大于等于 \(a_{x-1}\) 和 \(a_{x+1}\)。可以得到另一个性质是位置 \(x\) 一定会被操作 \(a_x\) 次。
因此,每个元素有三种状态:自己进行操作消掉,被左边元素的操作消掉,被右边元素的操作消掉。这样,一个区间的信息只与左右区间的开头结尾元素有关,且只有可能会出现开头结尾元素消去状态在这个区间内不需要管,可以区间合并,于是考虑线段树。
对每个区间维护四个状态下的操作总次数与区间内最左最右选的数的位置,四个状态分别对应区间开头和结尾的元素管不管。合并区间时,对一个状态,左右元素管不管分别继承左右儿子状态,左儿子的右端点左儿子管,右儿子的左端点右儿子管。但这样可能会导致左儿子的右端点和右儿子的左端点同时被选,通过区间内最左最右选的数的位置判定是否连续后选较大的,较小的元素对应的区间改为取对应位置不用管的状态。
时间复杂度 \(O(n\log n)\)。
实现细节
边界情况为叶子节点处左右端点不管,这种情况下没有选的元素所以最左最右选的数的位置,因此设为负无穷。
可以把每个状态左儿子右端点管不管,右儿子左端点管不管对应的状态记录下来,就可以用 for 循环处理所有状态,减少码量。
[Ynoi Easy Round 2015] 即便看不到未来
知识点:树状数组,扫描线。
思维难度困难,代码难度简单,卡常难度简单。推荐完成。
题目解法
这个东西线段树不好维护,由于可以离线,考虑扫描线。把每个询问挂到右端点,从左到右扫,维护每一个左端点的信息。由于连续段长度 \(k\) 很小,我们考虑开 \(k\) 个数据结构维护每个左端点的每个长度的值域连续段数量的信息。
考虑右端点左移,即加入一个元素 \(a_i\) 的影响。我们发现,影响为把这个元素值域上左边的连续段和右边的连续段拼起来。由于需要统计的区间长度很小,因此我们只需要处理对 \(b\in[a_i,a_i+k],c\in[a_i-k,a_i]\) 组成的 \([a_i+1,b]\) 和 \([c,a_i-1]\) 的区间拼合。注意两端区间可以为空。
有一个比较显然的观察是加入 \(a_i\) 只会影响上一次 \(a_i\) 出现之后的位置。我们考虑枚举每一个值域上的空位,这样 \(b,c\) 都需枚举到 \(k+1\)。然后,一个空位在上一次出现的位置之前这个空位对应的元素才会有贡献。对于某一个位置,影响为计算所有有贡献的元素,减去 \(a_i\) 值域上左边的连续段的长度,减去右边的连续段的长度,再加上左右拼合后连续段的长度。
用元素贡献到序列上的位置比较困难,因此我们考虑对序列上的位置求拼合的贡献。把每个空位按照位置排序,从右往左依次扫每一个空位。开桶记录有贡献的元素,暴力求左右连续段边界,进而求出长度。由于只增加元素,左右连续段边界只会扩张,所以复杂度是对的。最后就是 \(k\) 个树状数组维护区间加,单调查询。
时间复杂度 \(O(nk\log n+m\log n)\),由于树状数组超小常数可以通过。
题目解法
实现基本没细节,轻微卡常。主要的卡常有用宏定义代替 lowbit 函数,树状数组内不需要取模,用 putchar 输出。
[Ynoi Easy Round 2015] 此时此刻的光辉
知识点:莫队,Pollard-rho。
思维难度简单,代码难度简单,卡常难度中等。
题目解法
有一个约数个数公式,设数 \(n\) 的质因数分解形式为 \(\alpha_1^{p_1}\alpha_2^{p_2}\dots \alpha_m^{p_m}\),则数 \(n\) 的约数个数为 \(\prod_{i=1}^m(p_i+1)\)。由于查询的是区间积的约数个数,因此我们维护序列上每个数的质因数,再维护区间每个质因数的数量和,就可以进行查询。
预处理出每个数的质因数可以用 Pollard-rho 做到 \(O(nV^{\frac{1}{4}}\log V)\)。由于每个数至多有 \(O(\log V)\) 个质因数,所以这个维护的复杂度是可以接受的。
区间类似值域信息显然线段树维护不了,可以离线,考虑莫队。加入删除直接对该元素的每个质因数离散化后维护桶用逆元撤销贡献依次修改每个质因数再把贡献乘回来。由于每个数至多有 \(O(\log V)\) 个质因数,这部分时间复杂度为 \(O(n\sqrt{n}\log V)\)。
总时间复杂度 \(O(nV^{\frac{1}{4}}\log V+n\sqrt{n}\log V)\),显然跑不满且我的 Pollard-rho 实现比较优秀,就直接过了。
实现细节
对于 Pollard-rho 的卡常有 Miller-rabin 只用三个底数,使用 Binary GCD,将 Miller-rabin 的判断过程倒着做,可以直接把上一次结果自乘,少一个快速幂的 \(\log\)。
对于其他地方的卡常,有莫队奇偶排序和预处理 \(O(1)\) 逆元。似乎并不需要用到题解里说的根号分治,但如果被卡了可以去学。
[Ynoi Easy Round 2015] 盼君勿忘
知识点:莫队,根号分治,光速幂。
思维难度中等,代码难度简单,卡常难度无。推荐完成。
题目解法
首先需要转化贡献体,考虑值域上某个数被计算了多少次。总个数是容易的,正难则反,考虑没有出现这个数的子序列个数。假设长度为 \(len\) 的区间内这个数出现了 \(cnt\) 次,则被计算的次数为 \(2^{len}-2^{len-cnt}\)。
和区间每个数出现次数有关,可以离线,考虑莫队。注意到题目每一个询问模数不同,显然不可以直接维护答案变化。虽然莫队修改有 \(O(n\sqrt{n})\) 次,但是询问只有 \(n\) 次,考虑莫队求出区间每个数出现次数,每次 \(O(\sqrt{n})\) 回答询问。
和每个数出现次数有关,考虑频数根号分治。对于出现次数 \(\gt\sqrt{n}\) 的数,不会超过 \(\sqrt{n}\) 个,我们暴力计算每一个的贡献即可。对于出现次数 \(\le \sqrt{n}\) 的数,因为每个数贡献只与出现次数有关,所以我们记录出现次数相同的数的出现次数,枚举出现次数依次计算贡献。
莫队维护出现次数的时候顺便维护一下 \(\gt\sqrt{n}\) 的数和 \(\le\sqrt{n}\) 的数转化,然后按照上述方式计算。
注意到每次算 \(2\) 的幂次带一个 \(\log\),会被卡,所以我们考虑光速幂。时间复杂度 \(O(n\sqrt{n})\)。
实现细节
我实现的时候用了莫队奇偶排序,以及用 unordered_set 存出现次数 \(\gt\sqrt{n}\) 的数。如果还是被卡常了尝试减少取模次数。
[Ynoi Easy Round 2015] 世上最幸福的女孩
知识点:KTT。
思维难度简单,代码难度中等,卡常难度困难。
题目解法
全局加可以等价于区间加,区间加区间最大子段和是 KTT 的经典应用。但 KTT 只能加正数,因此我们需要通过某种操作使每一次询问加的都是正数。
我们没有使用可以离线的条件,考虑离线。把每一个询问按照这个询问时整个序列加了多少数升序排序,把初始数组加上排序后第一个询问时整个序列加了多少数。这样查询下一个询问的时候由于是升序排序,变化量一定为正数,就可以 KTT 直接维护了。
由于是全局加,时间复杂度 \(O((n+m)\log^2n+m\log n)\)。
实现细节
本题 KTT 做法复杂度较劣,需要卡常。主要的卡常有预处理除法倒数做乘法;求答案时不要直接用合并节点的 merge,专门写一个,可以省去求交点的时间;交点非整数时向下取整不要向上取整,可以减少三次加减法运算;两倍空间线段树。如果还是被卡了考虑底层分块。

浙公网安备 33010602011771号