GESP 客观题评测系统

试卷解析 · 理解每一步

2021 CCF CSP-S 第一轮(提高级 C++)

CSPS-2021-R1

试卷解析总览,可直接查看每题答案与解析。

一、单项选择题

第 1 题(单选题,2 分)

在 Linux 系统终端中,用于列出当前目录下所含的文件和子目录的命令为()。

A.
ls
B.
cd
C.
cp
D.
all

正确答案A

解析详情
答案 ALinux 常用命令
思路与推导
  1. 先明确任务:查看当前目录中有哪些文件,而不是改变所在位置。ls 不带路径时列出当前目录内容,因此选 A。

  2. cd 用于切换目录,cp 用于复制文件;all 不是本题所需的列目录命令。若要显示点号开头的隐藏文件,通常另加 -a 选项,但不影响本题判断。

第 2 题(单选题,2 分)

二进制数 001010102 和 000101102 的和为()。

A.
001111002
B.
010000002
C.
001111002
D.
010000102

正确答案B

解析详情
答案 B二进制加法
思路与推导
  1. 先按位权转换:001010102=32+8+2=4200101010_2=32+8+2=42,000101102=16+4+2=2200010110_2=16+4+2=22。相加得到 64=2664=2^6,写成八位二进制是 01000000201000000_2,选 B。

  2. 也可从右向左竖式相加:最低位为 0,第二位的 1+11+1 产生进位,此后连续向高位传递。不能把二进制中的“满二进一”当作十进制加法。

第 3 题(单选题,2 分)

在程序运行过程中,如果递归调用的层数过多,可能会由于( )引发错误。

A.
系统分配的栈空间溢出
B.
系统分配的队列空间溢出
C.
系统分配的链表空间溢出
D.
系统分配的堆空间溢出

正确答案A

解析详情
答案 A递归与调用栈
思路与推导
  1. 普通递归调用需要保存当前函数的返回位置、局部变量等信息,形成一层调用栈帧。递归没有返回时,这些栈帧会同时保留;深度不断增加,所需栈空间也不断增加。

  2. 系统为线程提供的栈空间有限,深度过大可能造成栈溢出,所以选 A。此处讨论通常的递归实现;编译器是否能优化某些特殊递归,不改变“可能栈溢出”的判断。

第 4 题(单选题,2 分)

以下排序方法中,()是不稳定的。

A.
插入排序
B.
冒泡排序
C.
堆排序
D.
归并排序

正确答案C

解析详情
答案 C排序算法的稳定性
思路与推导
  1. 稳定性要求:两个关键字相等的元素,排序后仍保持原来的先后次序。可给相等元素附上标签,再观察标签顺序。

  2. 堆排序会把堆顶与末尾元素交换,交换可能跨过另一个相等元素。例如两个相等元素组成的堆,取出堆顶时的一次交换就可能颠倒二者。因此堆排序不稳定,选 C。通常教材实现中,插入排序、冒泡排序及合并时优先取左侧相等元素的归并排序均可保持稳定。

第 5 题(单选题,2 分)

以比较为基本运算,对于 2n 个数,同时找到最大值和最小值,最坏情况下需要的最小的比较次数为()。

A.
4n-2
B.
3n+13n+1
C.
3n - 2
D.
2n+12n+1

正确答案C

解析详情
答案 C比较模型与极值查找
思路与推导
  1. 先把 2n2n 个数分成 nn 对,每对比较一次,共 nn 次。每对较大者进入最大值候选组,较小者进入最小值候选组。

  2. 从 nn 个较大者中找最大值需 n−1n-1 次,从 nn 个较小者中找最小值也需 n−1n-1 次,合计 n+(n−1)+(n−1)=3n−2n+(n-1)+(n-1)=3n-2,选 C。

  3. 为什么这种配对能达到最优?每个非最大元素都必须有一次“输”的证据,每个非最小元素都必须有一次“赢”的证据。比较对抗分析中,只有尚未比较过的两个元素首次配对,才能一次同时有效减少两类候选;最多有 nn 次这样的双重收益,其余候选仍需逐个排除,得到同样的下界。

第 6 题(单选题,2 分)

现有一个地址区间为 θ∼1θ\theta \sim 1\theta 的哈希表,对于出现冲突情况,会往后找第一个空的地址存储(到 10 冲突了就从 θ\theta 开始往后),现在要依次存储( θ\theta,1,2,3,4,5,6,7),哈希函数为 h(x)=x2mod  11h(x) = x^2 \mod 11。请问 7 存储在哈希表哪个地址中()。

A.
5
B.
6
C.
7
D.
8

正确答案C

解析详情
答案 C哈希表与线性探测
思路与推导
  1. 依次算初始地址并处理已有冲突:0、1、2、3、4、5 的初始地址分别是 0、1、4、9、5、3,均为空,可以直接存放。

  2. 插入 6 时,36 mod 11=336\bmod11=3。地址 3、4、5 已占用,继续探测到空地址 6,因此 6 存在地址 6。

  3. 插入 7 时,49 mod 11=549\bmod11=5。地址 5、6 都已占用,第一个空位是 7,因此选 C。线性探测应检查已经存入的所有元素,而不只是上一个数。

第 7 题(单选题,2 分)

G 是一个非连通简单无向图(没有自环和重边),共有 36 条边,则该图至少有()个点。

A.
8
B.
9
C.
10
D.
11

正确答案C

解析详情
答案 C简单无向图的边数上界
思路与推导
  1. 要在固定点数下容纳尽量多的边,应把尽量多的点集中到一个完全图中,同时留下至少一个独立点以保证非连通。因此 vv 个点最多有 (v−12)\binom{v-1}{2} 条边。

  2. 若只有 9 个点,至多是 K8K_8 加一个独立点,边数为 28,不够 36。若有 10 个点,K9K_9 加一个独立点恰有 9×8/2=369\times8/2=36 条边。下界可达到,所以至少需要 10 个点,选 C。

第 8 题(单选题,2 分)

令根结点的高度为 1,则一棵含有 2021 个结点的二叉树的高度至少为()。

A.
10
B.
11
C.
12
D.
2021

正确答案B

解析详情
答案 B二叉树高度与结点数
思路与推导
  1. 根结点处于第 1 层,第 ii 层最多有 2i−12^{i-1} 个结点。高度为 hh 的二叉树最多容纳 1+2+⋯+2h−1=2h−11+2+\cdots+2^{h-1}=2^h-1 个结点。

  2. 高度 10 时上限为 1023,小于 2021;高度 11 时上限为 2047,可以容纳 2021 个结点。因此最小高度是 11,选 B。这个问题求的是“至少”,应考虑树尽量宽,而不是退化为链的情况。

第 9 题(单选题,2 分)

前序遍历和中序遍历相同的二叉树为且仅为()。

A.
只有 1 个点的二叉树
B.
根结点没有左子树的二叉树
C.
非叶子结点只有左子树的二叉树
D.
非叶子结点只有右子树的二叉树

正确答案D

解析详情
答案 D二叉树遍历
思路与推导
  1. 前序次序是“根、左、右”,中序次序是“左、根、右”。若某结点存在非空左子树,前序中该结点先于左子树,中序中却晚于左子树,两种次序就不同。

  2. 所以不仅根没有左子树,右子树里的每个结点也都不能有左子树。反过来,若整棵树是一条向右延伸的链,两种遍历都从根沿右儿子访问,次序相同。因此选 D。这里沿用遍历题通常假定的结点可区分条件。

第 10 题(单选题,2 分)

定义一种字符串操作为交换相邻两个字符。将 “DACFEB” 变为 “ABCDEF” 最少需要()次上述操作。

A.
7
B.
8
C.
9
D.
6

正确答案A

解析详情
答案 A逆序对与相邻交换
思路与推导
  1. 按目标字母顺序将 DACFEB 写为 4,1,3,6,5,24,1,3,6,5,2。逐个统计右侧更小的数:4 后面有 1、3、2,共 3 个;1 后面没有;3 后面有 2;6 后面有 5、2;5 后面有 2。

  2. 逆序对总数为 3+0+1+2+1=73+0+1+2+1=7。一次相邻交换最多消去一个逆序对,所以至少要 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.
1
B.
7
C.
12
D.
22

正确答案A

解析详情
答案 A费马小定理
思路与推导
  1. 递归边界 solve(1,n)=1,每增加一层就多乘一个 5 并取模,因此 solve(t,n) 等于 5t−1 mod n5^{t-1}\bmod n。代入本题得到 522 mod 235^{22}\bmod23。

  2. 23 是质数,且 5 与 23 互质,费马小定理给出 522≡1(mod23)5^{22}\equiv1\pmod{23},所以选 A。指数是 22,因为边界层直接返回 1,并不再乘 5。

第 12 题(单选题,2 分)

斐波那契数列的定义为:F1=1F_1=1,F2=1F_2=1,Fn=Fn−1+Fn−2F_n=F_{n-1}+F_{n-2}(n>=3n>=3)。现在用如下程序来计算斐波那契数列的第 nn 项:

F(n):
    if n <= 2 return 1
    else return F(n - 1) + F(n - 2)

其时间复杂度为( )。

A.
O(n)O(n)
B.
O(n2)O(n^{2})
C.
O(2n)O(2^{n})
D.
O(nlog⁡n)O(n \log n)

正确答案C

解析详情
答案 C递归时间复杂度
思路与推导
  1. 令调用工作量为 T(n)T(n)。当 n>2n>2 时,要分别计算 F(n−1)F(n-1) 和 F(n−2)F(n-2),所以 T(n)=T(n−1)+T(n−2)+O(1)T(n)=T(n-1)+T(n-2)+O(1)。相同子问题会在不同分支反复计算。

  2. 递归树高度为 O(n)O(n),每个结点至多分出两个孩子,可以用 O(2n)O(2^n) 作为上界,给定选项中选 C。更紧的增长量是 Θ(φn)\Theta(\varphi^n),其中 φ=(1+5)/2\varphi=(1+\sqrt5)/2;这不影响 O(2n)O(2^n) 这个正确上界。若改成记忆化或顺序递推,才可降到线性时间。

第 13 题(单选题,2 分)

有 8 个苹果从左到右排成一排,你要从中挑选至少一个苹果,并且不能同时挑选相邻的两个苹果,一共有( )种方案。

A.
36
B.
48
C.
54
D.
64

正确答案C

解析详情
答案 C组合计数递推
思路与推导
  1. 先允许一个也不选,设前 ii 个苹果的合法方案数为 fif_i。若不选第 ii 个,有 fi−1f_{i-1} 种;若选第 ii 个,第 i−1i-1 个必须不选,剩下有 fi−2f_{i-2} 种。

  2. 因此 fi=fi−1+fi−2f_i=f_{i-1}+f_{i-2},从 f0=1,f1=2f_0=1,f_1=2 依次得到 f2f_2 至 f8f_8 为 3、5、8、13、21、34、55。

  3. 最后排除“一个也不选”的唯一方案,得到 55−1=5455-1=54,选 C。

第 14 题(单选题,2 分)

设一个三位数 n=abc‾n = \overline{abc},a,b,c 均为 1~9 之间的整数,若以 a、b、c 作为三角形的三条边可以构成等腰三角形(包括等边),则这样的 n 有( )个。

A.
81
B.
120
C.
165
D.
216

正确答案C

解析详情
答案 C分类计数与三角形条件
思路与推导
  1. 分成等边和恰有两边相等两类。等边对应 111、222、……、999,共 9 个三位数。

  2. 恰有两边相等时,设边长为 x,x,yx,x,y,其中 1≤x,y≤91\le x,y\le9 且 $y

  3. e x。正边长下只需额外保证。正边长下只需额外保证 y<2x。当。当 x=1,2,\ldots,9时,可选的 时,可选的 y$ 个数依次为 0、2、4、6、8、8、8、8、8,总计 52。

  4. 每组不同的 x,yx,y,不相等的数字 yy 可放在百、十、个位三个位置,因此共 9+52×3=1659+52\times3=165,选 C。把等边先单列,能避免把它重复乘 3。

图解 · 看懂这一步等腰三角形的分类计数

左右滑动表格,可查看完整内容。

相等边 x可取的不等边 y数量
1无0
21、32
31、2、4、54
41、2、3、5、6、76
51 至 9,除 58
61 至 9,除 68
71 至 9,除 78
81 至 9,除 88
91 至 9,除 98

这里只数恰有两边相等的 x,x,y;每组还有三个位置排列,等边 9 种另外计算。

第 15 题(单选题,2 分)

有如下的有向图,节点为 A,B,...,J,其中每条边的长度都标在图中。则节点 A 到节点 J 的最短路径长度为( )。

Image
A.
16
B.
19
C.
20
D.
22

正确答案B

解析详情
答案 B有向图最短路径
思路与推导
  1. 按图的层次从左向右计算,记 d(X)d(X) 为 A 到 X 的最短距离。第一层有 d(B)=2,d(C)=5,d(D)=1d(B)=2,d(C)=5,d(D)=1。

  2. 第二层:d(E)=min⁡(2+12,5+6,1+13)=11d(E)=\min(2+12,5+6,1+13)=11;d(F)=min⁡(2+14,5+10,1+12)=13d(F)=\min(2+14,5+10,1+12)=13;d(G)=min⁡(2+10,5+4,1+11)=9d(G)=\min(2+10,5+4,1+11)=9。

  3. 第三层:d(H)=min⁡(11+3,13+6,9+8)=14d(H)=\min(11+3,13+6,9+8)=14;d(I)=min⁡(11+9,13+5,9+10)=18d(I)=\min(11+9,13+5,9+10)=18。最后 d(J)=min⁡(14+5,18+2)=19d(J)=\min(14+5,18+2)=19,选 B。对应路径为 A→C→E→H→J。这样既给出一条路径,也证明其他进入各层的路径不会更短。

图解 · 看懂这一步按层求最短路

左右滑动表格,可查看完整内容。

顶点最短距离选择的前驱
A0起点
B2A
C5A
D1A
E11C
F13D
G9C
H14E
I18F
J19H

每个点取所有前驱路径长度的最小值;所列前驱给出一条最短路径。

二、阅读程序(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,不会影响程序运行的结果。()

正确答案正确

解析详情
答案 正确数值类型转换
思路与推导
  1. 先检查取值范围。每个坐标绝对值不超过 1000,坐标差绝对值不超过 2000,三个平方和至多 3×20002=120000003\times2000^2=12000000,在通常的 32 位 int 范围内,也可由 double 精确表示。

  2. 改类型后,存入 t 的整数值没有变化;后面比较用到的整数及平方根运算也具有相同数值。因此按题目通常的数值环境,输出不变,判断正确。不能仅凭“double 更精确”下结论,关键是本题原来的整数计算没有溢出。

第 17 题(判断题,1.5 分)

将第 26、27 行中的 “/ sqrt(t) / 2” 替换为 “/ 2 / sqrt(t)”,不会影响程序运行的结果。()

正确答案错误

解析详情
答案 错误整数除法
思路与推导
  1. 原式分子为整数,但先除以 sqrt(t) 后执行浮点除法;改成先 /2 时,分子与 2 都是整数,会先截去小数部分。

  2. 用题中两单位球、球心距为 1 的输入举例:分子为 1,原式 1/1/2=0.51/1/2=0.5,得到 x=y=0.5x=y=0.5;改后整数 1/2=01/2=0,得到 x=y=1x=y=1。最终输出会从 1.3090 变成 4.1888,所以判断错误。

第 18 题(判断题,1.5 分)

将第28行中的“x*x”改成“sq(x)”、“y*y”改成“sq(y)”,不会影响程序运行的结果。()

正确答案错误

解析详情
答案 错误隐式类型转换
思路与推导
  1. 函数声明 sq(const int x) 决定了实参先转成整数,然后才平方。inline 只是实现层面的提示,不会改变参数类型。

  2. 仍取半径都为 1、球心距为 1 的情况,原程序中 x=y=0.5x=y=0.5,直接平方为 0.25;调用 sq(x) 时却先把 0.5 转为 0,平方为 0。相交体积将被算成 0,输出改变,因此判断错误。

第 19 题(判断题,2 分)

当输入为“0 0 0 1 1 0 0 1”时,输出为“1.3090”。()

正确答案正确

解析详情
答案 正确两球相交体积
思路与推导
  1. 输入表示两个半径为 1 的球,球心分别为 (0,0,0)(0,0,0) 和 (1,0,0)(1,0,0)。距离平方 t=1t=1,大于半径差平方 0、小于半径和平方 4,进入部分相交分支。

  2. r=arccos⁡(0.5)=π/3r=\arccos(0.5)=\pi/3,x=y=1−(1−1+1)/1/2=0.5x=y=1-(1-1+1)/1/2=0.5。代入输出式得到 [0.25×2.5+0.25×2.5]π/3=5π/12≈1.30899694[0.25\times2.5+0.25\times2.5]\pi/3=5\pi/12\approx1.30899694。保留四位小数为 1.3090,判断正确。

第 20 题(单选题,3 分)

当输入为 “1 1 1 1 1 1 1 2” 时,输出为()。

A.
“3.1416”
B.
“6.2832”
C.
“4.7124”
D.
“4.1888”

正确答案D

解析详情
答案 D球的包含关系
思路与推导
  1. 两个球的球心都是 (1,1,1)(1,1,1),所以 t=0t=0;半径分别为 1 和 2,半径差平方为 1。满足 t≤(d2−d1)2t\le(d_2-d_1)^2,进入第一个分支。

  2. 程序取较小半径 1,输出 13×(π/3)×4=4π/3≈4.188790201^3\times(\pi/3)\times4=4\pi/3\approx4.18879020。固定四位小数得到 4.1888,选 D。这里直接使用包含分支,不会执行除以球心距的公式。

第 21 题(单选题,2.5 分)

这段代码的含义为()。

A.
求圆的面积并
B.
求球的体积并
C.
求球的体积交
D.
求椭球的体积并

正确答案C

解析详情
答案 C球体相交体积
思路与推导
  1. 把前三个输入量看作球心坐标,第四个量看作半径,则 t 是球心距平方。若距离不大于半径差,交集是较小的整个球,程序输出其体积;若距离不小于半径和,交集体积为 0。

  2. 剩下情况是部分相交。x、y 是公共截面两侧球冠的高度,球冠体积公式为 V=πh2(3R−h)/3V=\pi h^2(3R-h)/3。程序把两个球冠体积相加,恰好得到两球公共部分的体积,因此选 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 分)

程序总是会正常执行并输出两行两个相等的数。()

正确答案正确

解析详情
答案 正确最大子段和
思路与推导
  1. 先读懂 Node:h 为最大前缀和,j 为最大子段和,m 为最大后缀和,w 为区间总和,前三项允许空段,因此至少为 0。合并时最大子段只可能完全在左边、完全在右边,或由左边最大后缀与右边最大前缀拼成。

  2. solve1 把这四项一起返回;solve2 用循环现算左后缀和右前缀,再与两侧递归答案比较。两者枚举的三类情况完全相同,因此在题目预期的有效数组范围和无整数溢出条件下,输出相等,原题判断为正确。全负数组也相等,均为 0。

第 23 题(判断题,1.5 分)

第 28 行与第 38 行分别有可能执行两次及以上。()

正确答案错误

解析详情
答案 错误分治递归边界
思路与推导
  1. 当初始 n≥1n\ge1 时,递归参数是非空闭区间。单个元素直接返回;长度至少为 2 时,中点 j 满足 h≤j<mh\le j<m,所以 [h,j]、[j+1,m] 都非空。由归纳可知不会进入 h>m。

  2. 即使考虑初始 n=0n=0 的空区间,每个函数也只会在第一次调用时返回一次,不可能各执行两次及以上。因此判断错误。

第 24 题(判断题,1.5 分)

当输入为 “5 -10 11 -9 5 -7” 时,输出的第二行为 “7”。()

正确答案错误

解析详情
答案 错误最大子段和
思路与推导
  1. 首个数 5 是数组长度,实际序列为 −10,11,−9,5,−7-10,11,-9,5,-7。逐项维护“以当前位置结尾、允许为空的最大和”,递推为 si=max⁡(0,si−1+ai)s_i=\max(0,s_{i-1}+a_i)。

  2. 依次得到 0、11、2、7、0,全局最大值为 11,对应只选第二项。7 只是处理到第四项时的局部结果,不能覆盖此前已出现的 11。两行均输出 11,题述第二行为 7,故判断错误。

第 25 题(单选题,3 分)

solve1(1, n) 的时间复杂度为()。

A.
Θ(log⁡n)\Theta(\log n)
B.
Θ(n)\Theta(n)
C.
Θ(nlog⁡n)\Theta(n\log n)
D.
Θ(n2)\Theta(n^{2})

正确答案B

解析详情
答案 B分治复杂度
思路与推导
  1. solve1 的每个叶子处理一个元素,内部结点通过重载加法做固定次数的加法和比较,合并耗时为 Θ(1)\Theta(1)。

  2. 一棵有 n 个叶子的二叉递归树共有 2n−12n-1 个结点,每个结点工作量为常数,所以总时间为 Θ(n)\Theta(n),选 B。也可写递推式 T(n)=2T(n/2)+Θ(1)T(n)=2T(n/2)+\Theta(1);不能把递归层数直接当作总时间。

第 26 题(单选题,3 分)

solve2(1, n) 的时间复杂度为()。

A.
Θ(log⁡n)\Theta(\log n)
B.
Θ(n)\Theta(n)
C.
Θ(nlog⁡n)\Theta(n\log n)
D.
Θ(n2)\Theta(n^{2})

正确答案C

解析详情
答案 C分治复杂度
思路与推导
  1. solve2 在每个非叶区间中,从中点向两侧分别扫描。这两个循环合计访问区间内的所有元素,所以长度为 k 的一次调用,合并部分需 Θ(k)\Theta(k)。

  2. 同一递归层的区间互不重叠,总长度为 n,因此每层总工作量为 Θ(n)\Theta(n)。递归有 Θ(log⁡n)\Theta(\log n) 层,总时间为 Θ(nlog⁡n)\Theta(n\log n),选 C。与上一题的差别正是合并需要重新扫描。

第 27 题(单选题,3 分)

当输入为 “10 -3 2 10 0 -8 9 -4 -5 9 4” 时,输出的第一行为()。

A.
“13”
B.
“17”
C.
“24”
D.
“12”

正确答案B

解析详情
答案 B最大子段和
思路与推导
  1. 使用与分治答案等价的线性手推:以当前位置结尾的最大非负子段和,依次为 0、2、12、12、4、13、9、4、13、17。过程中出现的最大值为 17。

  2. 达到它的连续子段是第 2 至第 10 项:2+10+0−8+9−4−5+9+4=172+10+0-8+9-4-5+9+4=17,因此第一行输出 17,选 B。中间虽有负数,但连接两端正数仍可能比舍弃整段更优。

图解 · 看懂这一步最大子段和的逐项手推

左右滑动表格,可查看完整内容。

位置元素以此处结尾的最大和全局最大和
1-300
2222
3101212
401212
5-8412
691313
7-4913
8-5413
991313
1041717

允许空子段;当前值为 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 分)

程序总是先输出一行一个整数,再输出一行一个字符串。()

正确答案错误

解析详情
答案 错误字符串解码与数组边界
思路与推导
  1. 输入格式合法仅表示读到了整数和不含空白的字符串,并没有保证非零 opt 对应的字符串是有效 Base64。decode 每次无条件按 4 个字符处理。

  2. 例如输入 1 A,循环仍会访问 str[i+2]、str[i+3],超过该字符串允许访问的位置,产生未定义行为,不能保证正常输出两行。因此判断错误。正常解码需要另外校验字符集合、长度和填充位置。

第 29 题(判断题,1.5 分)

对于任意不含空白字符的字符串 str1,先执行程序输入 “0 str1”,得到输出的第二行记为 str2;再执行程序输入 “1 str2”,输出的第二行必为 str1。()

正确答案正确

解析详情
答案 正确Base64 编解码
思路与推导
  1. 按本题预期的普通 ASCII 字符模型,编码把 3 个字节的 24 位拆成 4 个 6 位索引,解码再将这些位拼回原来的 3 个字节。最后不足 3 字节时,编码加 =,解码通过 = 判断只还原 1 或 2 个字节。因此对编码结果再解码可还原原串,原题答案为正确。

  2. 需区分命题意图与代码适用范围:题面“任意不含空白字符”若包含最高位为 1 的任意字节,在有符号 char 环境中,str[i] >> 2 可能为负并造成负下标。本实现没有先转为 unsigned char,不能据此证明对任意字节串都安全;这里保留原答案,并明确其字符范围前提。

第 30 题(判断题,1.5 分)

当输入为 “1 SGVsbG93b3JsZA==” 时,输出的第二行为 “HelloWorld”。()

正确答案错误

解析详情
答案 错误Base64 解码
思路与推导
  1. 先确定 opt 为 1,程序执行解码。每四个编码字符还原一组:SGVs→Hel,bG93→low,b3Js→orl,ZA==→d。

  2. 拼接得到 Helloworld,其中第六个字符是小写 w,而题述 HelloWorld 使用大写 W。字符大小写不同,字符串就不相等,所以判断错误。

第 31 题(单选题,3 分)

设输入字符串长度为 n,encode 函数的时间复杂度为()。

A.
Θ(n)\Theta(\sqrt{n})
B.
Θ(n)\Theta(n)
C.
Θ(nlog⁡n)\Theta(n \log n)
D.
Θ(n2)\Theta(n^2)

正确答案B

解析详情
答案 B线性时间复杂度
思路与推导
  1. 主循环每次让 i 增加 3,每轮处理 3 个输入字符并追加 4 个输出字符,因此循环次数为 ⌊n/3⌋\lfloor n/3\rfloor。末尾分支最多处理剩余两个字符。

  2. 在字符串追加按摊还常数时间计算的通常模型下,总耗时为 Θ(n)\Theta(n),选 B;输出长度 4⌈n/3⌉4\lceil n/3\rceil 也与输入长度同阶。位运算次数固定,并不会引入对数因子。

第 32 题(单选题,3 分)

输出的第一行为()。

A.
“0xff”
B.
“255”
C.
“0xFF”
D.
“-1”

正确答案D

解析详情
答案 Dchar 的符号扩展
思路与推导
  1. 初始化先把 table 的所有位置设为 0xff,随后只覆盖 Base64 字符以及等号对应的位置。下标 0 对应空字符,不在这些字符中,所以仍保留全 1 的字节位型。

  2. 本题答案采用 8 位有符号 char 环境,全 1 位型表示 -1,转换成 int 后输出十进制 -1,选 D。C++ 的普通 char 是否有符号取决于实现,若使用无符号 char 则可能得到 255;因此这道题具有环境前提,不是跨平台固定输出。

第 33 题(单选题,4 分)

当输入为 “0 CSP2021csp” 时,输出的第二行为()。

A.
“Q1NQMjAyMWNzcAv=”
B.
“Q1NQMjAyMGNzcA==”
C.
“Q1NQMjAyMGNzcAv=”
D.
“Q1NQMjAyMWNzcA==”

正确答案D

解析详情
答案 DBase64 编码
思路与推导
  1. 把 10 个字符按三个一组分为 CSP、202、1cs、p。前三组分别编码为 Q1NQ、MjAy、MWNz;最后仅剩一个字节 p,其编码为 cA==。

  2. 例如首组 ASCII 值为 67、83、80,拆出的四个索引是 16、53、13、16,对应 Q、1、N、Q。将四组拼接,得到 Q1NQMjAyMWNzcA==,选 D。末尾两个等号来自只剩一个字节,而不是编码的普通字符。

图解 · 看懂这一步Base64 分组编码

左右滑动表格,可查看完整内容。

组别输入输出
第 1 组CSPQ1NQ
第 2 组202MjAy
第 3 组1csMWNz
第 4 组pcA==

每 3 个字节变成 4 个编码字符;最后只有 1 个字节,补两个等号。

三、完善程序(1)

(1)(魔法数字)小 H 的魔法数字是 4。给定 n,他希望用若干个 4 进行若干次加法、减法和整除运算得到 n。但由于小 H 计算能力有限,计算过程中只能出现不超过 M = 10000 的正整数。求至少可能用到多少个 4。

例如,当 n=2 时,有 2=(4+4)/42=(4+4)/4,用到了 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 分)

①处应填( )

A.
F[4] = 0
B.
F[1] = 4
C.
F[1] = 2
D.
F[4] = 1

正确答案D

解析详情
答案 D状态初始化
思路与推导
  1. 先定义代价:F[x] 是表示正整数 x 所用的数字 4 的最少个数。无需任何运算就能写出一个 4,因此初始已知状态是 F[4]=1,选 D,其余状态暂为无穷大。

  2. 若设为 0,就把数字 4 本身的使用次数漏掉了。F[1]=2 虽对应表达式 4/44/4,却不应代替基础状态;算法应从最基本的一个 4 自动推导这些组合。

第 35 题(单选题,3 分)

②处应填()

A.
!Vis[n]
B.
r < n
C.
F[M] == INT_MAX
D.
F[n] == INT_MAX

正确答案A

解析详情
答案 ADijkstra 式状态扩展
思路与推导
  1. Vis[x] 表示 x 的最小代价已经确定。每轮从未确定状态中选择 F 最小的一个,再与已确定状态组合产生候选表达式。两个子表达式各至少用一个 4,所以组合的代价严格大于每个子表达式的代价,这支持按代价递增确定状态。

  2. 当目标 n 被确定后即可结束,故循环条件是 !Vis[n],选 A。仅仅发现 F[n] 为有限值,不表示该方案已经最优;轮数 r 也不等于目标值或最优代价。

第 36 题(单选题,3 分)

③处应填()

A.
F[i] == r
B.
!Vis[i] && F[i] == r
C.
F[i] < F[x]
D.
!Vis[i] && F[i] < F[x]

正确答案D

解析详情
答案 D最小未访问状态
思路与推导
  1. 每轮先令 x=0,而 F[0]=INT_MAX 是哨兵。扫描 1 至 M 时,只考虑尚未确定的 i,并在 F[i]<F[x] 时更新 x。

  2. 所以条件必须同时满足 !Vis[i] 和 F[i]<F[x],选 D。若没有未访问条件,会反复选中已经确定的便宜状态;若按 r 筛选,则把“第几次确定状态”和“用了几个 4”误当成同一件事。

第 37 题(单选题,3 分)

④处应填( )

A.
F[i] < F[x]
B.
F[i] <= r
C.
Vis[i]
D.
i <= x

正确答案C

解析详情
答案 C状态转移
思路与推导
  1. 刚确定 x 后,需要与每个已经确定的 i 组合。相加、作正差或整除,都能得到一个新表达式,所用 4 的个数为 F[i]+F[x],据此松弛相应状态。因此填 Vis[i],选 C。

  2. 每一对状态会在较晚确定的那个状态被处理时得到组合机会;x 刚被标记为已确定,也允许 i=x。例如 4/4=14/4=1 恰好需要把两个 4 组合,代价是 2。只有除数整除被除数时才尝试除法,且差为 0 的情况被排除,符合题目只允许正整数的限制。

三、完善程序(2)

(2)(RMQ 区间最值问题)给定序列 a0,…,an−1a_0, \ldots, a_{n-1},和 mm 次询问,每次询问给定 l,rl, r,求 max⁡{al,…,ar}\max \{a_l, \ldots, a_r\}。

为了解决该问题,有一个算法叫 the Method of Four Russians,其时间复杂度为 O(n+m)O(n + m),步骤如下:

- 建立 Cartesian(笛卡尔)树,将问题转化为树上的 LCA(最近公共祖先)问题。

- 对于 LCA 问题,可以考虑其 Euler 序(即按照 DFS 过程,经过所有点,环游回根的序列),即求 Euler 序列上两点间一个新的 RMQ 问题。

- 注意新的问题为 ±1\pm 1 RMQ,即相邻两点的深度差一定为 1。

下面解决这个 ±1\pm1 RMQ 问题,“序列”指 Euler 序列:

设 tt为 Euler 序列长度。取b=⌈log⁡2t2⌉b = \left\lceil \frac{\log_2 t}{2} \right\rceil。将序列每 bb个分为一大块,使用 ST 表(倍增表)处理大块间的 RMQ 问题,复杂度O(tblog⁡t)=O(n)O\left(\frac{t}{b}\log t\right)=O(n)。

• (重点)对于一个块内的 RMQ 问题,也需要 O(1)O(1) 的算法。由于差分数组 2b−12^{b-1} 种,可以预处理出所有情况下的最值位置,预处理复杂度 O(b2b)O(b2^{b}),不超过 O(n)O(n)。

- 最终,对于一个查询,可以转化为中间整的大块的 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 分)

①处应填()

A.
p->son[0] = S[top--]
B.
p->son[1] = S[top--]
C.
S[top--]->son[0] = p
D.
S[top--]->son[1] = p

正确答案A

解析详情
答案 A单调栈构建笛卡尔树
思路与推导
  1. 大根笛卡尔树同时保持两个性质:中序遍历按原下标顺序,父结点的值不小于孩子。单调栈保存已处理部分的右侧路径,栈内值从底到顶不增。

  2. 新结点 p 比栈顶大时,栈顶应被放到 p 之下。因为这些结点的下标都在 p 左边,必须成为 p 的左子树,所以每次弹出时赋 p->son[0]=S[top--],选 A。连续弹出时最后弹出的结点带着先前子树,覆盖指针不会丢掉那些结点。

第 39 题(单选题,3 分)

②处应填()

A.
p->son[0] = S[top]
B.
p->son[1] = S[top]
C.
S[top]->son[0] = p
D.
S[top]->son[1] = p

正确答案D

解析详情
答案 D单调栈构建笛卡尔树
思路与推导
  1. 弹出所有比 p 小的栈顶后,若栈还非空,当前栈顶值不小于 p。新结点下标在其右边,因此应连接为它的右儿子:S[top]->son[1]=p,选 D。

  2. 之前被弹出的右侧子树已经挂到 p 的左边,所以改写旧栈顶的右指针仍保持中序顺序。注意弹栈条件是严格小于,因此相等值不会弹出;栈顶可能与 p 相等,不能说一定严格更大。

第 40 题(单选题,3 分)

③处应填()

A.
x->dep < y->dep
B.
x < y
C.
x->dep > y->dep
D.
x->val < y->val

正确答案A

解析详情
答案 AEuler 序与 LCA
思路与推导
  1. 原问题虽求数组最大值,但建立大根笛卡尔树后,区间最大值对应两端结点的最近公共祖先。再转到 Euler 序,LCA 对应两次首次出现位置之间深度最小的结点。

  2. 因此 min 函数比较的是 dep:当 x->dep<y->dep 时返回 x,否则返回 y,选 A。直接比较 val 会混淆两次转化前后的问题;比较指针地址也没有树上意义。

第 41 题(单选题,3 分)

④处应填( )

A.
A[i * b + j - 1] == A[i * b + j]->son[0]
B.
A[i * b + j]->val < A[i * b + j - 1]->val
C.
A[i * b + j] == A[i * b + j - 1]->son[1]
D.
A[i * b + j]->dep < A[i * b + j - 1]->dep

正确答案D

解析详情
答案 D±1 RMQ 差分编码
思路与推导
  1. DFS 走向孩子时深度加 1,返回父亲时深度减 1,因此 Euler 序相邻深度差只能是 +1+1 或 −1-1。本程序约定把 −1-1 编成位 1,把 +1+1 编成位 0。

  2. 所以第 j 个位置比前一个位置更浅时,设置第 j−1 位:条件是 A[i*b+j]->dep<A[i*b+j-1]->dep,选 D。左右孩子都可能产生去程与回程,用“是否右儿子”等局部关系不能完整识别所有差分。

第 42 题(单选题,3 分)

⑤处应填()

A.
v += (S >> i & 1) ? -1 : 1
B.
v += (S >> i & 1) ? 1 : -1
C.
v += (S >> (i - 1) & 1) ? 1 : -1
D.
v += (S >> (i - 1) & 1) ? -1 : 1

正确答案D

解析详情
答案 D位掩码与块内预处理
思路与推导
  1. 预处理枚举块内差分位型 S,从相对深度 v=0 开始还原整条深度序列。由上一个空的约定,位 1 应让 v 减 1,位 0 应让 v 加 1。

  2. 从位置 i−1 走到位置 i 对应第 i−1 个差分位,因此应填 v += (S >> (i - 1) & 1) ? -1 : 1,选 D。每当 v 严格小于已有最小值 mx,就记录这个位置到 Pos[S]。变量虽名为 mx,实际上维护最小深度,不应被名字误导。

第 43 题(单选题,3 分)

⑥处应填( )

A.
(Dif[p] >> (r - p * b)) & ((1 << (r - l)) - 1)
B.
Dif[p]
C.
(Dif[p] >> (l - p * b)) & ((1 << (r - l)) - 1)
D.
(Dif[p] >> ((p + 1) * b - r)) & ((1 << (r - l + 1)) - 1)

正确答案C

解析详情
答案 C位区间截取
思路与推导
  1. 块 p 从位置 p*b 开始。查询闭区间 [l,r] 有 r−l+1 个结点,只有 r−l 个相邻差分;它的首个差分位在块内偏移 l−p*b。

  2. 先将 Dif[p] 右移 l−p*b,再用低 r−l 位全为 1 的掩码截取,得到 (Dif[p] >> (l - p * b)) & ((1 << (r - l)) - 1),选 C。

  3. 为什么可以直接查统一长度的 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];四个结点只需三个差分。