试卷解析 · 理解每一步
2021 CCF CSP-S 第一轮(提高级 C++)
CSPS-2021-R1
试卷解析总览,可直接查看每题答案与解析。
第 1 题(单选题,2 分)
在 Linux 系统终端中,用于列出当前目录下所含的文件和子目录的命令为()。
正确答案A
解析详情
先明确任务:查看当前目录中有哪些文件,而不是改变所在位置。ls 不带路径时列出当前目录内容,因此选 A。
cd 用于切换目录,cp 用于复制文件;all 不是本题所需的列目录命令。若要显示点号开头的隐藏文件,通常另加 -a 选项,但不影响本题判断。
第 2 题(单选题,2 分)
二进制数 001010102 和 000101102 的和为()。
正确答案B
解析详情
先按位权转换:,。相加得到 ,写成八位二进制是 ,选 B。
也可从右向左竖式相加:最低位为 0,第二位的 产生进位,此后连续向高位传递。不能把二进制中的“满二进一”当作十进制加法。
第 3 题(单选题,2 分)
在程序运行过程中,如果递归调用的层数过多,可能会由于( )引发错误。
正确答案A
解析详情
普通递归调用需要保存当前函数的返回位置、局部变量等信息,形成一层调用栈帧。递归没有返回时,这些栈帧会同时保留;深度不断增加,所需栈空间也不断增加。
系统为线程提供的栈空间有限,深度过大可能造成栈溢出,所以选 A。此处讨论通常的递归实现;编译器是否能优化某些特殊递归,不改变“可能栈溢出”的判断。
第 4 题(单选题,2 分)
以下排序方法中,()是不稳定的。
正确答案C
解析详情
稳定性要求:两个关键字相等的元素,排序后仍保持原来的先后次序。可给相等元素附上标签,再观察标签顺序。
堆排序会把堆顶与末尾元素交换,交换可能跨过另一个相等元素。例如两个相等元素组成的堆,取出堆顶时的一次交换就可能颠倒二者。因此堆排序不稳定,选 C。通常教材实现中,插入排序、冒泡排序及合并时优先取左侧相等元素的归并排序均可保持稳定。
第 5 题(单选题,2 分)
以比较为基本运算,对于 2n 个数,同时找到最大值和最小值,最坏情况下需要的最小的比较次数为()。
正确答案C
解析详情
先把 个数分成 对,每对比较一次,共 次。每对较大者进入最大值候选组,较小者进入最小值候选组。
从 个较大者中找最大值需 次,从 个较小者中找最小值也需 次,合计 ,选 C。
为什么这种配对能达到最优?每个非最大元素都必须有一次“输”的证据,每个非最小元素都必须有一次“赢”的证据。比较对抗分析中,只有尚未比较过的两个元素首次配对,才能一次同时有效减少两类候选;最多有 次这样的双重收益,其余候选仍需逐个排除,得到同样的下界。
第 6 题(单选题,2 分)
现有一个地址区间为 的哈希表,对于出现冲突情况,会往后找第一个空的地址存储(到 10 冲突了就从 开始往后),现在要依次存储( ,1,2,3,4,5,6,7),哈希函数为 。请问 7 存储在哈希表哪个地址中()。
正确答案C
解析详情
依次算初始地址并处理已有冲突:0、1、2、3、4、5 的初始地址分别是 0、1、4、9、5、3,均为空,可以直接存放。
插入 6 时,。地址 3、4、5 已占用,继续探测到空地址 6,因此 6 存在地址 6。
插入 7 时,。地址 5、6 都已占用,第一个空位是 7,因此选 C。线性探测应检查已经存入的所有元素,而不只是上一个数。
第 7 题(单选题,2 分)
G 是一个非连通简单无向图(没有自环和重边),共有 36 条边,则该图至少有()个点。
正确答案C
解析详情
要在固定点数下容纳尽量多的边,应把尽量多的点集中到一个完全图中,同时留下至少一个独立点以保证非连通。因此 个点最多有 条边。
若只有 9 个点,至多是 加一个独立点,边数为 28,不够 36。若有 10 个点, 加一个独立点恰有 条边。下界可达到,所以至少需要 10 个点,选 C。
第 8 题(单选题,2 分)
令根结点的高度为 1,则一棵含有 2021 个结点的二叉树的高度至少为()。
正确答案B
解析详情
根结点处于第 1 层,第 层最多有 个结点。高度为 的二叉树最多容纳 个结点。
高度 10 时上限为 1023,小于 2021;高度 11 时上限为 2047,可以容纳 2021 个结点。因此最小高度是 11,选 B。这个问题求的是“至少”,应考虑树尽量宽,而不是退化为链的情况。
第 9 题(单选题,2 分)
前序遍历和中序遍历相同的二叉树为且仅为()。
正确答案D
解析详情
前序次序是“根、左、右”,中序次序是“左、根、右”。若某结点存在非空左子树,前序中该结点先于左子树,中序中却晚于左子树,两种次序就不同。
所以不仅根没有左子树,右子树里的每个结点也都不能有左子树。反过来,若整棵树是一条向右延伸的链,两种遍历都从根沿右儿子访问,次序相同。因此选 D。这里沿用遍历题通常假定的结点可区分条件。
第 10 题(单选题,2 分)
定义一种字符串操作为交换相邻两个字符。将 “DACFEB” 变为 “ABCDEF” 最少需要()次上述操作。
正确答案A
解析详情
按目标字母顺序将 DACFEB 写为 。逐个统计右侧更小的数:4 后面有 1、3、2,共 3 个;1 后面没有;3 后面有 2;6 后面有 5、2;5 后面有 2。
逆序对总数为 。一次相邻交换最多消去一个逆序对,所以至少要 7 次;不断交换相邻逆序元素又能恰好做到每次减少一个,故最少就是 7 次,选 A。
第 11 题(单选题,2 分)
有如下递归代码:
solve(t, n):
if t = 1 return 1
else return 5 * solve(t - 1, n) mod n则 solve(23, 23) 的结果为( )。
正确答案A
解析详情
递归边界 solve(1,n)=1,每增加一层就多乘一个 5 并取模,因此 solve(t,n) 等于 。代入本题得到 。
23 是质数,且 5 与 23 互质,费马小定理给出 ,所以选 A。指数是 22,因为边界层直接返回 1,并不再乘 5。
第 12 题(单选题,2 分)
斐波那契数列的定义为:,,()。现在用如下程序来计算斐波那契数列的第 项:
F(n):
if n <= 2 return 1
else return F(n - 1) + F(n - 2)其时间复杂度为( )。
正确答案C
解析详情
令调用工作量为 。当 时,要分别计算 和 ,所以 。相同子问题会在不同分支反复计算。
递归树高度为 ,每个结点至多分出两个孩子,可以用 作为上界,给定选项中选 C。更紧的增长量是 ,其中 ;这不影响 这个正确上界。若改成记忆化或顺序递推,才可降到线性时间。
第 13 题(单选题,2 分)
有 8 个苹果从左到右排成一排,你要从中挑选至少一个苹果,并且不能同时挑选相邻的两个苹果,一共有( )种方案。
正确答案C
解析详情
先允许一个也不选,设前 个苹果的合法方案数为 。若不选第 个,有 种;若选第 个,第 个必须不选,剩下有 种。
因此 ,从 依次得到 至 为 3、5、8、13、21、34、55。
最后排除“一个也不选”的唯一方案,得到 ,选 C。
第 14 题(单选题,2 分)
设一个三位数 ,a,b,c 均为 1~9 之间的整数,若以 a、b、c 作为三角形的三条边可以构成等腰三角形(包括等边),则这样的 n 有( )个。
正确答案C
解析详情
分成等边和恰有两边相等两类。等边对应 111、222、……、999,共 9 个三位数。
恰有两边相等时,设边长为 ,其中 且 $y
e xy<2xx=1,2,\ldots,9y$ 个数依次为 0、2、4、6、8、8、8、8、8,总计 52。
每组不同的 ,不相等的数字 可放在百、十、个位三个位置,因此共 ,选 C。把等边先单列,能避免把它重复乘 3。
左右滑动表格,可查看完整内容。
| 相等边 x | 可取的不等边 y | 数量 |
|---|---|---|
| 1 | 无 | 0 |
| 2 | 1、3 | 2 |
| 3 | 1、2、4、5 | 4 |
| 4 | 1、2、3、5、6、7 | 6 |
| 5 | 1 至 9,除 5 | 8 |
| 6 | 1 至 9,除 6 | 8 |
| 7 | 1 至 9,除 7 | 8 |
| 8 | 1 至 9,除 8 | 8 |
| 9 | 1 至 9,除 9 | 8 |
这里只数恰有两边相等的 x,x,y;每组还有三个位置排列,等边 9 种另外计算。
第 15 题(单选题,2 分)
有如下的有向图,节点为 A,B,...,J,其中每条边的长度都标在图中。则节点 A 到节点 J 的最短路径长度为( )。

正确答案B
解析详情
按图的层次从左向右计算,记 为 A 到 X 的最短距离。第一层有 。
第二层:;;。
第三层:;。最后 ,选 B。对应路径为 A→C→E→H→J。这样既给出一条路径,也证明其他进入各层的路径不会更短。
左右滑动表格,可查看完整内容。
| 顶点 | 最短距离 | 选择的前驱 |
|---|---|---|
| A | 0 | 起点 |
| B | 2 | A |
| C | 5 | A |
| D | 1 | A |
| E | 11 | C |
| F | 13 | D |
| G | 9 | C |
| H | 14 | E |
| I | 18 | F |
| J | 19 | H |
每个点取所有前驱路径长度的最小值;所列前驱给出一条最短路径。
二、阅读程序(1)
1 | #include <iostream>
2 | #include <cmath>
3 | using namespace std;
4 |
5 | const double r = acos(0.5);
6 |
7 | int a1, b1, c1, d1;
8 | int a2, b2, c2, d2;
9 |
10 | inline int sq(const int x) { return x * x; }
11 | inline int cu(const int x) { return x * x * x; }
12 |
13 | int main()
14 | {
15 | cout.flags(ios::fixed);
16 | cout.precision(4);
17 |
18 | cin >> a1 >> b1 >> c1 >> d1;
19 | cin >> a2 >> b2 >> c2 >> d2;
20 |
21 | int t = sq(a1 - a2) + sq(b1 - b2) + sq(c1 - c2);
22 |
23 | if (t <= sq(d2 - d1)) cout << cu(min(d1, d2)) * r * 4;
24 | else if (t >= sq(d2 + d1)) cout << 0;
25 | else {
26 | double x = d1 - (sq(d1) - sq(d2) + t) / sqrt(t) / 2;
27 | double y = d2 - (sq(d2) - sq(d1) + t) / sqrt(t) / 2;
28 | cout << (x * x * (3 * d1 - x) + y * y * (3 * d2 - y)) * r;
29 | }
30 | cout << endl;
31 | return 0;
32 | }假设输入的所有数的绝对值都不超过 1000,完成下面的判断题和单选题:
第 16 题(判断题,1.5 分)
将第 21 行中 t 的类型声明从 int 改为 double,不会影响程序运行的结果。()
正确答案正确
解析详情
先检查取值范围。每个坐标绝对值不超过 1000,坐标差绝对值不超过 2000,三个平方和至多 ,在通常的 32 位 int 范围内,也可由 double 精确表示。
改类型后,存入 t 的整数值没有变化;后面比较用到的整数及平方根运算也具有相同数值。因此按题目通常的数值环境,输出不变,判断正确。不能仅凭“double 更精确”下结论,关键是本题原来的整数计算没有溢出。
第 17 题(判断题,1.5 分)
将第 26、27 行中的 “/ sqrt(t) / 2” 替换为 “/ 2 / sqrt(t)”,不会影响程序运行的结果。()
正确答案错误
解析详情
原式分子为整数,但先除以 sqrt(t) 后执行浮点除法;改成先 /2 时,分子与 2 都是整数,会先截去小数部分。
用题中两单位球、球心距为 1 的输入举例:分子为 1,原式 ,得到 ;改后整数 ,得到 。最终输出会从 1.3090 变成 4.1888,所以判断错误。
第 18 题(判断题,1.5 分)
将第28行中的“x*x”改成“sq(x)”、“y*y”改成“sq(y)”,不会影响程序运行的结果。()
正确答案错误
解析详情
函数声明 sq(const int x) 决定了实参先转成整数,然后才平方。inline 只是实现层面的提示,不会改变参数类型。
仍取半径都为 1、球心距为 1 的情况,原程序中 ,直接平方为 0.25;调用 sq(x) 时却先把 0.5 转为 0,平方为 0。相交体积将被算成 0,输出改变,因此判断错误。
第 19 题(判断题,2 分)
当输入为“0 0 0 1 1 0 0 1”时,输出为“1.3090”。()
正确答案正确
解析详情
输入表示两个半径为 1 的球,球心分别为 和 。距离平方 ,大于半径差平方 0、小于半径和平方 4,进入部分相交分支。
,。代入输出式得到 。保留四位小数为 1.3090,判断正确。
第 20 题(单选题,3 分)
当输入为 “1 1 1 1 1 1 1 2” 时,输出为()。
正确答案D
解析详情
两个球的球心都是 ,所以 ;半径分别为 1 和 2,半径差平方为 1。满足 ,进入第一个分支。
程序取较小半径 1,输出 。固定四位小数得到 4.1888,选 D。这里直接使用包含分支,不会执行除以球心距的公式。
第 21 题(单选题,2.5 分)
这段代码的含义为()。
正确答案C
解析详情
把前三个输入量看作球心坐标,第四个量看作半径,则 t 是球心距平方。若距离不大于半径差,交集是较小的整个球,程序输出其体积;若距离不小于半径和,交集体积为 0。
剩下情况是部分相交。x、y 是公共截面两侧球冠的高度,球冠体积公式为 。程序把两个球冠体积相加,恰好得到两球公共部分的体积,因此选 C。几何解释以半径非负为前提。
二、阅读程序(2)
1 | #include <algorithm>
2 | #include <iostream>
3 | using namespace std;
4 |
5 | int n, a[1005];
6 |
7 | struct Node
8 | {
9 | int h, j, m, w;
10 |
11 | Node(const int _h, const int _j, const int _m, const int _w):
12 | h(_h), j(_j), m(_m), w(_w)
13 | { }
14 |
15 | Node operator+(const Node &o) const
16 | {
17 | return Node(
18 | max(h, w + o.h),
19 | max(max(j, o.j), m + o.h),
20 | max(m + o.w, o.m),
21 | w + o.w);
22 | }
23 | };
24 |
25 | Node solve1(int h, int m)
26 | {
27 | if (h > m)
28 | return Node(-1, -1, -1, -1);
29 | if (h == m)
30 | return Node(max(a[h], 0), max(a[h], 0), max(a[h], 0), a[h]);
31 | int j = (h + m) >> 1;
32 | return solve1(h, j) + solve1(j + 1, m);
33 | }
34 |
35 | int solve2(int h, int m)
36 | {
37 | if (h > m)
38 | return -1;
39 | if (h == m)
40 | return max(a[h], 0);
41 | int j = (h + m) >> 1;
42 | int wh = 0, wm = 0;
43 | int wht = 0, wmt = 0;
44 | for (int i = j; i >= h; i--) {
45 | wht += a[i];
46 | wh = max(wh, wht);
47 | }
48 | for (int i = j + 1; i <= m; i++) {
49 | wmt += a[i];
50 | wm = max(wm, wmt);
51 | }
52 | return max(max(solve2(h, j), solve2(j + 1, m)), wh + wm);
53 | }
54 |
55 | int main()
56 | {
57 | cin >> n;
58 | for (int i = 1; i <= n; i++) cin >> a[i];
59 | cout << solve1(1, n).j << endl;
60 | cout << solve2(1, n) << endl;
61 | return 0;
62 | }第 22 题(判断题,1.5 分)
程序总是会正常执行并输出两行两个相等的数。()
正确答案正确
解析详情
先读懂 Node:h 为最大前缀和,j 为最大子段和,m 为最大后缀和,w 为区间总和,前三项允许空段,因此至少为 0。合并时最大子段只可能完全在左边、完全在右边,或由左边最大后缀与右边最大前缀拼成。
solve1 把这四项一起返回;solve2 用循环现算左后缀和右前缀,再与两侧递归答案比较。两者枚举的三类情况完全相同,因此在题目预期的有效数组范围和无整数溢出条件下,输出相等,原题判断为正确。全负数组也相等,均为 0。
第 23 题(判断题,1.5 分)
第 28 行与第 38 行分别有可能执行两次及以上。()
正确答案错误
解析详情
当初始 时,递归参数是非空闭区间。单个元素直接返回;长度至少为 2 时,中点 j 满足 ,所以 [h,j]、[j+1,m] 都非空。由归纳可知不会进入 h>m。
即使考虑初始 的空区间,每个函数也只会在第一次调用时返回一次,不可能各执行两次及以上。因此判断错误。
第 24 题(判断题,1.5 分)
当输入为 “5 -10 11 -9 5 -7” 时,输出的第二行为 “7”。()
正确答案错误
解析详情
首个数 5 是数组长度,实际序列为 。逐项维护“以当前位置结尾、允许为空的最大和”,递推为 。
依次得到 0、11、2、7、0,全局最大值为 11,对应只选第二项。7 只是处理到第四项时的局部结果,不能覆盖此前已出现的 11。两行均输出 11,题述第二行为 7,故判断错误。
第 25 题(单选题,3 分)
solve1(1, n) 的时间复杂度为()。
正确答案B
解析详情
solve1 的每个叶子处理一个元素,内部结点通过重载加法做固定次数的加法和比较,合并耗时为 。
一棵有 n 个叶子的二叉递归树共有 个结点,每个结点工作量为常数,所以总时间为 ,选 B。也可写递推式 ;不能把递归层数直接当作总时间。
第 26 题(单选题,3 分)
solve2(1, n) 的时间复杂度为()。
正确答案C
解析详情
solve2 在每个非叶区间中,从中点向两侧分别扫描。这两个循环合计访问区间内的所有元素,所以长度为 k 的一次调用,合并部分需 。
同一递归层的区间互不重叠,总长度为 n,因此每层总工作量为 。递归有 层,总时间为 ,选 C。与上一题的差别正是合并需要重新扫描。
第 27 题(单选题,3 分)
当输入为 “10 -3 2 10 0 -8 9 -4 -5 9 4” 时,输出的第一行为()。
正确答案B
解析详情
使用与分治答案等价的线性手推:以当前位置结尾的最大非负子段和,依次为 0、2、12、12、4、13、9、4、13、17。过程中出现的最大值为 17。
达到它的连续子段是第 2 至第 10 项:,因此第一行输出 17,选 B。中间虽有负数,但连接两端正数仍可能比舍弃整段更优。
左右滑动表格,可查看完整内容。
| 位置 | 元素 | 以此处结尾的最大和 | 全局最大和 |
|---|---|---|---|
| 1 | -3 | 0 | 0 |
| 2 | 2 | 2 | 2 |
| 3 | 10 | 12 | 12 |
| 4 | 0 | 12 | 12 |
| 5 | -8 | 4 | 12 |
| 6 | 9 | 13 | 13 |
| 7 | -4 | 9 | 13 |
| 8 | -5 | 4 | 13 |
| 9 | 9 | 13 | 13 |
| 10 | 4 | 17 | 17 |
允许空子段;当前值为 max(0, 上一当前值 + 元素)。
二、阅读程序(3)
1 | #include <iostream>
2 | #include <string>
3 | using namespace std;
4 |
5 | char base[64];
6 | char table[256];
7 |
8 | void init()
9 | {
10 | for (int i = 0; i < 26; i++) base[i] = 'A' + i;
11 | for (int i = 0; i < 26; i++) base[26 + i] = 'a' + i;
12 | for (int i = 0; i < 10; i++) base[52 + i] = '0' + i;
13 | base[62] = '+', base[63] = '/';
14 |
15 | for (int i = 0; i < 256; i++) table[i] = 0xff;
16 | for (int i = 0; i < 64; i++) table[base[i]] = i;
17 | table['='] = 0;
18 | }
19 |
20 | string encode(string str)
21 | {
22 | string ret;
23 | int i;
24 | for (i = 0; i + 3 <= str.size(); i += 3) {
25 | ret += base[str[i] >> 2];
26 | ret += base[(str[i] & 0x03) << 4 | str[i + 1] >> 4];
27 | ret += base[(str[i + 1] & 0x0f) << 2 | str[i + 2] >> 6];
28 | ret += base[str[i + 2] & 0x3f];
29 | }
30 | if (i < str.size()) {
31 | ret += base[str[i] >> 2];
32 | if (i + 1 == str.size()) {
33 | ret += base[(str[i] & 0x03) << 4];
34 | ret += "==";
35 | }
36 | else {
37 | ret += base[(str[i] & 0x03) << 4 | str[i + 1] >> 4];
38 | ret += base[(str[i + 1] & 0x0f) << 2];
39 | ret += "=";
40 | }
41 | }
42 | return ret;
43 | }
44 |
45 | string decode(string str)
46 | {
47 | string ret;
48 | int i;
49 | for (i = 0; i < str.size(); i += 4) {
50 | ret += table[str[i]] << 2 | table[str[i + 1]] >> 4;
51 | if (str[i + 2] != '=')
52 | ret += (table[str[i + 1]] & 0x0f) << 4 | table[str[i + 2]] >> 2;
53 | if (str[i + 3] != '=')
54 | ret += table[str[i + 2]] << 6 | table[str[i + 3]];
55 | }
56 | return ret;
57 | }
58 |
59 | int main()
60 | {
61 | init();
62 | cout << int(table[0]) << endl;
63 |
64 | int opt;
65 | string str;
66 | cin >> opt >> str;
67 | cout << (opt ? decode(str) : encode(str)) << endl;
68 | return 0;
69 | }假设输入总是合法的(一个整数和一个不含空白字符的字符串,用空格隔开),完成下面的判断题和单选题:
第 28 题(判断题,1.5 分)
程序总是先输出一行一个整数,再输出一行一个字符串。()
正确答案错误
解析详情
输入格式合法仅表示读到了整数和不含空白的字符串,并没有保证非零 opt 对应的字符串是有效 Base64。decode 每次无条件按 4 个字符处理。
例如输入 1 A,循环仍会访问 str[i+2]、str[i+3],超过该字符串允许访问的位置,产生未定义行为,不能保证正常输出两行。因此判断错误。正常解码需要另外校验字符集合、长度和填充位置。
第 29 题(判断题,1.5 分)
对于任意不含空白字符的字符串 str1,先执行程序输入 “0 str1”,得到输出的第二行记为 str2;再执行程序输入 “1 str2”,输出的第二行必为 str1。()
正确答案正确
解析详情
按本题预期的普通 ASCII 字符模型,编码把 3 个字节的 24 位拆成 4 个 6 位索引,解码再将这些位拼回原来的 3 个字节。最后不足 3 字节时,编码加 =,解码通过 = 判断只还原 1 或 2 个字节。因此对编码结果再解码可还原原串,原题答案为正确。
需区分命题意图与代码适用范围:题面“任意不含空白字符”若包含最高位为 1 的任意字节,在有符号 char 环境中,str[i] >> 2 可能为负并造成负下标。本实现没有先转为 unsigned char,不能据此证明对任意字节串都安全;这里保留原答案,并明确其字符范围前提。
第 30 题(判断题,1.5 分)
当输入为 “1 SGVsbG93b3JsZA==” 时,输出的第二行为 “HelloWorld”。()
正确答案错误
解析详情
先确定 opt 为 1,程序执行解码。每四个编码字符还原一组:SGVs→Hel,bG93→low,b3Js→orl,ZA==→d。
拼接得到 Helloworld,其中第六个字符是小写 w,而题述 HelloWorld 使用大写 W。字符大小写不同,字符串就不相等,所以判断错误。
第 31 题(单选题,3 分)
设输入字符串长度为 n,encode 函数的时间复杂度为()。
正确答案B
解析详情
主循环每次让 i 增加 3,每轮处理 3 个输入字符并追加 4 个输出字符,因此循环次数为 。末尾分支最多处理剩余两个字符。
在字符串追加按摊还常数时间计算的通常模型下,总耗时为 ,选 B;输出长度 也与输入长度同阶。位运算次数固定,并不会引入对数因子。
第 32 题(单选题,3 分)
输出的第一行为()。
正确答案D
解析详情
初始化先把 table 的所有位置设为 0xff,随后只覆盖 Base64 字符以及等号对应的位置。下标 0 对应空字符,不在这些字符中,所以仍保留全 1 的字节位型。
本题答案采用 8 位有符号 char 环境,全 1 位型表示 -1,转换成 int 后输出十进制 -1,选 D。C++ 的普通 char 是否有符号取决于实现,若使用无符号 char 则可能得到 255;因此这道题具有环境前提,不是跨平台固定输出。
第 33 题(单选题,4 分)
当输入为 “0 CSP2021csp” 时,输出的第二行为()。
正确答案D
解析详情
把 10 个字符按三个一组分为 CSP、202、1cs、p。前三组分别编码为 Q1NQ、MjAy、MWNz;最后仅剩一个字节 p,其编码为 cA==。
例如首组 ASCII 值为 67、83、80,拆出的四个索引是 16、53、13、16,对应 Q、1、N、Q。将四组拼接,得到 Q1NQMjAyMWNzcA==,选 D。末尾两个等号来自只剩一个字节,而不是编码的普通字符。
左右滑动表格,可查看完整内容。
| 组别 | 输入 | 输出 |
|---|---|---|
| 第 1 组 | CSP | Q1NQ |
| 第 2 组 | 202 | MjAy |
| 第 3 组 | 1cs | MWNz |
| 第 4 组 | p | cA== |
每 3 个字节变成 4 个编码字符;最后只有 1 个字节,补两个等号。
三、完善程序(1)
(1)(魔法数字)小 H 的魔法数字是 4。给定 n,他希望用若干个 4 进行若干次加法、减法和整除运算得到 n。但由于小 H 计算能力有限,计算过程中只能出现不超过 M = 10000 的正整数。求至少可能用到多少个 4。
例如,当 n=2 时,有 ,用到了 3 个 4,是最优方案。
试补全程序。
#include <iostream>
#include <cstdlib>
#include <climits>
using namespace std;
const int M = 10000;
bool Vis[M + 1];
int F[M + 1];
void update(int &x, int y) {
if (y < x)
x = y;
}
int main() {
int n;
cin >> n;
for (int i = 0; i <= M; i++)
F[i] = INT_MAX;
①;
int r = 0;
while (②) {
r++;
int x = 0;
for (int i = 1; i <= M; i++)
if (③)
x = i;
Vis[x] = 1;
for (int i = 1; i <= M; i++)
if (④) {
int t = F[i] + F[x];
if (i + x <= M)
update(F[i + x], t);
if (i != x)
update(F[abs(i - x)], t);
if (i % x == 0)
update(F[i / x], t);
if (x % i == 0)
update(F[x / i], t);
}
}
cout << F[n] << endl;
return 0;
}第 34 题(单选题,3 分)
①处应填( )
F[4] = 0F[1] = 4F[1] = 2F[4] = 1正确答案D
解析详情
先定义代价:F[x] 是表示正整数 x 所用的数字 4 的最少个数。无需任何运算就能写出一个 4,因此初始已知状态是 F[4]=1,选 D,其余状态暂为无穷大。
若设为 0,就把数字 4 本身的使用次数漏掉了。F[1]=2 虽对应表达式 ,却不应代替基础状态;算法应从最基本的一个 4 自动推导这些组合。
第 35 题(单选题,3 分)
②处应填()
!Vis[n]r < nF[M] == INT_MAXF[n] == INT_MAX正确答案A
解析详情
Vis[x] 表示 x 的最小代价已经确定。每轮从未确定状态中选择 F 最小的一个,再与已确定状态组合产生候选表达式。两个子表达式各至少用一个 4,所以组合的代价严格大于每个子表达式的代价,这支持按代价递增确定状态。
当目标 n 被确定后即可结束,故循环条件是 !Vis[n],选 A。仅仅发现 F[n] 为有限值,不表示该方案已经最优;轮数 r 也不等于目标值或最优代价。
第 36 题(单选题,3 分)
③处应填()
F[i] == r!Vis[i] && F[i] == rF[i] < F[x]!Vis[i] && F[i] < F[x]正确答案D
解析详情
每轮先令 x=0,而 F[0]=INT_MAX 是哨兵。扫描 1 至 M 时,只考虑尚未确定的 i,并在 F[i]<F[x] 时更新 x。
所以条件必须同时满足 !Vis[i] 和 F[i]<F[x],选 D。若没有未访问条件,会反复选中已经确定的便宜状态;若按 r 筛选,则把“第几次确定状态”和“用了几个 4”误当成同一件事。
第 37 题(单选题,3 分)
④处应填( )
F[i] < F[x]F[i] <= rVis[i]i <= x正确答案C
解析详情
刚确定 x 后,需要与每个已经确定的 i 组合。相加、作正差或整除,都能得到一个新表达式,所用 4 的个数为 F[i]+F[x],据此松弛相应状态。因此填 Vis[i],选 C。
每一对状态会在较晚确定的那个状态被处理时得到组合机会;x 刚被标记为已确定,也允许 i=x。例如 恰好需要把两个 4 组合,代价是 2。只有除数整除被除数时才尝试除法,且差为 0 的情况被排除,符合题目只允许正整数的限制。
三、完善程序(2)
(2)(RMQ 区间最值问题)给定序列 ,和 次询问,每次询问给定 ,求 。
为了解决该问题,有一个算法叫 the Method of Four Russians,其时间复杂度为 ,步骤如下:
- 建立 Cartesian(笛卡尔)树,将问题转化为树上的 LCA(最近公共祖先)问题。
- 对于 LCA 问题,可以考虑其 Euler 序(即按照 DFS 过程,经过所有点,环游回根的序列),即求 Euler 序列上两点间一个新的 RMQ 问题。
- 注意新的问题为 RMQ,即相邻两点的深度差一定为 1。
下面解决这个 RMQ 问题,“序列”指 Euler 序列:
设 为 Euler 序列长度。取。将序列每 个分为一大块,使用 ST 表(倍增表)处理大块间的 RMQ 问题,复杂度。
• (重点)对于一个块内的 RMQ 问题,也需要 的算法。由于差分数组 种,可以预处理出所有情况下的最值位置,预处理复杂度 ,不超过 。
- 最终,对于一个查询,可以转化为中间整的大块的 RMQ 问题,以及两端块内的 RMQ 问题。
试补全程序。
#include <iostream>
#include <cmath>
using namespace std;
const int MAXN = 100000, MAXT = MAXN << 1;
const int MAXL = 18, MAXB = 9, MAXC = MAXT / MAXB;
struct node {
int val;
int dep, dfn, end;
node *son[2]; // son[0], son[1] 分别表示左右儿子
} T[MAXN];
int n, t, b, c, Log2[MAXC + 1];
int Pos[(1 << (MAXB - 1)) + 5], Dif[MAXC + 1];
node *root, *A[MAXT], *Min[MAXL][MAXC];
void build() { // 建立 Cartesian 树
static node *S[MAXN + 1];
int top = 0;
for (int i = 0; i < n; i++) {
node *p = &T[i];
while (top && S[top]->val < p->val)
①;
if (top)
②;
S[++top] = p;
}
root = S[1];
}
void DFS(node *p) { // 构建 Euler 序列
A[p->dfn = t++] = p;
for (int i = 0; i < 2; i++)
if (p->son[i]) {
p->son[i]->dep = p->dep + 1;
DFS(p->son[i]);
A[t++] = p;
}
p->end = t - 1;
}
node *min(node *x, node *y) {
return ③ ? x : y;
}
void ST_init() {
b = (int)(ceil(log2(t) / 2));
c = t / b;
Log2[1] = 0;
for (int i = 2; i <= c; i++)
Log2[i] = Log2[i >> 1] + 1;
for (int i = 0; i < c; i++) {
Min[0][i] = A[i * b];
for (int j = 1; j < b; j++)
Min[0][i] = min(Min[0][i], A[i * b + j]);
}
for (int i = 1, l = 2; l <= c; i++, l <<= 1)
for (int j = 0; j + l <= c; j++)
Min[i][j] = min(Min[i - 1][j], Min[i - 1][j + (l >> 1)]);
}
void small_init() { // 块内预处理
for (int i = 0; i <= c; i++)
for (int j = 1; j < b && i * b + j < t; j++)
if (④)
Dif[i] |= 1 << (j - 1);
for (int S = 0; S < (1 << (b - 1)); S++) {
int mx = 0, v = 0;
for (int i = 1; i < b; i++) {
⑤;
if (v < mx) {
mx = v;
Pos[S] = i;
}
}
}
}
node *ST_query(int l, int r) {
int g = Log2[r - l + 1];
return min(Min[g][l], Min[g][r - (1 << g) + 1]);
}
node *small_query(int l, int r) { // 块内查询
int p = l / b;
int S = ⑥;
return A[l + Pos[S]];
}
node *query(int l, int r) {
if (l > r)
return query(r, l);
int pl = l / b, pr = r / b;
if (pl == pr) {
return small_query(l, r);
} else {
node *s = min(small_query(l, pl * b + b - 1), small_query(pr * b, r));
if (pl + 1 <= pr - 1)
s = min(s, ST_query(pl + 1, pr - 1));
return s;
}
}
int main() {
int m;
cin >> n >> m;
for (int i = 0; i < n; i++)
cin >> T[i].val;
build();
DFS(root);
ST_init();
small_init();
while (m--) {
int l, r;
cin >> l >> r;
cout << query(T[l].dfn, T[r].dfn)->val << endl;
}
return 0;
}第 38 题(单选题,3 分)
①处应填()
p->son[0] = S[top--]p->son[1] = S[top--]S[top--]->son[0] = pS[top--]->son[1] = p正确答案A
解析详情
大根笛卡尔树同时保持两个性质:中序遍历按原下标顺序,父结点的值不小于孩子。单调栈保存已处理部分的右侧路径,栈内值从底到顶不增。
新结点 p 比栈顶大时,栈顶应被放到 p 之下。因为这些结点的下标都在 p 左边,必须成为 p 的左子树,所以每次弹出时赋 p->son[0]=S[top--],选 A。连续弹出时最后弹出的结点带着先前子树,覆盖指针不会丢掉那些结点。
第 39 题(单选题,3 分)
②处应填()
p->son[0] = S[top]p->son[1] = S[top]S[top]->son[0] = pS[top]->son[1] = p正确答案D
解析详情
弹出所有比 p 小的栈顶后,若栈还非空,当前栈顶值不小于 p。新结点下标在其右边,因此应连接为它的右儿子:S[top]->son[1]=p,选 D。
之前被弹出的右侧子树已经挂到 p 的左边,所以改写旧栈顶的右指针仍保持中序顺序。注意弹栈条件是严格小于,因此相等值不会弹出;栈顶可能与 p 相等,不能说一定严格更大。
第 40 题(单选题,3 分)
③处应填()
x->dep < y->depx < yx->dep > y->depx->val < y->val正确答案A
解析详情
原问题虽求数组最大值,但建立大根笛卡尔树后,区间最大值对应两端结点的最近公共祖先。再转到 Euler 序,LCA 对应两次首次出现位置之间深度最小的结点。
因此 min 函数比较的是 dep:当 x->dep<y->dep 时返回 x,否则返回 y,选 A。直接比较 val 会混淆两次转化前后的问题;比较指针地址也没有树上意义。
第 41 题(单选题,3 分)
④处应填( )
A[i * b + j - 1] == A[i * b + j]->son[0]A[i * b + j]->val < A[i * b + j - 1]->valA[i * b + j] == A[i * b + j - 1]->son[1]A[i * b + j]->dep < A[i * b + j - 1]->dep正确答案D
解析详情
DFS 走向孩子时深度加 1,返回父亲时深度减 1,因此 Euler 序相邻深度差只能是 或 。本程序约定把 编成位 1,把 编成位 0。
所以第 j 个位置比前一个位置更浅时,设置第 j−1 位:条件是 A[i*b+j]->dep<A[i*b+j-1]->dep,选 D。左右孩子都可能产生去程与回程,用“是否右儿子”等局部关系不能完整识别所有差分。
第 42 题(单选题,3 分)
⑤处应填()
v += (S >> i & 1) ? -1 : 1v += (S >> i & 1) ? 1 : -1v += (S >> (i - 1) & 1) ? 1 : -1v += (S >> (i - 1) & 1) ? -1 : 1正确答案D
解析详情
预处理枚举块内差分位型 S,从相对深度 v=0 开始还原整条深度序列。由上一个空的约定,位 1 应让 v 减 1,位 0 应让 v 加 1。
从位置 i−1 走到位置 i 对应第 i−1 个差分位,因此应填 v += (S >> (i - 1) & 1) ? -1 : 1,选 D。每当 v 严格小于已有最小值 mx,就记录这个位置到 Pos[S]。变量虽名为 mx,实际上维护最小深度,不应被名字误导。
第 43 题(单选题,3 分)
⑥处应填( )
(Dif[p] >> (r - p * b)) & ((1 << (r - l)) - 1)Dif[p](Dif[p] >> (l - p * b)) & ((1 << (r - l)) - 1)(Dif[p] >> ((p + 1) * b - r)) & ((1 << (r - l + 1)) - 1)正确答案C
解析详情
块 p 从位置 p*b 开始。查询闭区间 [l,r] 有 r−l+1 个结点,只有 r−l 个相邻差分;它的首个差分位在块内偏移 l−p*b。
先将 Dif[p] 右移 l−p*b,再用低 r−l 位全为 1 的掩码截取,得到 (Dif[p] >> (l - p * b)) & ((1 << (r - l)) - 1),选 C。
为什么可以直接查统一长度的 Pos 表?截取后的高位全为 0,预处理将这些补位解释成连续的深度加 1,不可能产生更小深度,因此最小值位置仍在真实查询段内。若 l=r,掩码为 0,Pos[0]=0,恰好返回 A[l]。
左右滑动表格,可查看完整内容。
| 步骤 | 计算 | 含义 |
|---|---|---|
| 偏移 | 12 − 10 = 2 | 跳过块内前两个差分 |
| 长度 | 15 − 12 = 3 | 保留三个差分位 |
| 掩码 | (1 << 3) − 1 = 7 | 二进制 111 |
| 截取 | (Dif[p] >> 2) & 7 | 得到从位置 12 开始的相对深度变化 |
假设块起点为 10、查询 [12,15];四个结点只需三个差分。