Codeforces Round #740 (Div. 1 + Div. 2) 解题报告
update : Div.1 的最后两题题题解已更新
这里是 Codeforces Round #740 (Div. 2) 的解题报告 qwq,这本来是一场 Div. 1 + Div. 2 的比赛,然而由于本人过菜qaq,不会 Div. 1 的 F 题,而官方题解到 2021/8/26 的 21:00 为止仍然在 soon,去网上搜索相关题解也无果,所以就先咕着。于 2021/9/3 更新了 Div. 1 最后两题的题解
当然本次比赛的题号因为 Div. 1 + Div. 2 的形式也会有些混乱qaq
这次比赛感觉要对数据范围和时限提起注意(并且在网慢的时候一定要多开几个镜像甚至使用流量
这次赛后补题最深的感受就是“多测不清空,调试两行泪”
部分题目题解参考了官方以及其他选手的题解qwq(这次官方题解似乎是 tourist 亲自写的qwq
数据范围中 \(t\) 若无特殊说明即为数据组数。
CF1561A. Simply Strange Sort
Description
给定一个长度为奇数 \(n\) 的 \(1 \sim n\) 的排列 \(\{a_n \}\),定义一种操作 \(f(i)\) 为对于给定的 \(i\,(1 \leq i \leq n - 1)\),如果 \(a_i > a_{i + 1}\) 则两者交换位置,否则什么都不做。
考虑一种排序算法,其迭代次数从第 1 次开始,第 \(i\) 次迭代会做以下事情:
- 如果 \(i\) 为奇数,则进行操作 \(f(1), f(3), \cdots, f(n - 2)\);
- 如果 \(i\) 为偶数,则进行操作 \(f(2), f(4), \cdots, f(n - 1)\).
可以证明在有限次迭代后,\(\{a_n\}\) 总是可以被排序成升序的,问如果要使 \(\{a_n\}\) 成为升序,需要上述算法迭代多少次?多组数据。
数据范围 \(t \leq 100, 3 \leq n \leq 999.\)
Solution
由于数据范围比较小,考虑模拟,如果要估计复杂度的话,我们可以看到对于一个完全降序的排列 \(n, n - 1, \cdots, 1\),算法会迭代 \(n\) 次,所以对于部分升序的排列,只会使迭代次数有减无增。当然这只是一个比较感性的证明,正式的证明可以看 Wikipedia page 上的相关证明(似乎这个算法和一个叫 Odd-even sort 的算法很相近)
综上直接模拟的时间复杂度为 \(O(tn^2)\)。
Code
CF1558A. Charmed by the Game
Description
Alice 和 Borys 在打网球,网球有许多局,每局都有一人发球,两个人轮流发球,一人一局。
每一局都有一个胜者,如果胜者为发球者,则称破发,反之则称保发。
现在已知 Alice 赢了 \(a\) 局 Borys 赢了 \(b\) 局,但不知道两人谁先发球以及两人具体赢了哪局,问所有可能的破发次数,升序输出。
数据范围 \(t \leq 10^3, a, b \leq 10^5\)
Solution
不妨将问题转化为两个 01 串的匹配问题,比如用 0 表示 Alice 胜,1 表示 Borys 胜,这样我们可以用 01 串表示这 \(a + b\) 局的胜负情况(简称为胜负串),同样的我们也可以用 01 串表示这 \(a + b\) 局的发球情况(简称为发球串),考虑让胜负串和发球串匹配,两个串不同的位数即为破发次数。
由于我们不知道谁先发球,所以我们要让胜负串和两个不同的发球串分别匹配(一个是以 1 开头的 0101…,一个是以 0 开头的 1010…),而对于胜负串我们只知道其中有 \(a\) 个 0 和 \(b\) 个 1。我们考虑如何找破发次数所有可能的取值,我们发现假设胜负串的某两位和发球串相同,我们可以交换胜负串的那两位,使得破发次数增加 2,所以我们只需找到最小的破发次数即可。
显然对于确定的发球串,最小的破发次数是发球串中 0 的个数与胜负串中 0 的个数(即 \(a\))之差的绝对值。由于两个串的长度是确定的,均为 \(a + b\),所以对于已经确定了的发球串可知其中 0 的个数为 \(\frac {a + b} {2}\)(当然,如果 \(a + b\) 为奇数的话要分为 \(\lfloor \frac {a + b} {2} \rfloor, \lceil \frac {a + b} {2} \rceil\) 两种情况分别计算)。从最小的破发次数每次加 2 一直加到超过 \(\min(a, b)\) 前,均为破发次数的可能值。
Code
CF1561C. Deep Down Below
Description
有一个游戏,需要玩家进到 \(n\) 个洞穴中去杀死怪物,你一开始有一定值的力量,每个洞穴有若干怪物,其中第 \(i\) 个洞穴内有 \(k_i\) 个怪物。
每一个怪物有一个装甲值,当你的力量值严格大于怪物的装甲值时,你才能杀死他,每杀死一个怪物,你的力量值增加 1。第 \(i\) 个洞穴的怪物的装甲值分别为 \(a_{i, 1}, a_{i, 2}, \cdots, a_{i,k_i}\),一旦你进入到一个洞穴,你必须按给定的顺序杀死所有怪物才可离开该洞穴,如果你无法杀死某个怪物,你就会输掉游戏。
当所有洞穴中的怪物都被杀死后,玩家就通过了本关,问玩家过关需要的最小力量值。多组数据。
数据范围 \(t \leq 10^5, n \leq 10^5, a_{i, j} \leq 10^9, \sum k_i \leq 10^5\).
Solution
我们发现对于一个洞穴,我们必须按照给定的顺序杀死怪物,所以我们可以知道进入洞穴 \(i\) 需要的最小力量值为 \(\max(a_{i, j} - j + 2)\),求出进入各个洞穴的最小力量值后,考虑贪心,我们将洞穴按照最小力量值从小到大排序,然后从需要力量值最小的洞穴开始进入,按照洞穴的顺序累加杀死怪物获得的力量值,如果有一个洞穴需要的力量值太多,以至于我们即使杀死了之前所有洞穴的怪物,也无法增加力量值,那么我们更新答案,让答案变为该洞穴需要的力量值减去之前进入所有洞穴获得的力量值之和(初始答案为排序后第一个洞穴需要的最小力量值)。
考虑贪心的正确性,显然我们尽可能多杀一些怪物会使我们力量尽可能多的增长,以至于在面对需要更高力量值的洞穴时,我们可以凭借杀死怪物获得的力量值而不是增加初始的力量值去杀死里面的怪物,所以这样贪心是正确的。
Code
CF1561D1 Up the Strip (simplified version)
Description
现在有 \(n\) 阶楼梯,假设你在第 \(x\,(x > 1)\) 阶,你有两种下楼梯的方法:
- 选择一个整数 \(y \in [1, x - 1]\),直接走到第 \(x - y\) 阶;
- 选择一个整数 \(z \in [2, x]\),直接走到第 \(\lfloor \frac{x} {z} \rfloor\) 阶。
现在问从第 \(n\) 阶走到第 1 阶有多少种不同的下楼梯方案,答案对 \(m\) 取模。注意使用第二种下楼梯的方法时,如果选择的 \(z\) 不同,即使使用该方法后走到的楼梯阶数相同,也算两种不同的方案。
数据范围 \(2 \leq n \leq 2 \times 10^5, 10^8 \leq m \leq 10^9\),\(m\) 为质数。
Solution
我们设 \(f(i)\) 表明从第 \(i\) 阶楼梯走到第一阶楼梯的方案数,分别考虑两种下楼梯的方法,我们不难列出转移方程:
显然前一部分可以使用前缀和优化 \(O(1)\) 转移,后一部分可以使用除法分块 \(O(\sum \sqrt{i})\) 解决,总时间复杂度 \(O(n \sqrt{n})\)。
Code
CF1558B. Up the Strip
Description
本题和上一题题目相同,只是数据范围有变。
变化的数据范围 \(2 \leq n \leq 4 \times 10^6\).
Solution
由于 \(n\) 最大可至 \(4 \times 10^6\) 所以 \(O(n \sqrt{n})\) 的算法会超时。考虑优化,发现复杂度瓶颈在于转移后一部分式子的计算,我们可以反过来考虑 \(f(j)\) 在后面一部分式子中被累加到了哪些 \(f(i)\,(i > j)\) 中,不妨设 \(j = \lfloor \frac {i}{k} \rfloor\),即 \(i = j \cdot k + r,r \in [1, k)\),显然对于一个 \(j\) 会对区间 \([j \cdot k, j \cdot k + k)\) 内所有数贡献一次。我们可以使用差分对一个区间 \(O(1)\) 累加 \(f(j)\) 的贡献。
时间复杂度显然为 \(O(\sum_{i = 1}^n \limits \lfloor \frac {n}{i} \rfloor) = O(n \log n)\),而本题时限为 6s 可以使用此算法通过。
Code
CF1558C. Bottom-Tier Reversals
Description
给定一个长度为奇数 \(n\) 的 \(1 \sim n\) 的排列 \(\{a_n \}\),你需要将其排序为升序,每一次操作你可以选择 \(\{a_n \}\) 的一个长度为奇数的前缀,然后将它反转。请给出任意一种使用该操作次数不超过 \(\frac {5n}{2}\) 的排序方案,如果不存在这样的方案,输出 -1,多组数据。
数据范围 \(t \leq 100, 3 \leq n \leq 2021, \sum n \leq 2021\).
Solution
我们发现在反转一个前缀时,假设选择的前缀长度为 \(j\),则反转时 \(a_i\) 与 \(a_{j - i + 1}\) 会交换位置,因为选择的 \(j\) 必须是奇数,所以 \(i\) 与 \(j - i + 1\) 奇偶性相同,所以所有数最终位置和初始位置的奇偶性也必定相同。所以如果有解则每一个 \(a_i\) 与 \(i\) 的奇偶性必须相同。
接下来我们考虑如何排序,考虑如果 \(a_{n - 1} = n - 1\) 且 \(a_n = n\),则我们就不需要再管这两个数,直接去解决规模为 \(n - 2\) 的子问题,否则我们可以:
- 找到 \(n\) 所在的位置 \(x\),反转 \([1, x]\) 这个前缀,此时 \(n\) 到达了位置 1;
- 找到 \(n - 1\) 所在的位置 \(y\),反转 \([1, y - 1]\) 这个前缀,此时 \(n\) 到达了位置 \(y - 1\);
- 再反转 \([1, y + 1]\) 这个前缀,此时 \(n - 1\) 到达位置 2,\(n\) 到达位置 3;
- 反转 \([1, 3]\) 这个前缀,此时 \(n - 1, n\) 分别到达位置 2 和位置 1;
- 最后反转 \([1, n]\) 这个前缀,此时 \(n - 1, n\) 均归位。
注意由于前面奇偶性相同的结论 \(x\) 总是奇数,而 \(y\) 总是偶数。其实这个排序最关键的是如何让 \(n - 1\) 与 \(n\) 贴在一起。正好这是 5 步,每一次这样的操作正好排序 2 个数,所以构造出来的方案的步数不会超过 \(\frac {5n} {2}\).
Code
CF1558D. Top-Notch Insertions
Description
对于一个长度为 \(n\) 的数组 \(\{a_n \}\) 做插入排序,即依次考虑 \(a_2, a_3, \cdots, a_n\) 每一个元素 \(a_i\):
- 如果 \(a_i \geq a_{i - 1}\),则跳过该元素,直接考虑下一个元素。
- 否则找到一个最小的 \(j\),使得 \(a_i < a_j\),然后使 \(a_i\) 插入到 \(a_j\) 前,并重新排列标号。 我们记这次插入为 \((i, j)\).
现在给定数组的长度 \(n\) 和 \(m\) 个插入操作 \((x_i, y_i)\),问有多少种数组只包含 \(1 \sim n\) 中的整数且做插入排序恰好只进行给定的 \(m\) 次插入。答案对 998244353 取模,多组数据。
数据范围 \(t \leq 10^5, 2 \leq n \leq 2 \times 10^5, 0 \leq m < n, \sum m \leq 2 \times 10^5\).
Solution
这么多排序题,这是排序 round 吗 qaq
直接计数较为麻烦,考虑转化问题,发现我们可以倒序操作 \(m\) 次插入,还原数组原本的样子,所以我们只需考虑排序后的数组有多少种即可,由于排序后最终数组是单调不减的,故对于排序后的数组我们总是可以找到一个诸如 \(a_{b_1} \leq a_{b_2} \leq \cdots \leq a_{b_n}\) 的限制,其中 \(b_i\) 是 \(a_{b_i}\) 这个数在排序前的原始位置。由上文可知,我们不用关心数变动的位置,只需求向这 \(n\) 个位置填数的方案有多少种即可。
但是上文的限制没有考虑到插入的因素,因为每次插入都保证 \(a_i\) 严格小于 \(a_j\),所以在限制中有一些小于等于号要被替换为小于号,我们先考虑假设需要替换的个数为 \(c\),先考虑 \(c = 0\) 时,根据隔板法可知方案数为 \(\dbinom {2n - 1} {n - 1}\) ,而 \(c > 0\) 时,相当于消去了 \(c\) 个隔板和 \(c\) 个物品,方案数即为 \(\dbinom {2n - 1 - c}{n - 1 - c}\) 也即 \(\dbinom {2n - 1 - c}{n}\).
对于如何使用隔板法得出这个答案,我们发现我们填数时不需要关注顺序,只需关注每个数出现的次数,我们可以将数的总数 \(2n\),将这 \(2n\) 个元素分为 \(n\) 组,每一组至少有一个元素,第 \(i\) 组的元素个数减 1 即对应 \(i\) 在此方案中出现的次数。至此我们就可用隔板法得出上述答案。
下面我们考虑计算 \(c\) 的值,发现只有前面被插入数字的位置其小于等于号会被替换,我们考虑逆序这些插入,这样我们总是能从一个有序的终态开始。我们考虑插入操作 \((x_i, y_i)\) 被逆序后就是将一个元素从 \(y_i\) 这个位置被移出区间 \([1, i]\),由于我们是倒序考虑,我们显然不用再考虑大于 \(i\) 的位置的替换情况,因为我们之前已经处理了。
所以不妨维护一个集合 \(S\),初始有 \(1 \sim n\) 这 \(n\) 个数,每次考虑插入操作 \((x_i, y_i)\) 时,我们找到集合中第 \(y_i\) 小的数 \(p\),即本来在此次插入中被插入的位置,以及第 \(y_i + 1\) 小的数 \(q\),即此次插入位置后面的位置,这样我们就找到一组使用严格小于号连接的数,然后由于 \(p\) 会移出我们考虑的范围,我们将 \(p\) 从 \(S\) 中删除,并将 \(q\) 打上标记。如果同一个位置 \(q\) 被多次打上标记,只能使 \(c\) 增加 1,这是因为,标记的意义为对于某个 \(a_p\) 有 \(a_p < a_q\),而若设 \(p_k\) 是离 \(q\) 最近的,则不等式链 \(a_{p_1} \leq \cdots \leq a_{p_k} < a_q\) 可直接满足所有关于 \(a_q\) 的条件。最终有多少数打上了标记即为 \(c\) 的值。
我们只需一个能查询第 \(k\) 小的数据结构,这里使用的是树状数组,然后使用 set 对标记进行去重即可。
Code
CF1558E. Down Below
Description
有一个游戏,需要玩家进入 \(n\) 个洞穴中去杀死其中的怪物,除 1 号洞穴没有怪物外,每个洞穴均有一只怪物。玩家一开始有一定的力量值,第 \(i\) 个洞穴的怪物只有玩家的力量值严格大于 \(a_i\) 时才可被杀死,且杀死怪物后玩家的力量值会增长 \(b_i\),洞穴也会变成空洞穴。
玩家可以通过连接两个洞穴的 \(m\) 条隧道从一个洞穴到达其他洞穴,但玩家不能连续两次通过一个洞穴,一开始玩家在 1 号洞穴。如果玩家首次到达一个洞穴,玩家必须杀死洞穴中的怪物,才能进入下一个洞穴。数据保证每个洞穴最少有两个隧道与之相连,且不存在自环。
玩家杀死所有洞穴中的怪物后即可过关,求玩家过关所需的最小初始力量值为多少,多组数据。
数据范围 \(t \leq 100, 3 \leq n \leq 1000, n \leq m \leq \min(\frac {n(n - 1)}{2}, 2000), a_i, b_i \leq 10^9\).
Solution
虽然和前面某题很像,但其实在解法上根本没有太大的关系
看到题面发现最小值在图上难以使用贪心的方法计算,故考虑将最优化问题转化为判定性问题,即考虑二分答案。
考虑对于二分到的答案 \(p\),我们如何判断最初拥有的力量值为 \(p\) 时就可以通关。由于玩家可以在空洞穴中随便移动,玩家通过走出一条能保证玩家不会在半路上遇到杀不死的怪物的路径来增加空洞穴的数量,考虑这样的路径有什么性质。
为了方便接下来的叙述,我们设 \(u, v\) 为出发前已经是空洞穴的洞穴,当然 \(u = v\) 也是可以的,这条路径要经过 \(k\) 个不同的非空洞穴,这样的路径可以是形如:
或者是形如:
所以我们可以直接用 bfs 寻找符合条件的路径,看最后是否可以杀死所有怪物,从而判定当前答案是否合法。这非常暴力,但是由于最多可以找到 \(O(m)\) 条路径,一条路径最多包含所有 \(n\) 个洞穴,所以一次 bfs 的时间复杂度为 \(O(nm)\),进而总的时间复杂度为 \(O(nm \log \max a_i)\) . 发现 \(n, m\) 都很小,本题又开了 5s,故可以通过本题。
Code
CF1558F. Strange Sort
Description
(本题与本篇第一题题目相同,只是数据范围有变)
变化的数据范围:\(t \leq 10^4, 3 \leq n < 2 \times 10^5\).
Solution
Div.2 的 A 题加强后竟然变成了 Div. 1 的 F 题
由于数据范围已经变化,我们不可能使用模拟通过。我们需要将问题简单化,变得看上去可以求,我们发现需要迭代算法的次数无非是序列中每个数要到达它相应位置所需的迭代次数的最大值。并且我们发现,只要我们让 \(1 \sim i\) 到达了相应位置,那么 \(i + 1\) 第一次到达其相应位置后,它就不会再移动了。基于此,我们发现我们按递增的顺序枚举从 1 到 \(n\) 枚举 \(i\),每次只需关心 \(a_j \leq i\) 这些数到位所需的步数,其中取最大的步数即为我们所需的答案。
为了简化问题,我们可以考虑将序列替换为一个 01 串从而便于排序,设 \(b_i = [a_i \geq m - 1]\),这样每次枚举仅是将 \(b\) 这个 01 序列进行排序。考虑给 01 序列排序所需的迭代次数,发现我们只需关心每个 0 是否已经到位即可。我们设序列中 \(m\) 个 0 的位置为 \(1 \leq p_1 \leq p_2 \leq \cdots \leq p_m \leq n\),我们考虑如何对于序列中第 \(i\) 个 0,设它到达目标位置所需的迭代次数为 \(c_i\),在排序的过程中,它可能会被左边一个和它紧挨着的 0 卡住,要等到左边的 0 开始移动了,它才开始移动,所有 \(c_i \geq c_{i - 1} + 1\);如果不考虑卡住的情况,设其左边有 \(k_i\) 个 1,所以至少要进行 \(k_i\) 次迭代,而如果其所在位置 \(p_i\) 为奇数,那么会浪费第一次迭代,所以 \(c_i \geq k_i + (p_i \bmod 2)\). 当然除这两种情况以外,没有其他情况,所以 \(c_i = \max(c_{i - 1} + 1, k_i + (p_i \bmod 2))\). 当然如果第 \(i\) 个 0 在排序前已经在第 \(i\) 个位置上,则 \(c_i = 0\).
我们看当前 01 序列是否排好序,只需关注第 \(m\) 个 0 是否到达其位置上,故 \(c_m\) 即为当前 01 序列排序所需迭代次数。考虑到如果有 \(t\) 个 0 在排序前已经在其相应的位置上了(如果形式化的讲,\(t\) 是一个最大的整数使得 \(a_1\) 到 \(a_t\) 均为 0),那它们就不会对 \(c_m\) 产生贡献,我们只需从 \(t + 1\) 个 0 开始计算,则:
至此,我们只需从 1 到 \(n\) 枚举 \(m\),一开始是一个全为 1 的序列,\(m\) 每增加 1,序列中就会有一个 1 变为 0. 考虑我们每一次都要维护一个区间最大值,使用支持区间加的线段树维护即可。对于 \(t\) 我们只需维护一个指针,如果 \(t + 1\) 已经变成 0,则指针右移,直到 \(t + 1\) 的位置为 1 为止。总时间复杂度 \(O(n \log n)\).