试卷解析 · 理解每一步
2022 CCF CSP-S 第一轮(提高级 C++)
CSPS-2022-R1
试卷解析总览,可直接查看每题答案与解析。
第 1 题(单选题,2 分)
在 Linux 系统终端中,用于切换工作目录的命令为()。
正确答案B
解析详情
cd 表示 change directory,执行 cd work 后,当前工作目录切换到 work,后续相对路径也从新目录解释。
ls 只列出内容,cp 用于复制;要改变当前工作目录应选 B。
第 2 题(单选题,2 分)
你同时用 time 命令和秒表为某个程序在单核 CPU 的运行计时。假如 time 命令的输出如下:
real 0m30.721s
user 0m24.579s
sys 0m6.123s
以下最接近秒表计时的时长为()。
正确答案A
解析详情
秒表从程序启动计时到程序结束,测的是经过的实际时间,对应 real=30.721 秒,最接近 30 秒,选 A。
user=24.579 秒是用户态 CPU 时间,sys=6.123 秒是内核态 CPU 时间;它们不包含所有等待时间,不能用其中一项代替秒表读数。
第 3 题(单选题,2 分)
若元素 a、b、c、d、e、f 依次进栈,允许进栈、退栈操作交替进行,但不允许连续三次退栈操作,则不可能得到的出栈序列是()。
正确答案D
解析详情
若要输出序列 afedcb,先把 a 入栈并立即弹出;随后要让 f 成为栈顶,必须依次压入 b、c、d、e、f。
此时待输入元素已耗尽,要依次输出 f,e,d,c,b,只能连续出栈 5 次,中间再无新元素可入栈打断,违反不能连续三次退栈。
因此 D 不可能;这并非普通栈序本身非法,而是额外操作限制排除了它。
第 4 题(单选题,2 分)
考虑对 n 个数进行排序,以下最坏时间复杂度低于 的排序方法是()。
正确答案C
解析详情
归并排序把数据反复二分,共有约 层;同一层的合并区间互不重叠,处理总量为 n。
所以最坏时间为 ,比平方级低,选 C。插入、冒泡以及未加保护的快速排序,都存在 的输入。
第 5 题(单选题,2 分)
假设在基数排序过程中,受宇宙射线的影响,某项数据异变为一个完全不同的值。请问排序算法结束后,可能出现的最坏情况是()。
正确答案A
解析详情
基数排序的每一趟按当前位进行稳定分配:若两个未受影响元素的当前位相同,仍保持上一趟的相对次序。
异常元素即使改变数值和所处桶,也不会改变其他两个正常元素的数位比较结果。忽略它之后,各趟对正常元素仍组成同样的稳定排序过程。
因此移除这一个异常元素后,其余数据仍有序,最坏也只会有它自身的位置不正确,选 A。
第 6 题(单选题,2 分)
计算机系统用小端(Little Endian)和大端(Big Endian)来描述多字节数据的存储地址顺序模式,其中小端表示将低位字节数据存储在低地址的模式、大端表示将高位字节数据存储在低地址的模式。在小端模式的系统和大端模式的系统分别编译和运行以下 C++代码段表示的程序,将分别输出什么结果?( )
unsigned x = 0xDEADBEEF;
unsigned char *p = (unsigned char *)&x;
printf("%X", *p);正确答案B
解析详情
将 0xDEADBEEF 按字节分为 DE、AD、BE、EF。p 指向变量首地址,即最低内存地址,并且只读取一个字节。
小端的地址从低到高存 EF、BE、AD、DE,故读到 EF;大端存 DE、AD、BE、EF,故读到 DE。
%X 用十六进制输出该字节,因此分别为 EF、DE,选 B。
第 7 题(单选题,2 分)
一个深度为5(根结点深度为1)的完全3叉树,按前序遍历的顺序给结点从1开始编号,
则第 100 号结点的父结点是第()号。
正确答案C
解析详情
按完整各层的三叉树模型,高为 4 的子树含 个结点。根编号 1,三棵子树的前序编号段分别为 2…41、42…81、82…121。
100 落在以 82 为根的第三棵子树,其中每个孩子子树有 13 个结点,编号段为 83…95、96…108、109…121。
再进入根 96 的子树,其第一个孩子编号 97,紧随其后的三个叶子为 98、99、100。故父结点为 97,选 C。
这里沿用答案把“完全3叉树”理解为各层全满的模型;若按末层可不满、从左向右填充的通常完全树定义,仅给深度无法唯一确定。例如 101 个结点的完全三叉树,前序第 100 个结点的父编号为 98。应区分原题的术语口径与一般定义。
第 8 题(单选题,2 分)
强连通图的性质不包括():
正确答案B
解析详情
强连通要求任意两点 u、v 都有 u 到 v 以及 v 到 u 的有向路径,不要求路径长度为 1。
例如有向环 1→2→3→4→1 是强连通图,但 1 和 3 之间没有直接边。它足以否定 B。
其余关于相邻边的描述按本题通常的非单点图口径理解,核心区别是“路径”与“直接边”。
第 9 题(单选题,2 分)
每个顶点度数均为2的无向图称为“2正规图”。由编号为从1到n的顶点构成的所有2正规图,其中包含欧拉回路的不同2正规图的数量为()。
正确答案D
解析详情
每点度数为 2 的简单无向图由若干个环组成。若要有遍历所有边的欧拉回路,这些环不能分属不同连通分量,所以必须是经过全部 n 点的一个环。
固定顶点 1 作为书写起点,剩余顶点有 种排列;把顺时针与逆时针视作同一个无向环,再除以 2。
因此在通常 n≥3 的环模型下,方案数为 ,选 D。
第 10 题(单选题,2 分)
共有 8 人选修了程序设计课程,期末大作业要求由 2 人组成的团队完成。假设不区分每个团队内 2 人的角色和作用,请问共有多少种可能的组队方案。()
正确答案A
解析详情
按选项所考查的“一支两人团队”计数,从 8 人中先选第一人有 8 种,再选另一人有 7 种。
同一对成员交换选择顺序仍是同一支队伍,故除以 2,得到 ,选 A。
若问题改成把全部 8 人同时分成 4 支无标号队伍,则会是 105 种;那是不同的计数任务。
第 11 题(单选题,2 分)
小明希望选到形如“省 A·LLDDD”的车牌号。车牌号在“·”之前的内容固定不变;后面的 5 位号码中,前 2 位必须是大写英文字母,后 3 位必须是阿拉伯数字(ℱ代表 A 至 Z,ℱ表示 0 至 9,两个ℱ和三个ℱ之间可能相同也可能不同)。请问总共有多少个可供选择的车牌
号。()
正确答案C
解析详情
固定的“省 A·”不产生选择。后面两位字母各可选 A 至 Z 共 26 种,三位数字各可选 0 至 9 共 10 种。
不同位置独立且允许重复,由乘法原理得到 ,选 C。数字部分是编号,不受三位整数首位不能为 0 的限制。
第 12 题(单选题,2 分)
给定地址区间为 0~9 的哈希表,哈希函数为 ,采用线性探查的冲突解决策略(对于出现冲突情况,会往后探查第一个空的地址存储;若地址 9 冲突了则从地址 0 重新开始探查)。哈希表初始为空表,依次存储(71, 23, 73, 99, 44, 79, 89)后,请问 89 存储在哈希表哪个地址中。()
正确答案D
解析详情
逐项插入:71→1,23→3;73 初始位置 3 已占,顺延到 4;99→9;44 的位置 4 已占,顺延到 5;79 从 9 回绕到空位 0。
最后插入 89 时,从余数位置 9 开始,9、0、1 都已占用,位置 2 为空,因此落在 2,选 D。
左右滑动表格,可查看完整内容。
| 插入值 | 初始地址 | 探测经过 | 最终地址 |
|---|---|---|---|
| 71 | 1 | 1 | 1 |
| 23 | 3 | 3 | 3 |
| 73 | 3 | 3 → 4 | 4 |
| 99 | 9 | 9 | 9 |
| 44 | 4 | 4 → 5 | 5 |
| 79 | 9 | 9 → 0 | 0 |
| 89 | 9 | 9 → 0 → 1 → 2 | 2 |
地址冲突后逐格后移,从 9 继续时回绕到 0。
第 13 题(单选题,2 分)
对于给定的 n,分析以下代码段对应的时间复杂度,其中最为准确的时间复杂度为()。
13. 对于给定的 n,分析以下代码段对应的时间复杂度,其中最为准确的时间复杂度为()。
int i, j, k = 0;
for (i = 0; i < n; i++) {
for (j = 0; j < n; j *= 2) {
k = k + n / 2;
}
}正确答案B
解析详情
原 PDF 的内层更新语句是 j*=2,但初值仍写为 j=0。n>0 时,0 每次乘 2 仍为 0,j<n 永远成立;该原卷代码不能作为一个正常结束的排序或计数循环分析。
给定答案 B 对应的是把初值改为 1 后的命题意图:内层依次取 1,2,4,…,执行 次,外层 n 次,得到 。
本题保留原卷及其答案,同时明确区分:B 是勘误后的复杂度,不是原样程序的时间复杂度;也不能用一次运行假定它会在整数溢出后正常结束。
第 14 题(单选题,2 分)
以比较为基本运算,在 n 个数的数组中找最大的数,在最坏情况下至少要做()次运算。
正确答案B
解析详情
每个不是最大值的元素,至少要在一次比较中输给别的元素,才能确定排除它。共有 n−1 个这样的元素,而一次比较最多新增一个被排除者。
所以至少要比较 n−1 次;从第一个元素作候选,依次与其他元素比较,恰能达到该下界。因此选 B。
第 15 题(单选题,2 分)
ack 函数在输入参数“ ”时的返回值为()。
unsigned ack(unsigned m, unsigned n) {
if (m == 0) return n + 1;
if (n == 0) return ack(m - 1, 1);
return ack(m - 1, ack(m, n - 1));
}正确答案B
解析详情
当 m=1 时,ack(1,0)=ack(0,1)=2;每增加一次 n,调用 ack(0,…) 把结果加 1,所以 ack(1,n)=n+2。
于是 ack(2,0)=ack(1,1)=3,ack(2,1)=ack(1,3)=5,ack(2,2)=ack(1,5)=7。
因此选 B;第三个分支必须先算内层,再把结果作为外层第二个参数。
二、阅读程序(1)
1 | #include <iostream>
2 | #include <string>
3 | #include <vector>
4 |
5 | using namespace std;
6 |
7 | int f(const string &s, const string &t)
8 | {
9 | int n = s.length(), m = t.length();
10 |
11 | vector<int> shift(128, m + 1);
12 |
13 | int i, j;
14 |
15 | for (j = 0; j < m; j++)
16 | shift[t[j]] = m - j;
17 |
18 | for (i = 0; i <= n - m; i += shift[s[i + m]]) {
19 | j = 0;
20 | while (j < m && s[i + j] == t[j]) j++;
21 | if (j == m) return i;
22 | }
23 |
24 | return -1;
25 | }
26 |
27 | int main()
28 | {
29 | string a, b;
30 | cin >> a >> b;
31 | cout << f(a, b) << endl;
32 | return 0;
33 | }假设输入字符串由 ASCII 可见字符组成,完成下面的判断题和单选题:
第 16 题(判断题,1 分)
当输入为“abcde fg”时,输出为-1。
正确答案正确
解析详情
输入运算符把空格两侧分别读入 s=abcde、t=fg,模式长度 m=2。shift 中 f 为 2、g 为 1,其他字符默认 3。
候选起点 i=0 时首字符失配,窗口后字符 c 的位移为 3,于是跳到 i=3;这里 d 与 f 仍不等,再跳出合法起点范围。
没有完整匹配,返回 −1,判断正确。
第 17 题(判断题,1.5 分)
当输入为 “abbababbbab abab” 时,输出为 4。()
正确答案错误
解析详情
主串 abbababbbab 的零基下标 3、4、5、6 依次为 a、b、a、b,组成第一次出现的 abab。
程序返回起点下标 i,而不是从 1 开始数的字符位置,因此输出 3。题目说输出 4,判断错误。
第 18 题(判断题,1.5 分)
当输入为 “GoodLuckCsp2022 22” 时,第 20 行的 “j++” 语句执行次数为 2。
正确答案正确
解析详情
模式“22”长度为 2,字符 2 最后一次出现于下标 1,因此 shift[2]=1;其他字符的默认位移为 3。
实际候选起点依次为 0、3、6、9、12。前四次首字符就失配,不执行 j++;i=12 时末尾两个 2 都匹配,使 j 从 0 增至 2。
因此 j++ 总计 2 次,判断正确。
第 19 题(单选题,3 分)
该算法最坏情况下的时间复杂度为()。
正确答案D
解析详情
每次尝试匹配,while 最多成功或检查约 m 个字符。跳转表虽能跳过一些起点,但最坏仍有与 n 同阶的候选窗口。
例如主串全为 a,模式为若干个 a 后接 b,每次先比较长 a 前缀再失败,窗口后字符 a 只让起点移动 2 位。
当 n 远大于 m 时,需要 个窗口,每个 次工作,最坏为 ,选 D。
第 20 题(单选题,3 分)
f(a, b)与下列()语句的功能最类似。
正确答案A
解析详情
函数依次检查可能的匹配起点,首次完整匹配就 return i;若全部失败,则返回 −1。
a.find(b) 同样返回 b 在 a 中第一次出现的位置,因此功能最接近 A。严格说 find 的未找到标记是 string::npos,而此函数用 int 的 −1;题目比较的是查找功能。
第 21 题(单选题,3 分)
当输入为“baaabaaabaaabaaaa aaaa”,第20行的“j++”语句执行次数为()。
正确答案B
解析详情
模式 aaaa 的 shift[a]=1;其他字符默认跳 5。程序只尝试起点 0、5、6、7、8、13,不会逐个起点检查。
这些窗口分别成功匹配 0、3、2、1、0、4 个字符;每成功一个字符才执行一次 j++,失败的那次比较不计。
合计 ,选 B。
左右滑动表格,可查看完整内容。
| 起点 i | 匹配成功字符数 | 累计次数 |
|---|---|---|
| 0 | 0 | 0 |
| 5 | 3 | 3 |
| 6 | 2 | 5 |
| 7 | 1 | 6 |
| 8 | 0 | 6 |
| 13 | 4 | 10 |
起点为零基下标,只记录实际访问的窗口;成功匹配一个字符才执行一次 j++。
二、阅读程序(2)
1 | #include <iostream>
2 |
3 | using namespace std;
4 |
5 | const int MAXN = 105;
6 |
7 | int n, m, k, val[MAXN];
8 | int temp[MAXN], cnt[MAXN];
9 |
10 | void init()
11 | {
12 | cin >> n >> k;
13 | for (int i = 0; i < n; i++) cin >> val[i];
14 | int maximum = val[0];
15 | for (int i = 1; i < n; i++)
16 | if (val[i] > maximum) maximum = val[i];
17 | m = 1;
18 | while (maximum >= k) {
19 | maximum /= k;
20 | m++;
21 | }
22 | }
23 |
24 | void solve()
25 | {
26 | int base = 1;
27 | for (int i = 0; i < m; i++) {
28 | for (int j = 0; j < k; j++) cnt[j] = 0;
29 | for (int j = 0; j < n; j++) cnt[val[j] / base % k]++;
30 | for (int j = 1; j < k; j++) cnt[j] += cnt[j - 1];
31 | for (int j = n - 1; j >= 0; j--) {
32 | temp[cnt[val[j] / base % k] - 1] = val[j];
33 | cnt[val[j] / base % k]--;
34 | }
35 | for (int j = 0; j < n; j++) val[j] = temp[j];
36 | base *= k;
37 | }
38 | }
39 |
40 | int main()
41 | {
42 | init();
43 | solve();
44 | for (int i = 0; i < n; i++) cout << val[i] << ' ';
45 | cout << endl;
46 | return 0;
47 | }假设输入的 n 为不大于 100 的正整数,k 为不小于 2 且不大于 100 的正整数,val[i] 在 int 表示范围内,完成下面的判断题和单选题:
第 22 题(判断题,1.5 分)
这是一个不稳定的排序算法。()
正确答案错误
解析详情
每一趟利用计数前缀和确定数位相同元素的区间,再从原数组末尾向前放置,放一个就把该桶的末位置减一。
若同位的两个元素 u 原本在 v 前面,则 v 先放到较后位置,u 后放到较前位置,相对顺序保持不变。
所以单趟计数排序稳定,各趟组成的基数排序也稳定,“不稳定”这一断言错误。此处按算法所需的非负键值与不溢出前提分析。
第 23 题(判断题,1.5 分)
该算法的空间复杂度仅与 n 有关。()
正确答案错误
解析详情
一趟排序至少需要 n 个位置保存 temp,此外还要有 k 个桶来统计每个数位出现次数。
按随输入规模分配空间的算法模型,额外空间为 ,会受到基数 k 的影响,所以断言错误。原代码用固定 MAXN 数组实现,不应把固定容量写法当成可扩展算法的空间结论。
第 24 题(判断题,1.5 分)
该算法的时间复杂度为 。()
正确答案正确
解析详情
一趟包括清空 k 个桶、统计 n 个元素、累加 k 个桶的前缀和、回填 n 个元素及复制 n 个元素,总量为 。
这样的过程执行 m 次,对应最大数的 k 进制位数,因此排序阶段为 ,判断正确;这里采用桶下标有效且整数计算不溢出的正常排序前提。
第 25 题(单选题,3 分)
当输入为 “5 3 98 26 91 37 46” 时,程序第一次执行到第 36 行,val[] 数组的内容依次为()。
正确答案D
解析详情
第一趟 base=1,数位取 val[j]%3。98、26 的余数为 2,91、37、46 的余数为 1,没有余数 0 的数。
稳定计数排序把余数 1 的组先放,组内保持 91,37,46;余数 2 的组保持 98,26。
第一次执行到第 36 行时已经复制回 val,得到 91 37 46 98 26,选 D。
左右滑动表格,可查看完整内容。
| 桶编号 | 原顺序进入桶的元素 | 回填位置(零基) |
|---|---|---|
| 0 | 无 | 无 |
| 1 | 91、37、46 | 0、1、2 |
| 2 | 98、26 | 3、4 |
按个位分桶;相同余数的元素保留输入顺序。
第 26 题(单选题,3 分)
若 val[i] 的最大值为 100,k 取()时算法运算次数最少。
正确答案C
解析详情
给定答案 C 对应比较趟数:最大值 100 分别需要 7 位二进制、5 位三进制、3 位十进制,因此 k=10 的趟数最少。
但题干问完整“运算次数”,每趟还要处理 k 个桶,不能只数趟数。按代码各循环体执行次数粗计,每趟为 k+n+(k−1)+n+n=3n+2k−1。
例如 n=1 时,k=2、3、10 分别得到 42、40、66 次这一口径下的循环体执行量,10 并非最少。因此缺少 n 与具体成本口径时,不能严格证明 C 为唯一最优;此处保留原答案并说明其简化假设。
第 27 题(单选题,3 分)
当输入的 k 比 val[i] 的最大值还大时,该算法退化为()算法。
正确答案C
解析详情
当所有非负键值都小于 k 时,init 中 maximum>=k 一开始为假,所以 m=1。
唯一的一趟中 base=1,val[j]/base%k 等于 val[j] 本身,计数桶直接按完整数值统计,再按前缀和回填。
这正是计数排序,选 C,无需再处理高位。
二、阅读程序(3)
1 | #include <iostream>
2 | #include <algorithm>
3 |
4 | using namespace std;
5 |
6 | const int MAXL = 1000;
7 |
8 | int n, k, ans[MAXL];
9 |
10 | int main(void)
11 | {
12 | cin >> n >> k;
13 | if (!n) cout << 0 << endl;
14 | else
15 | {
16 | int m = 0;
17 | while (n)
18 | {
19 | ans[m++] = (n % (-k) + k) % k;
20 | n = (ans[m - 1] - n) / k;
21 | }
22 | for (int i = m - 1; i >= 0; i--)
23 | cout << char(ans[i] >= 10 ?
24 | ans[i] + 'A' - 10 :
25 | ans[i] + '0');
26 | cout << endl;
27 | }
28 | return 0;
29 | }假设输入的 n 在 int 范围内,k 为不小于 2 且不大于 36 的正整数,完成下面的判断题和单选题:
第 28 题(判断题,1.5 分)
该算法的时间复杂度为 。 ()
正确答案正确
解析详情
每轮选取余数 r∈[0,k−1],并更新商 ,保持 。
于是商的绝对值满足 ,经过与对数同阶的次数就降至常数范围,算法生成一位后再处理余下商。
按非零 n 的通常规模写法,时间为 ,题目意指对数量级,判断正确;负数不能直接写实数对数 。
第 29 题(判断题,1.5 分)
删除第 23 行的强制类型转换,程序的行为不变。 ()
正确答案错误
解析详情
即使加上字符常量,ans[i]+'0' 和 ans[i]+'A'−10 的算术结果也是整数。char(...) 把它转回字符,使 cout 输出字形。
例如输入 1 2 时,有转换输出字符“1”;删除转换则输出字符编码的十进制值 49。因此程序行为改变,判断错误。
第 30 题(判断题,1.5 分)
除非输入的 n 为 0,否则程序输出的字符数为 。 ()
正确答案正确
解析详情
每轮写入一个负 k 进制数位,最终 m 就是位数,倒序循环每次输出一个字符。
商的绝对值大致每轮缩小 k 倍,所以位数为 ,判断正确。
末尾的加 1 覆盖 n=1 等对数为 0 的小规模情形;n=0 则由单独分支输出一个 0。
第 31 题(单选题,3 分)
当输入为 “100 7” 时,输出为()。
正确答案A
解析详情
当前 n=100 时,规范余数为 2,新的 n=(2−100)/7=−14。
n=−14 时余数为 0,新的 n=2;再取余数 2,新的 n=0,结束。
得到低位到高位 [2,0,2],倒序输出 202,且 ,选 A。
第 32 题(单选题,3 分)
当输入为 “-255 8” 时,输出为 “( )”。
正确答案B
解析详情
逐轮规范余数与新商为:−255→余数 1、商 32;32→余数 0、商 −4;−4→余数 4、商 1;1→余数 1、商 0。
低位到高位为 [1,0,4,1],倒序得到 1401。
验证 ,选 B。
第 33 题(单选题,3 分)
当输入为 “1000000 19” 时,输出为 “( )。
正确答案B
解析详情
每轮的“当前 n→数位、新商”为:1000000→11,−52631;−52631→18,2771;2771→16,−145;−145→7,8;8→8,0。
数位 11、18、16 分别编码为 B、I、G,低位到高位为 B,I,G,7,8,倒序输出 87GIB,选 B。
代回负十九进制位权,,与输入一致。
左右滑动表格,可查看完整内容。
| 轮次 | 当前数 | 规范余数 | 新商 | 字符 |
|---|---|---|---|---|
| 1 | 1000000 | 11 | −52631 | B |
| 2 | −52631 | 18 | 2771 | I |
| 3 | 2771 | 16 | −145 | G |
| 4 | −145 | 7 | 8 | 7 |
| 5 | 8 | 8 | 0 | 8 |
每行保持 当前数 = (−19) × 新商 + 余数;将余数字符倒序得到 87GIB。
三、完善程序(1)
(1)(归并第 k 小)已知两个长度均为 n 的有序数组 a1 和 a2(均为递增序,但不保证严格单调递增),并且给定正整数 k( ),求数组 a1 和 a2 归并排序后的数组里第 k 小的数值。
试补全程序。
#include <bits/stdc++.h>
using namespace std;
int solve(int *a1, int *a2, int n, int k) {
int left1 = 0, right1 = n - 1;
int left2 = 0, right2 = n - 1;
while (left1 <= right1 && left2 <= right2) {
int m1 = (left1 + right1) >> 1;
int m2 = (left2 + right2) >> 1;
int cnt = ①;
if (②) {
if (cnt < k) left1 = m1 + 1;
else right2 = m2 - 1;
} else {
if (cnt < k) left2 = m2 + 1;
else right1 = m1 - 1;
}
}
if (③) {
if (left1 == 0) {
return a2[k - 1];
} else {
int x = a1[left1 - 1], ④;
return std::max(x, y);
}
} else {
if (left2 == 0) {
return a1[k - 1];
} else {
int x = a2[left2 - 1], ⑤;
return std::max(x, y);
}
}
}第 34 题(单选题,3 分)
①处应填( )
(m1 + m2) * 2(m1 - 1) + (m2 - 1)m1 + m2(m1 + 1) + (m2 + 1)正确答案C
解析详情
若 a1[m1]≤a2[m2],严格小于 a1[m1] 的元素至多来自第一数组前 m1 项和第二数组前 m2 项,总数至多为 m1+m2。若这个和小于 k,则 a1[m1] 不会大于合并后的第 k 小值,可以把第一侧这个中点及以前的元素纳入左前缀。
反之,若 m1+m2≥k,则 a2[m2] 前面已有足够多的不大于它的候选,可以缩小第二侧右边界。另一分支完全对称,因此①采用零基下标和 m1+m2,选 C。这里比较的是分割位置,不应直接替换成两个前缀长度之和。
需同时注意原卷的边界缺陷:例如 n=1、k=1、a1=[1]、a2=[2],循环后 left1=1,收尾访问 a2[k−left1−1]=a2[−1]。本题答案体现原卷分割意图,不能据此断言代码对所有输入都安全。
第 35 题(单选题,3 分)
②处应填( )
a1[m1] == a2[m2]a1[m1] <= a2[m2]a1[m1] >= a2[m2]a1[m1] != a2[m2]正确答案B
解析详情
②为真时,cnt<k 会向右移动 left1;否则向左移动 right2。因此这一分支应对应 a1 的中点不大于 a2 的中点。
填 a1[m1]<=a2[m2] 后,两支恰好按较小/较大中点作对称处理,选 B;只比较相等或不等不能确定该移动哪一侧。
整体代码的空前缀访问风险见第 34、37 题,局部比较方向正确并不能消除它。
第 36 题(单选题,3 分)
③处应填( )
left1 == right1left1 < right1left1 > right1left1 != right1正确答案C
解析详情
循环前提是两边候选区间都非空。对包含端点的闭区间 [left1,right1],只有 left1>right1 才表示它已经为空。
因此③选 C,用来区分哪侧搜索先结束。left1==right1 时还剩一个候选元素,不应判为空。
第 37 题(单选题,3 分)
④处应填( )
y = a1[k - left2 - 1]y = a1[k - left2]y = a2[k - left1 - 1]y = a2[k - left1]正确答案C
解析详情
按分割的计数意图,第一侧前缀已有 left1 个元素,第二侧应补 k−left1 个。第二侧最后一个元素的零基下标为 k−left1−1,所以④选 C。
若两侧前缀都非空,取 a1[left1−1] 与 a2[k−left1−1] 的较大值,就是合并前缀的末项。
但是 k−left1=0 时第二侧为空,这个下标为 −1;原代码只检查 left1==0,没有检查这一对称边界。因此该表达式有边界前提,不能直接作为可复用的完整第 k 小模板。
第 38 题(单选题,3 分)
⑤处应填( )
y = a1[k - left2 - 1]y = a1[k - left2]y = a2[k - left1 - 1]y = a2[k - left1]正确答案A
解析详情
这一分支与上一分支对称:第二侧前缀有 left2 个元素,第一侧需要 k−left2 个,对应末元素下标 k−left2−1。
因此⑤填 y=a1[k−left2−1],选 A,再与 a2[left2−1] 取最大值。
同样必须警惕 k−left2=0 时的空前缀;原卷未提供该边界保护,解析说明的是选项的分割含义。
三、完善程序(2)
(2)(容器分水)有两个容器,容器 1 的容量为为 a 升,容器 2 的容量为 b 升;同时允许下列的三种操作,分别为:
1)FILL(i):用水龙头将容器 i(i∈{1,2})灌满水;
2)DROP(i):将容器 i 的水倒进下水道;
3)POUR(i,j):将容器 i 的水倒进容器 j(完成此操作后,要么容器 j 被灌满,要么容器 i 被清空)。
求只使用上述的两个容器和三种操作,获得恰好 c 升水的最少操作数和操作序列。上述 a、b、c 均为不超过 100 的正整数,且 。
试补全程序。
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int f[N][N];
int ans;
int a, b, c;
int init;
int dfs(int x, int y) {
if (f[x][y] != init)
return f[x][y];
if (x == c || y == c)
return f[x][y] = 0;
f[x][y] = init - 1;
f[x][y] = min(f[x][y], dfs(a, y) + 1);
f[x][y] = min(f[x][y], dfs(x, b) + 1);
f[x][y] = min(f[x][y], dfs(0, y) + 1);
f[x][y] = min(f[x][y], dfs(x, 0) + 1);
int t = min(a - x, y);
f[x][y] = min(f[x][y], ①);
t = min(x, b - y);
f[x][y] = min(f[x][y], ②);
return f[x][y];
}
void go(int x, int y) {
if (③)
return;
if (f[x][y] == dfs(a, y) + 1) {
cout << "FILL(1)" << endl;
go(a, y);
} else if (f[x][y] == dfs(x, b) + 1) {
cout << "FILL(2)" << endl;
go(x, b);
} else if (f[x][y] == dfs(0, y) + 1) {
cout << "DROP(1)" << endl;
go(0, y);
} else if (f[x][y] == dfs(x, 0) + 1) {
cout << "DROP(2)" << endl;
go(x, 0);
} else {
int t = min(a - x, y);
if (f[x][y] == ④) {
cout << "POUR(2,1)" << endl;
go(x + t, y - t);
} else {
t = min(x, b - y);
if (f[x][y] == ⑤) {
cout << "POUR(1,2)" << endl;
go(x - t, y + t);
} else
assert(0);
}
}
}
int main() {
cin >> a >> b >> c;
ans = 1 << 30;
memset(f, 127, sizeof f);
init = **f;
if ((ans = dfs(0, 0)) == init - 1)
cout << "impossible";
else {
cout << ans << endl;
go(0, 0);
}
}第 39 题(单选题,3 分)
①处应填()
dfs(x + t, y - t) + 1dfs(x + t, y - t) - 1dfs(x - t, y + t) + 1dfs(x - t, y + t) - 1正确答案A
解析详情
当前容器水量为 (x,y),向容量 a 的容器 1 倒水,最多还能装 a−x,同时容器 2 只有 y 升可倒,因此 t=min(a−x,y)。
新状态为 (x+t,y−t),当前倒水消耗一步,所以①填 dfs(x+t,y−t)+1,选 A。
例如 a=5、x=2、y=4 时 t=3,新状态为 (5,1),不能把目标容器装到超过容量。
第 40 题(单选题,3 分)
②处应填()
dfs(x + t, y - t) + 1dfs(x + t, y - t) - 1dfs(x - t, y + t) + 1dfs(x - t, y + t) - 1正确答案C
解析详情
向容器 2 倒水时,可倒量同时受容器 1 的现存水量 x 和容器 2 的剩余容量 b−y 限制,故 t=min(x,b−y)。
倒完后容器 1 减少 t、容器 2 增加 t,新状态 (x−t,y+t),再加这一步操作,②选 C。
第 41 题(单选题,3 分)
③处应填()
x == c || y == cx == c && y == cx >= c || y >= cx >= c && y >= c正确答案A
解析详情
题目只要求任意一个容器中恰好有 c 升。到达 x=c 或 y=c 时已经成功,后面不需要继续输出操作。
所以③填 x==c || y==c,选 A。&& 会错误地要求两容器同时达到 c,>= 则会把多于 c 的水也当作完成。
第 42 题(单选题,3 分)
④处应填()
dfs(x + t, y - t) + 1dfs(x + t, y - t) - 1dfs(x - t, y + t) + 1dfs(x - t, y + t) - 1正确答案A
解析详情
还原路径时,若记录的当前代价恰等于后继代价加 1,就可以输出这次操作并递归到后继。
本分支打印 POUR(2,1),后继明确为 (x+t,y−t),所以④必须与 dfs 中相应转移一致,填 dfs(x+t,y−t)+1,选 A。
这个等式解释的是路径还原规则;对于带环状态图,不能仅凭使用了记忆化 DFS 就一般性保证所有 f 值均为最短距离。
第 43 题(单选题,3 分)
⑤处应填()
dfs(x + t, y - t) + 1dfs(x + t, y - t) - 1dfs(x - t, y + t) + 1dfs(x - t, y + t) - 1正确答案C
解析详情
最后一个分支对应 POUR(1,2),后继为 (x−t,y+t),故需比较 f[x][y] 与 dfs(x−t,y+t)+1,⑤选 C。
选择后就按代码进入该后继,每次把剩余操作数减一;方向和加一成本都必须与搜索转移保持一致。
若用它实现独立的最少步数求解,应另外证明带环图上的距离计算正确,通常可用 BFS 作可靠对照。