CSP-S 提高第一轮 积累本
绷。
随便应付应付得了。
CSP-S 2022
54pts。
单项选择
- 你同时用 time 命令和秒表为某个程序在单核 CPU 的运行计时。假如 time 命令的输出如下:
real 0m30.721s
user 0m24.579s
sys 0m6.123s
以下最接近秒表计时的时长为( )。
A. 30s
B. 24s
C. 18s
D. 6s
ans:A
real
最接近真实值,user
是程序运行时间,sys
是因为各种因素用的时间。
- 考虑对 n 个数进行排序,以下最坏时间复杂度低于 \(O(n^2)\) 的排序方法是( )。
A. 插入排序
B. 冒泡排序
C. 归并排序
D. 快速排序
ans:C
回顾各种排序方式:
-
插入排序 \(O(n)-O(n^2)\):把 \(n\) 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 \(n-1\) 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
-
冒泡排序 \(O(n)-O(n^2)\):单次将最大的元素往前冒泡。
-
归并排序 \(O(n)-O(nlogn)\):分治,将两个的有序数列合并成一个有序数列。
-
快速排序 \(O(n)-O(n^2)\):分治,是冒泡排序的改进,通常时间复杂度低于 \(O(nlogn)\)。
-
桶排序 \(O(n)-O(n^2)\):设置一个定量的数组当作空桶,遍历序列,并将元素一个个放到对应的桶中,对每个不是空的桶进行排序,从不是空的桶里把元素再放回原来的序列中。平均是 \(O(n+\frac{n^2}{k}+k)\) 的(将值域平均分成 n 块 + 排序 + 重新合并元素)。
-
计数排序 \(O(n+w)\)(\(w\) 指值域最大值):计算每个数出现了几次,求出每个数出现次数的前缀和,利用出现次数的前缀和,从右至左计算每个数的排名。
-
基数排序 \(O\left(nk\right)\)(\(k\) 指进制下最大位数),详见下阅读程序 17.
-
希尔排序 \(O(n)-O(n^2)\):这是一个不稳定的排序,时间复杂度通常为 \(O(nlog^2n)\)。
-
堆排序 \(O(nlogn)\):这是一个不稳定的排序,首先建立大顶堆,然后将堆顶的元素取出,作为最大值,与数组尾部的元素交换,并维持残余堆的性质,之后将堆顶的元素取出,作为次大值,与数组倒数第二位元素交换,并维持残余堆的性质,以此类推,在第 \(n-1\) 次操作后,整个数组就完成了排序。
- 计算机系统用小端(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
ans:B
大端和小端是用于描述存储多字节数据在计算机内存中的字节顺序的概念。
-
在大端字节序中,高位字节被存储在较低的内存地址,而低位字节被存储在较高的内存地址。这就好像把多字节数据当作一个整数,高位字节在前,低位字节在后。
-
在小端字节序中,高位字节被存储在较高的内存地址,而低位字节被存储在较低的内存地址。这就好像把多字节数据当作一个整数,低位字节在前,高位字节在后。
- 每个顶点度数均为 2 的无向图称为“2 正规图”。由编号为从 1 到 n 的顶点构成的所有 2 正规图,其中包含欧拉回路的不同 2 正规图的数量为( )。
A. n!
B. (n-1)!
C. n!/2
D. (n-1)!/2
ans:D
欧拉回路:通过图中每条边恰好一次的回路。
欧拉图:具有欧拉回路的图。
所以环一定是欧拉图。
而“每个顶点度数均为 \(2\) 的无向图”指环。
所以,问环的排列,即环排列,\((n-1)!\)。
但是因为 \(4\ 1\ 2\ 3\) 与 \(2\ 1\ 4 \ 3\) 在图上似乎是相同的,所以答案是 \(\large\frac{(n-1)!}{2}\)。
程序阅读
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 }
整体分析:
\(shift_{t_j}\) 表示的应该是在 \(t\) 串中 \(t_j\) 字符最后出现的位置后有多少个字符(包括 \(t_j\))。
所以这个程序是一个比对两个字符串的程序,其中 \(s\) 是母串,\(t\) 是子串,返回 \(t\) 在 \(s\) 第一次出现的位置。
而 \(i\) 枚举的是起始位置,如果合法返回 \(i\),否则返回 \(-1\)。
1.(1 分)当输入为“abcde fg”时,输出为-1。
A. 正确
B. 错误
ans:A
- 当输入为“abbababbbab abab”时,输出为 4。
A. 正确
B. 错误
ans:B
\(s\) 串里都没有 \(t\),为啥这能看错。
- 当输入为“GoodLuckCsp2022 22”时,第 20 行的“j++”语句执行次数为 2。
A. 正确
B. 错误
ans:B
神笔题。
只有 2022
这四个字符会比对,出现了 \(3\) 个 2
,当然是执行了 \(3\) 次。
- 该算法最坏情况下的时间复杂度为( )。
A. \(O(n+m)\)。
B. \(O(nlogm)\)。
C. \(O(mlogn)\)。
D. \(O(mn)\)。
ans:D
每次寸止挑战,就到 \(t\) 的最后一个字符就不匹配了,到 \(s\) 的最后也没匹配上。
- f(a, b) 与下列( )语句的功能最类似。
A. a.find(b)
B. a.rfind(b)
C. a.substr(b)
D. a.compare(b)
ans:A
回顾一下 string
类型各种好用的函数:
-
a.find(b,pos)
:从母串 \(a\) 的 \(pos\) 位置向右查找,在母串 \(a\) 中寻找子串 \(b\) 第一次出现的位置,返回第一个字符在 \(a\) 中的下标,若没有找到,返回一个特别的标记a.npos
(一个极大值)。 -
a.rfind(b,pos)
:从母串 \(a\) 的 \(pos\) 位置向左查找,在母串 \(a\) 中寻找子串 \(b\) 第一次出现的位置,返回第一个字符在 \(a\) 中的下标,若没有找到,返回一个特别的标记a.npos
(一个极大值)。 -
a.substr(pos,len)
:从母串 \(a\) 的 \(pos\) 位置向右截取 \(len\) 个字符(包括 \(a_pos\))。 -
a.compare(b)
:字符串比较。
- 当输入为“baaabaaabaaabaaaa aaaa”,第 20 行的“j++”语句执行次数为 ( )。
A. 9
B. 10
C. 11
D. 12
ans:B
因为我们最初 \(shift\) 的初始值赋成了 \(m+1\),所以第一个 \(a\) 枚举完 \(3\) 个 ++j
之后会跳到第五个 \(a\)。
依次类推,应该是 \(3+2+2+3=10\)。
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 表示范围内,完成下面的判断题和单选题:
整体分析:
这是一个基数排序(这个动态演示好像还挺好)。
\(m\) 是最大数可以分为的 \(k\) 进制位数,而 \(base\) 是当前进行的第几位,从个位开始向高位走,而 \(val_j/base\mod k\) 的含义就是第 \(j\) 个数当前数位上的数。
因此 \(temp\) 针对 \(j\) 个数在当前位上的数排序的数组。
- 这是一个不稳定的排序算法。( )
A. 正确
B. 错误
ans:B
- 该算法的空间复杂度仅与 n 有关。( )
A. 正确
B. 错误
ans:B
- 该算法的时间复杂度为 \(O\left(m\left(n+k\right)\right)\)
A. 正确
B. 错误
ans:A
- 当输入为“5 3 98 26 91 37 46”时,程序第一次执行到第 36 行,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
ans:C
第一次执行到第 \(36\) 行就是对于这些数的 \(3\) 进制第一位数进行排序。
\(98\) 的第一位数是 \(2\),\(26\) 的第一位数是 \(2\),\(91\) 的第一位数是 \(1\),\(37\) 的第一位数是 \(1\),\(46\) 的第一位数是 \(1\)。
根据先进先出和升序原则,最后答案应是:91 37 46 98 26。
- 若 val[i]的最大值为 100,k 取( )时算法运算次数最少。
A. 2
B. 3
C. 10
D. 不确定
ans:D
时间复杂度为 \(O(mn+mk)\),在最大值是 \(100\) 的情况下 \(m\) 等于 \(\left\lfloor\frac{100}{k}\right\rfloor\),发现时间复杂度很难去找一个确切的 \(k\) 使其最小。
- 当输入的 k 比 val[i]的最大值还大时,该算法退化为( )算法。
A. 选择排序
B. 冒泡排序
C. 计数排序
D. 桶排序
ans:C
建议向上单项选择 4. 回顾各种排序。
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 的正整数,完成下面的判断题和单选题:
整体分析:
这是一个比较简单的将 \(n\) 转 \(k\) 进制的代码,但是不知道为啥写成这样。
另外,\(n \mod k\) 和 \(n \mod (-k)\) 在 C++ 没有任何区别,因为 C++ 是向 \(0\) 取整的。
- 该算法的时间复杂度为 \(O(log_kn)\)。
A. 正确
B. 错误
ans:A
- 删除第 23 行的强制类型转换,程序的行为不变。
A. 正确
B. 错误
ans:B
- 除非输入的 \(n\) 为 \(0\),否则程序输出的字符数为 \(\left\lfloor log_k{|n|} \right\rfloor+1\)。
A. 正确
B. 错误
ans:A
- 当输入为“100 7”时,输出为( )。
A. 202
B. 1515
C. 244
D. 1754
ans:A
- 当输入为“-255 8”时,输出为( )。
A. 1400
B. 1401
C. 417
D. 400
ans:B
- 当输入为“1000000 19”时,输出为( )。
A. BG939
B. 87GIB
C. 1CD428
D. 7CF1B
ans:B
我也不知道这咋整,就草稿纸上算呗,又不可能计算机上模。
完善程序
(1)(归并第 k 小) 已知两个长度均为 n 的有序数组 a1 和 a2(均为递增序,但不保证严 格单调递增),并且给定正整数 k(1≤k≤2n),求数组 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);
}
}
}
①~⑤处应填( )
不是这是啥,不是,这啥。
解:
它先二分出两个序列:\(a_1\) 的 \(0-m_1\),\(a_2\) 的 \(0-m_2\),然后判断比当前位置小的数量。
所以 ① 应该填 m1+m2
。(但是我其实不知道为啥)
然后两个序列,如果当前的序列数量比 \(k\) 小,考虑增加,增加当前小的。
否则减小,减小当前大的。
所以 ② 应是 a1[m1] == a2[m2]
。
然后二分结束条件应该是 \(l_1>r_1\) 或者 \(l_2>r_2\)。
所以 ③ 应该是 left1 > right1
。
注意下标从 \(0\) 开始,然后比较明显的:
④ 应该是 y = a2[k - left1 - 1]
。
⑤ 应该是 y = a1[k - left2 - 1]
。
2021
写了 4 年 CSP-S 全解析,这也太强了!
42.5 pts。
这也太难了。
分数线 15pts。
单项选择
- 在 Linux 系统终端中,用于列出当前目录下所含的文件和子目录的命令为( )。
A. ls
B. cd
C. cp
D. all
ans:A
ls
命令用于显示指定工作目录下之内容(列出目前工作目录所含之文件及子目录)。
cd
命令用于切换当前工作目录。
cp
命令主要用于复制文件或目录。
- 在程序运行过程中,如果递归调用的层数过多,可能会由于( )引发错误。
A. 系统分配的栈空间溢出
B. 系统分配的队列空间溢出
C. 系统分配的链表空间溢出
D. 系统分配的堆空间溢出
ans:A
- 以比较为基本运算,对于 \(2n\) 个数,同时找到最大值和最小值,最坏情况下需要的最小的比较次数为( )。
A. \(4n-2\)
B. \(3n+1\)
C. \(3n-2\)
D. \(2n+1\)
ans:C
最逆天的一集。
将 \(2n\) 个数分成 \(n\) 个二元组 \((x,y)\),\(x\) 与 \(y\) 之间进行比较,获得 \(n\) 个较小值和较大值。
较小值之间进行 \(n-1\) 次比较得到最小值,较大值之间进行 \(n-1\) 次比较得到较大值。
\(n+n-1+n-1=3n-2\)。
- \(G\) 是一个非连通简单无向图(没有自环和重边),共有 \(36\) 条边,则该图至少有( )个点。
A. 8
B. 9
C. 10
D. 11
ans:C
在设想中,它是一个完全图。
所以解方程 \(\large\frac{n\times (n-1)}{2}=36\) 是 \(n=9\)。
但是“非联通”。所以是 \(10\)。
8.令根结点的高度为 \(1\),则一棵含有 \(2021\) 个结点的二叉树的高度至少为( )。
A. 10
B. 11
C. 12
D. 2021
ans:B
忘了根节点高度为 \(1\)……
深度为 \(10\) 时:\(2^0+2^1+\dots+2^9=1023\)。
深度为 \(11\) 时:\(2^0+2^1+\dots+2^{10}=2047\)。
- 前序遍历和中序遍历相同的二叉树为且仅为( )。
A. 只有 \(1\) 个点的二叉树
B. 根结点没有左子树的二叉树
C. 非叶子结点只有左子树的二叉树
D. 非叶子结点只有右子树的二叉树
ans:D
蒙对了。
-
前序遍历:首先访问根结点,然后遍历左子树,最后遍历右子树。
-
中序遍历:首先遍历左子树,然后访问根结点,最后遍历右子树。
-
后序遍历:首先遍历左子树,然后遍历右子树,最后访问根结点。
所以前序遍历和中序遍历相同,等于无左子树。
- 有如下递归代码:
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
ans:A
快速算出答案:费马小定理。
\(5^{22} \equiv 1 \pmod {23}\)
- \(8\) 个苹果从左到右排成一排,你要从中挑选至少一个苹果,并且不能同时挑选相邻的两个苹果,一共有( )种方案。
A. 36
B. 48
C. 54
D. 64
ans:C
\(1\) 个苹果:\(8\) 种答案。
\(2\) 个苹果:\(6+5+4+3+2+1=21\) 种答案。
\(3\) 个苹果:\(4+3+2+1+3+2+1+2+1+1=20\) 种答案。
\(4\) 个苹果:\(5\) 种答案。
也可以斐波那契解。
设 \(f_i\) 表示有 \(i\) 个苹果的方案总数(包括不取)
有 \(f_i=f_{i-1}+f_{i-2}\),\(f_8=55\),答案为 \(54\)。
设一个三位数 \(n = \overline{abc}\),其中 \(a,b,c\) 是 \(1\) 到 \(9\) 的正整数,若以 \(a,b,c\) 作为三角形的三条边可以构成等腰三角形(包括等边),则这样的 \(n\) 有( )个。
A. 81
B. 120
C. 165
D. 216
ans:C
先考虑 \(a=b\neq c\) 有几种。
\(1,1\) 有 \(0\) 种,\(2,2\) 有 \(2\) 种,\(3,3\) 有 \(4\) 种,\(4,4\) 有 \(6\) 种,此后皆为 \(8\) 种,共 \(52\) 种。
而 \(a=b=c\) 有 \(9\) 种。
共 \(52\times 3+9=165\) 种。
阅读程序
整体分析:
首先 \(\cos\ 60^\circ = 0.5\),在弧度制下为 \(\large\frac{\pi}{3}\)。
而因为球的体积公式 \(V=\dfrac{4}{3}\pi r^3\),而且每一组给了四个数,大概能猜出三个数用来描述球心的坐标,一个数是球的半径。
而且发现它竟然可以输出 0
,排除是体积并,猜测为体积交。
发现我挺傻逼的,这题有啥可错的,原来是看错选项了,更傻逼了。
整体分析:
\(solve1\) 和 \(solve2\) 都是在求最大子段和,看出来这个其实挺简单,问题是我没想看出来。
然后它让你测测它的数据,你注意它给出来的数据第一个数是 \(n\),挺缺德的。
整体分析:
分析啥啊,这是啥啊,不是,这啥。
大概是字符串的加密解密过程,所以我选择蒙。
2020
还是您。