美团杯2020 简要题解

update:忘记写测试赛题解了,虽然只有一道题(

  • 测试赛A

考虑最后的序列长什么样子。显然它的长度是 \(2^n-1\),并且关于第 \(2^{n-1}\) 个字符是左右对称的。将中间的字符去掉之后,两边就都成了原串的后 \(n-1\) 个字符如此操作后的样子。因此,设 \(f_{i, j}\) 表示前 \(i\) 个字符中,以 \(j\) 结尾的不同子序列个数,转移的时候我们令所有相同的子序列在它最后一次出现的位置被取到,因此 \(f_{i+1, s_{i+1}}=\sum_{j\in \mathrm{alphabet}} f_{i, j}, f_{i+1, k}=f_{i, k}\)。由此可以写出转移矩阵,直接从后往前倍增地转移即可。


由于我水平有限(虽然两位队友都稳得不行),所以并不能写出全部题目的题解(

题目按照通过人数排序,这样可以让咕掉的题尽量排在后面(

  • N

热身题,队长说直接手推即可。实在不行就猜几个上去,实际上可能要猜的很少。

  • A

签到题,标程的做法是 dp,根据每个位置的字符判断从哪个方向转移。实际上,注意到字符串的结构是前面三个 x,后面两个 l,可以枚举第三个 x 的位置,只要保证后面的 l 数量不超过 \(1\) 即可,预处理前缀和后可以 \(O(1)\) 判断。注意要特判 l 总数不超过 \(2\) 的情况。

  • M

注意到一次询问 \(\{a, b\}\) 的时候,结果只会是 \(1\)\(2\),对应了它们在排列中位置的两种先后情况。

实际上,这个操作等价于比较两个元素的大小(指它们在排列中的位置),因此只需要在 \(O(n\log n)\) 次比较内将序列排序即可。如果直接用 sort,由于它在数据比较小时采用了一些 \(O(n^2)\) 次比较的排序方式,在 \(n=100\) 时可能比较次数会超过 \(650\)

一个解决方法是使用 stable_sort 等较稳定的排序方法,或者用我们队研究的一个奇怪方式:

每次要将一个给定集合排序时,先随机一个元素作为中间位置,然后只需要区分出每个元素和中间位置元素的大小,就可以分别递归。虽然随机笛卡尔树的深度是 \(O(\log n)\),但是由于树的形态并非充分平衡,常数比较大,如果仍然逐个比较会超出限制。但是在比较的时候,每次可以拿出两个元素,分别放到中间元素的前、后再询问。如果返回值是 \(1\)\(3\),可以直接确定这两个元素所属集合,否则这两个元素要么都在中间元素前面,要么都在后面。只需要再询问其中一个即可。可以发现,这样确定一个元素的位置的期望代价只有 \(\frac{3}{4}\),即可通过本题。

  • B

好像有点丢人啊,我们队没人会画 large 的图(

small 比较简单,只需要用 excel 打开,调整列宽后缩放到最小,就可以看到字母了。《大局观》就是指作为一个整体去看(

large 的意图可以说比较明确,给出的数字都是秒数,只需要用某些方式将秒数转化为钟表的分针和时针,即可看出大致的字母。官方是用 Python 画出的图,实际上 cpp 也可以读写 PPM 图像,只不过文件体积会比较……

  • F

small 其实不算难,只需要凭直觉猜出第一行是 #include <iostream> 即可。然后就可以通过换行符、空格、制表符和一些已知的字符替换,猜出 using namespace std;int main() { 等信息。唯一比较难猜的是运算中的 ^,但是可以直接试样例得到。

large 开头放了一个迷惑性的 /* this is a comment */,但是如果能看出成对出现的反向字符,猜出注释也不难。后面的部分其实大同小异,只有两个问题:weight[] 前的运算符,和数表的每一位对应的值。

由于绝大多数运算符都是单调的,但是答案在没有取模的情况下有大有小,运算符只能是 ^。最后要确定数表的值,发现 \(0, 1\) 都已经确定好了,只需要爆搜 \(8!\) 个情况。判断时不需要跑完 \(4000\) 组样例,只要跑前两组就可以唯一确定答案了。

  • E

非常有趣的字符串题,虽然我全靠队友带(

对于这种本质不同的字符串计数问题,通常都是将某个串在它第一次或最后一次出现时统计。本题中,如果将每个字符串在第一次出现的前缀中(即尽可能缩短用到的前缀长度),那么看起来就不太能做。但是如果将每一个串在最后一次出现的前缀中统计,就会非常简单:对于一个串,假设它可以表示为 \(s[1\ldots i]+s[j\ldots k]\)\(s[1\ldots i+a]+s[j+a\ldots k]\),那么有 \(s_{i+1}=s_j\)。并且,如果它不能表示为 \(s[1\ldots i+1]+s[j+1\ldots k]\) 的形式,必然也不能表示成 \(s[1\ldots i+a]+s[j+a\ldots k], a>0\),而且有 \(s_{i+1}\ne s_j\)。这也就是说,对于一个串 \(t=s[1\ldots i]+s[j\ldots k]\),它是最后一次出现在某个前缀中,当且仅当 \(s_{i+1}\ne s_j\)。所以我们要做的事情实际上就是对于每个后缀 \(s[i+1\ldots n]\),求出它包含的本质不同子串个数,减掉以 \(s_{i+1}\) 开头的不同子串个数即可。这个操作可以用 SAM 简单维护。

  • G

本场比赛中我最喜欢的一道题。

首先将题意抽象一下:对于 \(n+1\) 是质数的 \(n\),需要构造 \((1, p_1), (2, p_2), \ldots, (n, p_n)\),满足 \(p\) 是一个长度为 \(n\) 的排列,并且任意的 \(i<j\),二元组 \((j-i, p_j-p_i)\) 都是独一无二的。

貌似有很多神仙看到 \(n+1\) 是质数和 \(p\) 是排列两个条件,就想到了原根的幂……但是我没这么强,所以只能用一些比较暴力的办法:

对比较小的 \(n\),枚举排列求出字典序最小的解。如果你运气比较好或者观察力比较敏锐,就可以发现当 \(n=10\) 时,字典序最小的解恰好是 \(\left(i, 2^{i-1}\bmod (n+1)\right)\)。但是如果这么写的话,在 \(n=6\) 的时候就会出现重复的数字。这个时候,原根的幂就呼之欲出了;\(n=10\) 的情况只不过是因为 \(2\) 恰好是 \(11\) 的原根而已,但它的字典序在所有解中是最小的,这一点为解题提供了莫大的助力。对于 \(n=m-1\),只需要求出 \(m\) 的原根 \(g\),输出 \(\left(i, g^{i-1}\bmod m\right)\) 即可。

至于这个做法的正确性证明也非常简单。设 \(i< j, a\in[1, n-2]\) 时(注意 \(a\) 更大的时候,\(j+a\) 会超过 \(n\)),有 \(g^{i-1+a}-g^{i-1}\equiv g^{j-1+a}-g^{j-1}\pmod m\),也就是说 \(g^{i-1}(g^{a}-1)\equiv g^{j-1}(g^{a}-1)\pmod m\),移项得到 \((g^{i-1}-g^{j-1})(g^a-1)\equiv 0\pmod m\)。由于 \(a\in[1, n-2]\)\(g^a\not \equiv 1\);由于 \(1\leq i<j\leq n\)\(g^{i-1}\not \equiv g^{j-1}\)。因此必然矛盾。

  • I

据某位神仙说是矩阵乘法练习题,然而我们做了 3h(

可以发现 small 中,\(f_0(x)\)\(f_1(x)\) 都可以表示成 \(a\cdot x+b\) 的形式,并且显然它是具有结合律的。因此,每个函数都等价于它所属的 \(f_l, f_r\) 对应的一次函数复合后的结果。写一个矩阵乘法,或者直接维护一次函数都可以。

large 中的 random 函数使用了 xor_shift 来生成随机数,并且 \(f_0\) 中对 \(a\) 的操作只有 \(\operatorname{xor}\),因此不难想到按位处理贡献。设 \(g_{i, j}\) 表示 \(x\) 在 xor_shift 迭代一次后,第 \(i\) 位的 \(1\) 对结束后的 \(x\)\(j\) 位的影响(即是否会给第 \(j\)\(\operatorname{xor}\) 上原本的第 \(i\) 位),这个矩阵可以由 xor_shift 的流程直接写出来,并且任意次 xor_shift 后的结果都可以由这个矩阵的幂来表示。记录答案时需要另一个矩阵 \(h_{i, j}\),表示在经过这个函数后,初始的 \(x\) 的第 \(i\) 位的 \(1\)\(a\) 的第 \(j\) 位的影响。对于每个函数要维护它的 \(g^k\)\(h\),复合的时候有 \(g=g_l\cdot g_r, h=h_l+g_l\cdot h_r\)。由于矩阵都是 \(0/1\),实际上可以用位运算优化,但是似乎也没什么用(

PS:很多人说 small 过于有提示性,但是我们队似乎反而被 small 迷惑了,因为我们觉得不可能两个 task 都考矩阵乘法,于是一直在找 random 函数的周期(

  • K

本场比赛中我第二喜欢的一道题。不得不说 jls 玩梗水平和出题水平都是一流,把垃圾梗变成了题目中不可或缺的一部分(

考虑从后往前扫描,每次遇到一个字符,就选择一个串,并将这个字符接到它前面。不难发现,这种条件下,每个串具体是哪些字符并不重要,重要的是这个串已有的字符个数。因此,每个时刻的状态都可以用一个长度为 \(7\) 的序列来描述每个长度的字符串有多少个。这个时候可以发现,遇到 5 的时候是完全不需要决策的,它能,也只能将长度为 \(2\) 的一个串变为 \(3\)(注意我们是倒着扫描);但是 14 的时候,就会产生决策,从而有可能导向无解的结果。

这个时候再来看 small,由于所有的字符串,它倒着扫描时,开头的 4 都是最后的 \(m=\frac{|S|}{6}\) 位中的某一个,我们可以直接从 \(|S|-m\) 的位置开始扫描,并把所有字符串的初始状态设为 \(1\)。这个时候,再遇到 4 就不会产生决策了,只有 1 仍然有决策的可能。但是,在 1 的三种决策中,有两种决策(\(4\rightarrow 5, 5\rightarrow 6\))进行后,都不会使可选的局面变多。换句话说就是,如果 \(1\rightarrow 2\)1 不选,可能会导致后面的 5 无法匹配,但是另外两种决策的 1 不选,并不会产生无法匹配的情况。

large 中虽然去掉了这个条件,但是 small 中的贪心提醒我们,如果能创造一种未来可选的局面相对单调的状态,就可以用这种方式贪心出解。这个时候,有些慧眼如炬的神仙可能已经发现了,4 在任何情况下,它左边紧邻着的字符都是 1。这提醒我们,也许可以将 14 先匹配起来,让它们作为一个字符(不妨称为 A)整体参与匹配。这样,需要匹配的串就变成了 1A5A,看起来只要从左往右正着扫,就可以套用 small 里的贪心。

但是,首先需要证明将 14 先匹配起来是对的。考虑一种方案里,每个串里的两个 14 ,可以发现,对于某个 14\(S\) 中间隔的这一段,对它们所属的这个串是不能产生任何贡献的。换句话说,假如它是 514,那么这个 5 不可能来自 14 中间间隔的这一段;114 同理。这样的话,14 之间的间隔越长,对我们的匹配越不利。因此,不难证明,从右往左考虑每一个 4,向左找到最近的未被匹配的 1 并将它们匹配起来之后,对于任意一个位置,不存在方案,使得这个位置隔断的 14 数量严格更少。

于是,我们就可以将 14 看做一个整体(挂在 14 原来的位置都是可以的,因为在这个串里,它们的中间不可能再选中其他字符)进行匹配。唯一要注意的一点就是,在将 1 变成 114 的时候,应该取当前位置最靠左的 1,以避免它出现在 14 的空档中;其他的位置同理,都需要取最左边的。

(很可惜,我们在场上对这道题贪心的各种性质认识还不够完整,导致出现了一些调试不出来的 bug,最后没有通过 large)

  • D

在讲题的时候,我很难相信这题居然这么简单。但是由于时间不足、题面较为复杂和榜上较少的通过人数,我们在场上认为这道题超出了我们的能力范围而没有仔细研究它,非常可惜。

首先有一个重要的观察:每方同时最多只有一个鱼没有圣盾。而对于进攻方来说,我们只关心只有负责攻击的鱼有没有圣盾;因为其他位置的鱼,不管当前有没有圣盾,在这一轮后都会拥有圣盾。同样地,对于防守方,由于所有鱼在此时是等概率受攻击的,并且除了受到攻击的鱼,其他所有鱼在下一轮都会拥有圣盾,我们只关心是否存在没有圣盾的鱼。因此,可以列出状态 \(f_{n, m, 0/1, 0/1}\),表示当前进攻、防守方各有多少鱼,\(01\) 表示双方的状态。

转移时,只需要枚举一下这次进攻时,选择的防守方的鱼是否拥有圣盾,如果是有圣盾的,是否恰好是下一次防守方用来进攻的鱼,每种情况对应的概率都很容易计算。理一下转移关系可以发现,这些状态构成了一个包含自环的 “DAG”,解一下每个自环处的方程即可。

  • L

注意到对于 \(01\) 串来说,汉明距离等于欧氏距离的平方,因此只需要按照欧氏距离考虑即可。

因此,这里的问题变成了,求两个 \(n\) 维空间内的点的欧氏距离的近似值,但是其中一个点的坐标是 \(n\) 过大而无法直接表示的常量。

根据 Johnson-Lindenstrauss Theorem,我们可以在多项式时间内将若干个 \(n\) 维空间内的点映射为若干个 \(k\) 维空间内的点,同时保持它们两两之间的欧氏距离满足 \(d_{\mathrm{new}}\in \left[(1-\epsilon) d_{\mathrm{old}}, (1+\epsilon) d_{\mathrm{old}}\right]\)。具体做法和证明可以自行查阅,简单叙述就是将这若干个 \(n\) 维向量同时右乘某个 \(n\times k\) 的随机矩阵并乘上一个和 \(n, k\) 有关的常数。随机矩阵可以用随机数生成器来 \(O(1)\) 表述,而我们只需要选择合适的 \(k\),来保证不超过码长限制的同时尽可能精确地表达 \(\pi\) 即可。

  • C

手玩!冲就完了!(

实际上,由于玩家的经验会逐渐丰富,在经过若干次摸索后,很容易就能在 \(10^5\) 步内通过 \(100\) 关,需要的只是耐心,但是显然正解不能这么做(

要让 AI 来完成这个任务,首先要会让可执行文件进行 IO 交互,这点可以通过搜索引擎完成。

然后是 AI 的设计。一种设计是降低重复获得的钥匙的价值来进行估价和贪心,但是通过 \(100\) 关大约需要 \(1.5\times 10^5\) 步。

事实上,? 需要尽量优先拿到,而确定的 rgb 的价值显然更高(因为它们可以作为缺乏某种钥匙时的备用资源)。并且,由于存在形如 RGB?????# 的块和出口前 RRRGGGBBB 的门,可以发现我们大部分时间都在最大化拥有数量最少的钥匙。因此,对于每个状态和决策的估价,可以基于数量最少的钥匙的数量;如果当前状态估价过低,就推倒重来。这种贪心大幅度优化了步数,大约只需要 \(5\times 10^4\) 步即可。当然,期望步数最优的做法是状压每个状态,但是时间复杂度比较高,代码也比较难写。

  • J

本场比赛中我第三喜欢的一道题。但是很可惜我场上完全不会做(

首先需要特判 \(x_l=x_r\)\(y_l=y_r\) 的情况:由于在任意一层分形中,都不包含两个相邻的 x,且任意一层 o 的最外面一圈 \(0\) 级分形都是 o,所以可以用数学归纳法证明,任意的 \(0\)x 都不与其他 \(0\)x 相邻。因此,对于这种情况,只需要求出一行/列中 x 的个数和首、尾位置是否是 x,就可以求出 o 连通块的数量。这个问题是经典的分形,可以直接递归解决。

注意到,任意一个 \(0\)o,如果它所在的连通块大小不小于 \(2\),那么它要么连接在一个 \(1\)o 上,要么本身就是一个 \(1\)o 的一部分。既然这样,统计大小不小于 \(2\)\(0\)o 连通块个数就等价于统计 \(1\)o 连通块个数(注意如果一个 \(1\) 级分形只被圈到了一部分,也要完全统计进去),可以递归处理。

剩下的只有每层零散的 \(0\)o。可以发现,这样的 o 只会在我们切开了某个 \(1\)xo(即 x|o)的时候出现,实际上就是对于四条边的每一条,如果是完整切开的,就计算这一条边上的 \(1\)x 数量(因为每一个 \(1\)x 周围都全是 \(1\)o,所以每个 x 在这个方向都恰有一个 \(0\)o 单独成块),发现就是上面 \(x_l=x_r\)\(y_l=y_r\) 的问题。

这样,就在单次询问 \(O(n^2)\) 的复杂度内解决了问题(外层递归的每一层,都会有 \(O(1)\)\(O(n)\) 的内层递归)。

  • H

麻将题,咕了(

posted @ 2020-05-18 21:04  suwakow  阅读(758)  评论(2编辑  收藏  举报
Live2D