2021 年 CSP-S 初赛试题详解
peppaking8 · · 个人记录
1. 试题总结
总体来讲,此次 CSP-S 初赛的难度相比去年有所增加。主要的难点在于,试题中涉及了很多新的知识、算法,例如 球体积交、BASE64、四毛子 RMQ 算法。从这个角度来说,对于曾经没有接触过这些东西的人,在考场上现场理解是极为困难的。
但正因如此,今年的试题才更值得我们去学习总结。
原题链接,在附件中下载
2. 单项选择题解答
-
Linux 系统下的命令为 ls,而 Windows 系统下则为 dir。
-
二进制加法,从第二位开始一直进位。
-
此题需要记忆。不单单是数组,函数调用也会占用空间。例如 dfs 程序,如果现在正在调用
10^7 次函数,存储它的栈就会有这么多的元素,有可能就会爆掉空间。 -
排序方法的详细信息如下表:
平均情况不用记忆。需要重点关注每个排序的最好/坏情况和稳定性。
还需要记住的是,堆排序是选择排序的优化版本。在具体程序实现中,归并排序或基数排序的效果通常最佳。
-
此题有
3n-2 次的方法:将2n 个数两两配对先比较n 次,得到比较大的n 个数和比较小的n 个数。再用2(n-1) 次各自比较即可。但为什么它是最佳的,需要用数学方法进行论证。个人没有很好的方法。 -
注意到题目对哈希存储的规则就很容易。
6,7 在存储时不存在原先的位置上。 -
去年考过类似的题。注意
G 是不连通的,对于10 个点,可以把其中九个点两两连边,从\dfrac{9\times 8}{2}=36 条边。容易证明是最佳的。 -
发现:高度为
x 的二叉树节点数最多为2^x-1 。 -
二叉树的前序遍历为:先祖先,再左子树,再右子树;中序遍历为:先左子树,再祖先,再右子树。两者相等说明左子树为空。故
D 是正确的。 -
每次交换最多减少一个逆序对。注意到原串有
7 个逆序对,故最少操作7 次。这里可以采用这样的方式构造:找到最小的,一直交换直到变到第一位;然后找后面最小的,再一直换到第二位;等等。构造虽简单,但保证了每次操作恰好减少一个逆序对。 -
发现:
Solve(x,n)\equiv 5^{x-1}\pmod n 。
费马小定理:
p 为质数,x 不是p 的倍数,则x^{p-1}\equiv 1\pmod p 。
所以 Solve(23,23) 即
- 如果按照
F(1),F(2) 计算一次来算,那么F(n) 的计算次数应大于等于F(n) 。Fibonacci 数列通项公式:
F(n) 是 Fibonacci 数列第n 项,则F(n)=\dfrac{1}{\sqrt5}\left[\left(\dfrac{1+\sqrt5}{2}\right)^n-\left(\dfrac{1-\sqrt5}{2}\right)^n\right]
所以说,
- 可以按照选的苹果个数枚举。
- 选一个:
8 种。 - 选两个:不考虑限制有
28 种,若相邻则有7 种,故有21 种。 - 选三个:不考虑限制有
56 种,若有两个相邻,一个不与它们相邻有5+4\times 5+5=30 种,三个相邻有6 种。共56-30-6=20 种。 - 选四个:
5 种。这里限制已经很多了,不用“整体减空白”去做了。
-
可以按照等腰三角形的腰长枚举,然后讨论底边。注意到如果腰长不等于底边长,对答案的贡献是
3 ;若相等(即等边三角形),则贡献为1 。 -
可以用 Dijkstra 的方法模拟。但注意到图是分层图,就可以一层一层地算出每个点的最短路了。最短路径:
A\to C\to E\to H\to J ,长度为19 。
3. 阅读程序解答
3.1 第一题
-
发现
sq()函数是关于int类型变量,返回值为int的函数。故t 的结果也应该是整数。将int改为double,它的值不变。故正确。 -
注意到:
sq(d1)-sq(d2)+t是整数 (int) 类型。程序中先将其除掉了\sqrt{t} ,这时自动变为了浮点类型的数,再除以2 也不是“整除”了。但如果先除2 ,因为是整数除整数,所以程序会自动认为除法的意思是整除,进而可能导致答案错误。故错误。 -
同样的知识点,
x,y 可能是小数,sq(X)需要X 是整数,故sq(x)相当于先把x 取整后再运算,进而可能导致答案错误。故错误。
总结一下这三道题的共同点:都是对变量类型和运算与其的关系进行考察,这需要我们对整数和浮点数有非常清晰的认识。
-
拓展:
unsigned和signed是“有无符号”的区别,这也可能会进行考察。 -
注意:
acos(x)函数是反三角函数\arccos ,有性质\cos(\arccos(x))=x 。所以程序中r=\arccos 0.5 ,即60\degree ,化成弧度制是\dfrac{\pi}{3} 。更多关于三角函数的知识,可以参考这里。
那么,19、20 题就可以直接计算了。
对于第 21 题,我们需要更深入地分析这个程序。
- 拓展:球体积交公式的推导
若两球相包含或相离,则可很简单地计算出答案。若不然,则可以转化为两个球冠的体积之和。球冠的体积公式如下:
那么只需要算出两个球冠的
左边的球冠,有
最后一步利用了在
3.2 第二题
我们先看 23、25、26 题。
- 若
n\ge 1 ,则调用的solve函数中永远满足:h\le m 。故 28,38 行调用次数一定为0 ;若n\le 0 ,调用一次就返回了,次数一定为1 。故不可能分别调用两次以上。
25 . 对于开始的 Solve1(1,n),如果把 Node 的加法是
26 . 和上面分析类似。但此次有所不同:对于 Solve2(l,r) 来讲,额外的运算时间是
对于剩下的题目,则需要更细致地理解程序。注意到 22 题给我们提示:Solve1 和 Solve2 可能是等价的!我们先看 Solve2。它在干什么呢?wht 求的是 wmt 求的是 wht+wmt 算进 max 中,自然想到:它求的是序列的最大子段和。
Solve1 求最大子段和的方式是线段树的常用维护方式。这里是拓展例题。具体做法是这样的:对于区间
22 题,两个函数求解问题相同,自然等价。正确。
24 题,最大子段和为
27 题,最大子段和为从
3.3 第三题
实际上没什么可以说的。这个代码就是BASE64 加密方法。
- 注意:需要注意的是
\text{0x} 表示十六进制,故\text{0xff} 表示255 。然而char类型是否有符号是根据系统不同而不同的,所以说32 题疑为错题。
-
乍一看是正确的,但细致一想会出现问题:如果解密时,输入的加密串是不合法的(例如
1 a),自然就不会正常输出。还有一种方式,就是让解密后的字符串存在换行,这样就不满足“一行”的条件了! -
BASE64 的加密是可逆的。
BASE64 的加密过程是将三个字符
a,b,c 加密为四个字符x_1,x_2,x_3,x_4 。其中,加密后的字符仅有64 种,用六位二进制数表示。$x_2$ 的前两位是 $a$ 的后两位,后四位是 $b$ 的前四位; $x_3$ 的前四位是 $b$ 的后四位,后两位是 $c$ 的前两位; $x_4$ 的六位是 $c$ 的后六位。
31 . 注意到 for 循环中的操作全部为
30、33 题需要暴力计算验证。
- 注意:这个题提醒我们,要对常用字符的 ASCII 码烂熟于心。这里给出几个需要记忆的:
0的 ascii 码是48 ,1至9依次类推;
A的 ascii 码是65 ,B至Z依次类推;
a的 ascii 码是97 ,b至z依次类推。
4. 完善程序解答
- 注意:我认为,做完善程序题,关键不是“读懂”,而是“会做”。如果只去分析这个程序到底在写什么,对于较难的题是不适用的。但如果你自己想到了这道题的解法,按照自己的解法去分析程序,填空就会非常快了。
4.1 第一题(魔法数字)
首先,考虑这道题给到我们手里,该怎么做。
这种题不能用很好的数学方法去分析,所以只能类搜索的方式解决。这里用动态规划解决这个问题。设
这里注意:
回到原题。第一空填的是 dp 的初状态,显然是
再次发现,原题采用的是类似 Dijkstra 的方式,每次选择一个 !Vis[n],因为如果 Vis[n] 是 true 那么它就不可能再被更新了。第三空也显然,把还未确定的选一个最小的确定下来。对于第四空来说,我们看一个“极端情况”:
- 注意:本题的第四问 B,C 经验证均为正确。C 的正确性易得,因为已经标
Vis=true的数是已经确定f 的数,可用于转移;而 B 的正确性较难说明,需要计算机辅助验证。
4.2 第二题(RMQ 区间最值问题)
题干中给出的四毛子算法(The Method of Four Russians)的解释较为详细。这里我给一些补充说明。
- 拓展:笛卡尔树与 RMQ 问题的转化
这里的笛卡尔树能做到什么呢?首先,它是二叉树,树上的每个结点一一对应原序列上的每个数。并且,对于任意两个结点,它们的最近公共祖先(LCA)即为对应序列区间的最大值。
自然想到,最大的那个数将作为笛卡尔树的根。它的两个儿子呢?如果按根的位置将序列分成两部分的话,那么它们的最大值就分别对应根的左右儿子,依此类推。
这里是 OI-wiki 上的一个例子。(它把 max 改为了 min)
正确性不难得出。
-
如果两结点分别在根的左、右子树中,则它们所表示的序列区间一定包含最大的那个数,这个数就是树的根。
-
如果两结点有祖先关系,那么两点 LCA 为其中一点,恰好是整个区间的最大值。
-
如果在同一子树中,再去递归证明。
-
拓展:Euler 序
这里的 Euler 序,为从根节点开始,进行如下操作:
将当前节点加入 Euler 序中;
递归所有子树;
将当前节点再次加入 Euler 序中。
利用 Euler 序,我们将 LCA 问题转化为了 +1-1RMQ 问题。如果我们按照序列中每个点的深度作为标准进行比较,那么两点
回到原题。
-
Build函数构建笛卡尔树。具体方法是:每次加入一个结点p ,然后修改当前的笛卡尔树。第一空,如果当前根的值小于p 的值,就把p 当作根。因为是从1 至n 编号加入,所以当前根一定在p 左侧,即p 的左儿子设为当前根。若不然,则可直接把p 接入根的右儿子。综上分析,选 A、D。 -
Dfs函数构建了树的 Euler 序,存储在数组A中。 -
ST_init构建块间的 ST 表。这里min函数需要比较深度,深度较小者成为最小值,故选 A。 -
Small_init对于小块构建差分数组然后预处理。程序把第i 个块的差分数组压缩为二进制数Dif[i],二进制位为1 表示 -1,0 表示 +1。显然这里是要比较深度差的,故选 D。之后对于每一种差分数组进行预处理,这里mx求的是最小值! 根据差分数组的压缩方式,如果这一位是1 则当前的v 减少 1,否则增加 1。故选 D。 -
small_query(l,r)求解在l,r 处于一个小块中的答案。p 表示它们所处块的编号,S 是需要我们找到的合适的差分数组。这里注意:不能直接运用Dif[p]! 需要单独提出[l,r] 间的部分放到前面,然后其余位均为0 ,达到最小值一定在区间里取到的效果。Dif[p]左移l-p\times b ,相当于把[l,r] 间的部分变为Dif的最后r-l 位;之后与上2^{r-l}-1 ,相当于只保留最后r-l 位,其余均为0 。正好是我们想要的效果。故选 C。
剩下的代码对解题无帮助,这里跳过。
5. 总结
写完之后的感觉是,确实难。
在这里给大家一些小建议:
- 对于整变量、浮点变量很多的代码,一定要把每个变量、函数以整型还是浮点型表示整清楚。
- 遇到阅读程序中比较长的,它一定有它本身的含义,或是在解决某个特定的问题,这时候需要我们先研究程序本身。
- 完善程序题,更好的方法是先研究这道题本身怎么做。