【Coel.解题报告】【没事找事】CSP-S1 2022 真题解析
昨天刚考完 CSP-S1,反正没什么想做的(最近好颓废…),来复盘一下。
本次比赛评价(转载):
CSP-S1 是由 CCF 自主研发的一款全新开放世界冒险游戏。游戏发生在一个被称作「基数排序」的幻想世界,在这里,被神选中的人将被授予「time计时」,导引宇宙射线。你将扮演一位名为「计数排序」的神秘角色,在自由的旅行中邂逅性格各异、能力独特的大作业,和他们一起击败强敌,找回失散的
% (-k)
——同时,逐步发掘「for(int j = 0; j < n; j *= 2)
」的真相。
以下答案来自有道小图灵,解析为个人见解,如果有错欢迎指出w
update on 2022.9.20:修正了部分题面,完善了 \(\LaTeX\)。
单选题
1.在 Linux 系统终端中,用于切换工作目录的命令为( )。
A. ls B. cd C. cp D. all
答案:B
解析:第一题,很水。ls 为列出目录下的文件夹和文件,cd 为切换目录,cp 为复制,all 只是一个操作值。
2.你同时用 time 命令和秒表为某个程序在单核 CPU 的运行计时。假如 time 命令的输出如下:
real 0m30.721s
user 0m24.579s
sys 0m6.123s
以下最接近秒表计时的时长为( )。
A.30s B. 24s C.18s D.6s
答案:A
解析:第二题就是一个下马威……这几年确实没考过 time 命令,弄得措手不及。
real 表示实际使用时间,user 表示用户态使用时间,sys 表示内核态使用时间,故为 A。顺带一提,real 远大于剩下两个是因为把大量的文件读入时间算了进去。
3.若元素 \(a、b、c、d、e、f\) 依次进栈, 允许进栈、退栈操作交替进行, 但不允许连续三次退栈操作,则不可能得到的出栈序列是( )。
A.\(dcebfa\) B.\(cbdaef\) C.\(bcaefd\) D.\(afedcb\)
答案:D
解析:手模一遍进出栈的操作就很容易得答案了。另外 D 选项必须进行 5 次退栈操作……
4.考虑对 \(n\) 个数进行排序,以下最坏时间复杂度低于 \(O(n^2)\) 的排序方法是( )。
A.插入排序 B.冒泡排序 C.归并排序 D.快速排序
答案:C
解析:插入和冒泡的时间复杂度都为 \(O(n^2)\)。快速排序虽然平均时间复杂度为 \(O(n\log n)\),但在极端数据下会退化成 \(O(n^2)\),而归并排序在极端情况下时间复杂度也为 \(O(n\log n)\)。
5.假设在基数排序过程中,受宇宙射线的影响,某项数据异变为一个完全不同的值。请问排序算法结束后,可能出现的最坏情况是( )。
A. 移除受影响的数据后,最终序列是有序序列
B. 移除受影响的数据后,最终序列是前后两个有序的子序列
C. 移除受影响的数据后,最终序列是一个有序的子序列和一个基本无序的子序列
D. 移除受影响的数据后,最终序列基本无序
答案:A
解析:基数排序的本质是对每一位(按给定的 \(k\) 值确定进制,具体可以参考后面的阅读程序)计算排名,如果某个数字改变了,那么只有这一个数字的排名会受到影响,其他数字不变。
6.计算机系统用小端(Little Endian)和大端(Big Endian)来描述多字节数据的存储地址顺序模式,其中小端表示将低位字节数据存储在低地址的模式、大端表示将高位字节数据存储在低地址的模式。在小端模式的系统和大端模式的系统分别编译和运行以下 C++ 代码段表示的程序,将分别输出什么结果?( )
unsigned x = 0xDEADBEEF;
unsigned char *p = (unsigned char *)&x;
printf("%X", *p);
A. EF、EF
B. EF、DE
C. DE、EF
D. DE、DE
答案:B
解析:熟悉各种指针操作就很容易得到答案,可惜没啥人用,实际写题也没必要用。
unsigned char
会取八位二进制数字,也就是两位十六进制。那么小端就是取 \(x\) 的最后两位,大端就是取最前两位。
7.一个深度为 \(5\)(根结点深度为 \(1\))的完全 \(3\) 叉树,按前序遍历的顺序给结点从 \(1\) 开始编号,则第 \(100\) 号结点的父结点是第( )号。
A. \(95\) B. \(96\) C. \(97\) D. \(98\)
答案:C
解析:按照前序遍历的原则手模就行了,不多解释。注意三叉树的前序遍历顺序是根-左-中-右。
8.强连通图的性质不包括( ):
A. 每个顶点的度数至少为 \(1\)
B. 任意两个顶点之间都有边相连
C. 任意两个顶点之间都有路径相连
D. 每个顶点至少都连有一条边
答案:B
解析:学过连通性问题那一块的知识就可以轻松答出来了。
对于一张有向图,如果这张图任意两个节点都可以到达,那么这张图是强连通图。根据这个定义,ACD 都是对的。
对于 B 选项,我们可以构造出一个反例:画一个四边形,取顶点为结点,此时这是一个强连通图,但对角线的点没有边相连。
9.每个顶点度数均为 \(2\) 的无向图称为“\(2\) 正规图”。由编号为从 \(1\) 到 \(n\) 的顶点构成的所有 \(2\) 正规图,其中包含欧拉回路的不同 \(2\) 正规图的数量为( )。
A. \(n!\)
B. \((n-1)!\)
C. \(n!/2\)
D. \((n-1)!/2\)
答案:D
解析:根据欧拉回路的判定定理(每个点的度数必定为偶数),可知 \(2\) 正规图一定有欧拉回路。
然后就可以构造特殊解了,画一个三角形,取顶点为结点,此时 \(2\) 正规图有且只有一个,符合的答案就是 D。
10.共有 \(8\) 人选修了程序设计课程,期末大作业要求由 \(2\) 人组成的团队完成。假设不区分每个
团队内 \(2\) 人的角色和作用,请问共有多少种可能的组队方案。( )
A.\(28\) B.\(32\) C.\(56\) D.\(64\)
答案:A
解析:很简单的排列组合,答案就是 \(C^2_8\)。
11.小明希望选到形如“省 A ·\(\mathfrak{LL}DDD\)”的车牌号。车牌号在“·”之前的内容固定不变; 后面 的 5 位号码中, 前 2 位必须是大写英文字母, 后 3 位必须是阿拉伯数字(\(\mathfrak L\) 代表 A 至 Z,\(D\) 表示 0 至 9,两个 \(\mathfrak L\) 和三个 \(D\) 之间可能相同也可能不同)。请问总共有多少个可供选择的车牌号。( )
A.\(20280\) B.\(5200\) C.\(67600\) D.\(1757600\)
答案:C
解析:也是很简单的排列组合,答案就是 \(26^2 \times 10^3\)。
12.给定地址区间为 \(0\sim 9\) 的哈希表,哈希函数为 h(x) = x % 10
,采用线性探查的冲突解决 策略(对于出现冲突情况,会往后探查第一个空的地址存储;若地址 \(9\) 冲突了则从地址 \(0\) 重新开始探查)。哈希表初始为空表,依次存储(\(71, 23, 73, 99, 44, 79, 89\))后,请问 \(89\) 存储在哈希表哪个地址中。( )
A.\(9\) B.\(0\) C.\(1\) D.\(2\)
答案:D
解析:去年就考过一次哈希冲突,这次也很简单。按照题意模拟即可。
13.对于给定的 \(n\),分析以下代码段对应的时间复杂度,其中最为准确的时间复杂度为( )。
int i, j, k = 0;
for (i = 0; i < n; i++) {
for (j = 1; j < n; j *= 2) {
k = k + n / 2;
}
}
A. \(O(n)\)
B. \(O(n\log n)\)
C. \(O(n\sqrt n)\)
D. \(O(n^2)\)
答案:B
解析:毫不意外地考了时间复杂度分析,不过难度没想象的难(我递推方程呢?!)。
外层会执行 \(n\) 次,内层由于每次 \(j\) 扩大两倍,执行次数是 \(\log_2 n\),所以答案就是 \(O(n\log n)\)。
顺带一提,试卷上 j = 1
写的是 j = 0
,如此显然的错误也能出现在 CCF 的卷子上,真是无语。
14.以比较为基本运算, 在 \(n\) 个数的数组中找最大的数,在最坏情况下至少要做( )次运算。
A.\(n / 2\) B. \(n - 1\) C. \(n\) D. \(n + 1\)
答案:B
解析:假设最大的数在最后面,从头开始找数字,自然就要找 \(n - 1\) 次了。
什么?你问我为什么不是 \(n\)?当然是因为第一个数不用比较啦……
15.ack 函数在输入参数“\((2,2)\)”时的返回值为( )。
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
解析:这个函数实际上是 阿克曼函数,增长速度非常快,感兴趣可以查一下资料。
还是手模出结果,就是有点累人……
注:其实这个函数最好的求解方法是画表格递推。
单选题终于结束啦!来看看阅读程序ww
阅读程序
第一大题
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int f(const string &s, const string &t) {
int n = s.length(), m = t.length();
vector<int> shift(128, m + 1);
int i, j;
for (j = 0; j < m; j++)
shift[t[j]] = m - j;
for (i = 0; i <= n - m; i += shift[s[i + m]]) {
j = 0;
while (j < m && s[i + j] == t[j]) j++;
if (j == m) return i;
}
return -1;
}
int main() {
string a, b;
cin >> a >> b;
cout << f(a, b) << endl;
return 0;
}
假设输入字符串由 ASCII 可见字符组成,完成下面的判断题和单选题:
判断题
16.(1 分)当输入为“abcde fg
”时,输出为 \(-1\)。( )
17.当输入为“abbababbbab abab
”时,输出为 \(4\)。( )
18 当输入为“GoodLuckCsp2022 22
”时,第 20 行的“j++
”语句执行次数为 \(2\)。
( )
单选题
19.该算法最坏情况下的时间复杂度为( )。
A.\(O(n+m)\) B.\(O(n \log m)\) C. \(O(m\log n)\) D. \(O(nm)\)
20.f(a, b)与下列( )语句的功能最类似。
A. a.find(b)
B. a.rfind(b)
C. a.substr(b)
D. a.compare(b)
21.当输入为“baaabaaabaaabaaaa aaaa
”,第 20 行的“j++
”语句执行次数为( )。
A. 9 B. 10 C. 11 D. 12
答案(A 为正确 B 为错误):ABADAB
解析:不难看出这是一个找字符串位置的函数,返回 \(b\) 在 \(a\) 中第一次出现的下标。原理是先对 \(b\) 构造一个用于跳转的 \(shift\) 数组(记录右数第一次出现这个字符的位置,不出现的字符记为 \(b\) 的长度加一),然后对 \(a\) 一位位比较,找不到就利用 \(shift\) 数组跳转。
16 题,显然不存在输出 \(-1\)。
17 题,由于是第一次出现,所以取 \(3\)。
18 题,由于字符串前面不存在字符 \(2\),所以会一直向后跳转直到匹配成功,也就只会自增两次。
19 题,取一个 \(m = 1\) 且完全不匹配的情况就是答案。
20 题,有点考 string 的用法,不过也可以猜到是 A。
21 题,每次找到 \(a\) 都会自增一次,结果就是 \(10\) 次。
第二大题
#include <iostream>
using namespace std;
const int MAXN = 105;
int n, m, k, val[MAXN];
int temp[MAXN], cnt[MAXN];
void init() {
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> val[i];
int maximum = val[0];
for (int i = 1; i < n; i++)
if (val[i] > maximum) maximum = val[i];
m = 1;
while (maximum >= k) {
maximum /= k;
m++;
}
}
void solve() {
int base = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < k; j++) cnt[j] = 0;
for (int j = 0; j < n; j++) cnt[val[j] / base % k]++;
for (int j = 1; j < k; j++) cnt[j] += cnt[j - 1];
for (int j = n - 1; j >= 0; j--) {
temp[cnt[val[j] / base % k] - 1] = val[j];
cnt[val[j] / base % k]--;
}
for (int j = 0; j < n; j++) val[j] = temp[j];
base *= k;
}
}
int main() {
init();
solve();
for (int i = 0; i < n; i++) cout << val[i] << ' ';
cout << endl;
return 0;
}
假设输入的 \(n\) 为不大于 \(100\) 的正整数,\(k\) 为不小于 \(2\) 且不大于 \(100\) 的正整数,\(val_i\) 在 \(int\) 表示范围内,完成下面的判断题和单选题:
判断题
22.这是一个不稳定的排序算法。( )
23.该算法的空间复杂度仅与 \(n\) 有关。( )
24.该算法的时间复杂度为 \(O(m(n+k))\)。( )
单选题
25.当输入为“5 3 98 26 91 37 46
”时,程序第一次执行到第 33 行,\(val\) 数组的内容依次为( )。
A. 91 26 46 37 98
B. 91 46 37 26 98
C. 98 26 46 91 37
D. 91 37 46 98 26
26.若 \(val_i\) 的最大值为 \(100\),\(k\) 取( )时算法运算次数最少。
A. \(2\) B. \(3\) C. \(10\) D. 不确定
27.当输入的 \(k\) 比 \(val_i\) 的最大值还大时,该算法退化为( )算法。
A. 选择排序 B. 冒泡排序 C. 计数排序 D. 桶排序
答案:BBADDC
解析:不难发现这就是一个基数排序,其中 \(m\) 为 \(\log_k \max\{ val_i\}\)。然后就没什么好解释的了(
22 题,显然基数排序是稳定的。
23 题,空间复杂度还和 \(k\) 有关。
24 题,数循环即可得到答案。
25 题,手模即可。
26 题,由于时间复杂度为 \(O(m(n+k))\),所以答案和 \(n\) 有关,只给定 \(val_i\) 最大值没法确定。
27 题要注意计数排序和桶排序的区别(虽然很多教材都没区分过……)。桶排序会对数据分类储存,而计数排序直接统计每个数出现次数。根据这点,基数排序会退化成计数排序。
第三大题
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXL = 1000;
int n, k, ans[MAXL];
int main(void) {
cin >> n >> k;
if (!n) cout << 0 << endl;
else {
int m = 0;
while (n) {
ans[m++] = (n % (-k) + k) % k;
n = (ans[m - 1] - n) / k;
}
for (int i = m - 1; i >= 0; i--)
cout << char(ans[i] >= 10 ?
ans[i] + 'A' - 10 :
ans[i] + '0');
cout << endl;
}
return 0;
}
假设输入的 \(n\) 在 \(int\) 范围内,\(k\) 为不小于 \(2\) 且不大于 \(36\) 的正整数,完成下面的判断题和单选题:
判断题
28.该算法的时间复杂度为 \(O(\log_km)\)。( )
29.删除第 23 行的强制类型转换,程序的行为不变。( )
30.除非输入的 \(n\) 为 \(0\),否则程序输出的字符数为 \(O(⌊\log𝑘|𝑛|⌋ + 1)\)。( )
单选题
31 当输入为“100 7
”时,输出为( )。
A. \(202\)
B. \(1515\)
C. \(244\)
D. \(1754\)
32.当输入为“-255 8
”时,输出为“( )”。
A.$ 1400$
B. \(1401\)
C.$ 417$
D. \(400\)
33.当输入为“1000000 19
”时,输出为“( )”。
A.$ BG939$
B. \(87GIB\)
C. \(1CD428\)
D. $ 7CF1B$
答案:ABBABB
解析:\(-k\) 进制转换。鬼知道为什么 CCF 要整个负的……
28 题,每次除以一次 \(k\),所以是 \(\log k\)。
29 题,这里类型转换是为了正确输出字母,如果删掉自然不会出答案。
30 题,根据 \(k\) 进制的关系可得答案。
31 到 33 题都可以手模得到,不解释。
补全程序
第一大题
(归并第 \(k\) 小)已知两个长度均为 \(n\) 的有序数组 \(a_1\) 和 \(a_2\)(均为递增序,但不保证严格单调递增),并且给定正整数 \(k\) \((1\leq k \leq 2n)\),求数组 \(a_1\) 和 \(a_2\) 归并排序后的数组里第 \(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.①处应填( )
A. (m1 + m2) * 2
B. (m1 - 1) + (m2 - 1)
C. m1 + m2
D. (m1 + 1) + (m2 + 1)
35.②处应填( )
A. a1[m1] == a2[m2]
B. a1[m1] <= a2[m2]
C. a1[m1] >= a2[m2]
D. a1[m1] != a2[m2]
36.③处应填( )
A. left1 == right1
B. left1 < right1
C. left1 > right1
D. left1 != right1
37.④处应填( )
A. y = a1[k - left2 - 1]
B. y = a1[k - left2]
C. y = a2[k - left1 - 1]
D. y = a2[k - left1]
38.⑤处应填( )
A. y = a1[k - left2 - 1]
B. y = a1[k - left2]
C. y = a2[k - left1 - 1]
D. y = a2[k - left1]
答案:DBCCA
解析:熟悉归并排序就可以知道答案,本质上就是对两边分别二分找。
34 题,\(cnt\) 代表两个数组中的数字,所以直接相加,注意边界问题。
35 题,下面操作说明 \(a_1\) 左半部分一定小于 \(a_2\) 右半部分,所以为小于等于。
36 题,循环结束条件为 \(left_1> right_1\) 或者 \(left_2>right_2\)。
第二大题
(容器分水)有两个容器,容器 \(1\) 的容量为 \(a\) 升,容器 \(2\) 的容量为 $b $ 升;同时允许下列的三种操作,分别为:
1)FILL(i):用水龙头将容器 i(\(i\in \{1,2\}\))灌满水;
2)DROP(i):将容器 \(i\) 的水倒进下水道;
3)POUR(i,j):将容器 \(i\) 的水倒进容器 \(j\)(完成此操作后,要么容器 \(j\) 被灌满,要
么容器 \(i\) 被清空)。求只使用上述的两个容器和三种操作,获得恰好 c 升水的最少操作数和操作序列。上述 \(a、b、c\) 均为不超过 \(100\) 的正整数,且 \(c\leq \text{max} \{a,b\}\)。
试补全程序。
#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.①处应填( )
A. dfs(x + t, y - t) + 1
B. dfs(x + t, y - t) - 1
C. dfs(x - t, y + t) + 1
D. dfs(x - t, y + t) - 1
40.②处应填( )
A. dfs(x + t, y - t) + 1
B. dfs(x + t, y - t) - 1
C. dfs(x - t, y + t) + 1
D. dfs(x - t, y + t) - 1
41.③处应填( )
A. x == c || y == c
B. x == c && y == c
C. x >= c || y >= c
D. x >= c && y >= c
42.④处应填( )
A. dfs(x + t, y - t) + 1
B. dfs(x + t, y - t) - 1
C. dfs(x - t, y + t) + 1
D. dfs(x - t, y + t) - 1
43.⑤处应填( )
A. dfs(x + t, y - t) + 1
B. dfs(x + t, y - t) - 1
C. dfs(x - t, y + t) + 1
D. dfs(x - t, y + t) - 1
答案:ACAAC
解析:其实就是一个记忆化搜索,不过又加一个 go 函数输出方案。
39 题,这里的决策是 \(y\) 的水倒入 \(x\)。
40 题,和上面相反,把 \(x\) 倒入 \(y\)。
41 题,搜索边界自然就是两桶水都是 \(c\) 升。
42 题和 43 题直接照抄下面的 go 函数即可。