Initial commit: C++ courseware

This commit is contained in:
xiaolinquan committed 2026-11-05 22:51:02 +08:00
commit 4a4ee7c287
1789 files changed
+373489

No files matched your search

@@ -0,0 +1,61 @@
### **2024 CCF 非专业级别软件能力认证第一轮 (CSP-J1) 参考答案**
#### **一、单项选择题**
1. **C**
2. **A** (原式为 $(12_{10} - 10_{10}) * 13_{10} - 13_{10} = 2 * 13 - 13 = 13$)
3. **B** (A选2人,BC各1人: C(4,2)×C(3,1)×C(3,1)=54; B选2人,AC各1人: C(4,1)×C(3,2)×C(3,1)=36; C选2人,AB各1人: C(4,1)×C(3,1)×C(3,2)=36。总计54+36+36=126)
4. **D** (格雷码的特性是相邻两个编码只有一位不同)
5. **D** (1MB = 1024 * 1024 Bytes = 1024 * 1024 * 8 bits = 8388608 bits)
6. **C** (`struct` 是构造数据类型,不是基本数据类型)
7. **D** (`repeat-until` 是 Pascal 等语言的语法)
8. **B** (字符'a'的ASCII码是97,97 + 13 = 110,110对应的字符是'n')
9. **B** (2⁹ = 512, 2¹⁰ = 1024。所以最多比较10次)
10. **A** (Notepad是记事本应用软件)
11. **B** (图论基本定理:每条边连接两个顶点,为总度数贡献2)
12. **A** (通过前序和中序遍历结果可以唯一确定一棵二叉树,从而得到其后序遍历)
13. **D** (栈的特性是后进先出,可逐项模拟验证,D项无法实现)
14. **A** (捆绑法:将3个女生视为一个整体,与5个男生进行全排列,有 `P(6,6)` 种方法。3个女生内部也有 `P(3,3)` 种排列。总数为 `6! * 3! = 720 * 6 = 4320`)
15. **B**
---
#### **二、阅读程序**
**(1)**
16. **√** (10以内的质数是2, 3, 5, 7,共4个,和为17)
17. **×** (判断逻辑依然正确,20以内的质数有8个,输出不会变为6)
18. **√**
19. **B** (1到50的质数之和为328)
20. **A** (修改后,任何大于1的数 `p` 在循环到 `i=p` 时都会因 `p % p == 0` 而被误判为非质数,导致结果错误)
**(2)**
21. **√** (模拟DP过程:dp[1]=10, dp[2]=min(10,0)+15=15, dp[3]=min(15,10)+20=30。返回min(dp[3], dp[2])即min(30,15)=15)
22. **×** (不会产生编译错误,但可能会因访问数组负数下标而产生运行时错误)
23. **×** (程序计算的是到达楼顶的最小花费路径,不一定是单个最小元素)
24. **A** (模拟DP过程,最终返回min(dp[10], dp[9]) = min(6, 104) = 6)
25. **B** (模拟DP过程,最终返回min(dp[7], dp[6]) = min(45, 30) = 30)
26. **A** (修改后模拟计算:dp[1]=5, dp[2]=dp[1]+cost[0]=5+5=10, dp[3]=dp[2]+cost[1]=10+10=20。返回min(20,10)=10)
**(3)**
27. **×** (`customFunction(2, 3)` 返回 8。题目问的是函数的返回值,而不是最终输出。最终输出 `pow(8, 2)` 是64)
28. **√** (b为负数,`b-1` 永远不会等于0,导致无限递归)
29. **√** (b越大,递归深度越大,运行时间越长)
30. **B** (该函数计算 `a * (b + 1)`,即 `5 * (4 + 1) = 25`)
31. **C** (`customFunction(3, 3)` 返回 `3 * (3 + 1) = 12`。程序最终输出 `pow(12, 2)` 即 144)
32. **D** (修改后递归计算:f(3,3) = 3+f(2,2) = 3+2+f(1,1) = 3+2+1+f(0,0) = 3+2+1+0=6。最终输出`pow(6,2)`即36)
---
#### **三、完善程序**
**(1) 判断平方数**
33. **A** (从1开始检查)
34. **B** (循环的上界是原数的平方根,没有必要超过它)
35. **C** (判断`i`的平方是否等于`num`)
36. **C** (如果找到,说明是平方数,返回`true`)
37. **D** (如果循环结束都没找到,说明不是平方数,返回`false`)
**(2) 汉诺塔问题**
38. **B** (递归的边界条件:当只剩一个盘子时,直接移动)
39. **B** (对应 `move(src, tgt)`)
40. **B** (第一步:将 `i-1` 个盘子从 `src` 经由 `tgt` 移到 `tmp`)
41. **B** (第三步的盘子数量为 `i-1`)
42. **B** (第三步:将 `i-1` 个盘子从 `tmp` 经由 `src` 移到 `tgt`)
@@ -0,0 +1,348 @@
<center><font size="6"><b>2019 CCF 非专业级别软件能力认证第一轮</b></font></center>
<center><font size="4"><b>(CSP-J1) 入门级 C++语言试题</b></font></center>
<center><font size="4"><b>认证时间:2019 年 9 月 16 日 09:30~11:30</b></font></center>
**考生注意事项:**
- 试题纸共有 10 页,答题纸共有 1 页,满分 100 分。请在答题纸上作答,写在试题纸上的一律无效。
- 不得使用任何电子设备(如计算器、手机、电子词典等)或查阅任何书籍资料。
---
#### **一、 单项选择题(共 15 题, 每题 2 分, 共计 30 分; 每题有且仅有一个正确选项)**
( ) 1. 中国的国家顶级域名是?
A. .cn B. .ch C. .chn D. .china
( ) 2. 二进制数 `11101110010111` 和 `01011011101011` 进行逻辑与运算的结果是?
A. 01001010001011
B. 01001010010011
C. 01001010000001
D. 01001010000011
( ) 3. 一个 32 位整型变量占用多少个字节?
A. 32 B. 128 C. 4 D. 8
( ) 4. 若有如下程序段,其中 s、a、b、c 均已定义为整型变量,且 a、c 均已赋值(c 大于 0)
```cpp
s = a;
for (b = 1; b <= c; b++)
s = s - 1;
```
则与上述程序段功能等价的赋值语句是?
A. `s = a - c;` B. `s = a - b;`
C. `s = s - c;` D. `s = b - c;`
( ) 5. 设有 100 个已排好序的数据元素,采用折半查找时,最大比较次数为?
A. 7 B. 10 C. 6 D. 8
( ) 6. 链表不具有的特点是?
A. 插入删除不需要移动元素
B. 不必事先估计存储空间
C. 所需空间与线性表长度成正比
D. 可随机访问任一元素
( ) 7. 把 8 个同样的球放在 5 个同样的袋子里,允许有的袋子空着不放,问共有多少种不同的分法?
提示:如果 8 个球都放在一个袋子里,无论是哪个袋子,都只算同一种分法。
A. 22 B. 24 C. 18 D. 20
( ) 8. 一棵二叉树如下图所示,若采用顺序存储结构,即用一维数组元素存储该二叉树中的结点(根结点的下标为 1,若某结点的下标为 i,则其左孩子位于下标 2i 处、右孩子位于下标 2i+1 处),则该数组的最大下标至少为?
<center><img src="C:\Users\xiaolinquan\AppData\Roaming\Typora\typora-user-images\image-20260630210146217.png" width="15%"></center>
​ A. 6 B. 10 C. 15 D. 12
( ) 9. 100 以内最大的素数是?
A. 89 B. 97 C. 91 D. 93
( ) 10. 319 和 377 的最大公约数是?
A. 27 B. 33 C. 29 D. 31
( ) 11. 新学期开学了,小胖想减肥,健身教练给小胖制定了两个训练方案。方案一:每次连续跑 3 公里可以消耗 300 千卡(耗时半小时);方案二:每次连续跑 5 公里可以消耗 600 千卡(耗时 1 小时)。小胖每周周一到周四能抽出半小时跑步,周五到周日能抽出一小时跑步。另外,教练建议小胖每周最多跑 21 公里,否则会损伤膝盖。请问如果小胖想严格执行教练的训练方案,并且不想损伤膝盖,每周最多通过跑步消耗多少千卡?
A. 3000 B. 2500 C. 2400 D. 2520
( ) 12. 一副纸牌除掉大小王有 52 张牌,四种花色,每种花色 13 张。假设从这 52 张牌中随机抽取 13 张纸牌,则至少多少张牌的花色一致。
A. 4 B. 2 C. 3 D. 5
( ) 13. 一些数字可以颠倒过来看,例如 0、1、8 颠倒过来还是本身, 6 颠倒过来是 9, 9 颠倒过来看还是 6,其他数字颠倒过来都不构成数字。类似的,一些多位数也可以颠倒过来看,比如 106 颠倒过来是 901。假设某个城市的车牌只由 5 位数字组成,每一位都可以取 0 到 9。请问这个城市最多有多少个车牌倒过来恰好还是原来的车牌?
A. 60 B. 125 C. 75 D. 100
( ) 14. 假设一棵二叉树的后序遍历序列为 `DGJHEBIFCA`,中序遍历序列为 `DBGEHJACIF`,则其前序遍历序列为?
A. ABCDEFGHIJ
B. ABDEGHJCFI
C. ABDEGJHCFI
D. ABDEGHJFIC
( ) 15. 以下哪个奖项是计算机科学领域的最高奖?
A. 图灵奖 B. 鲁班奖 C. 诺贝尔奖 D. 普利策奖
---
#### **二、阅读程序(程序输入不超过数组或字符串定义的范围:判断题正确填√,错误填×;除特殊说明外,判断题 1.5 分,选择题 3 分,共计 40 分)**
**(1)**
```cpp
#include <cstdio>
#include <cstring>
using namespace std;
char st[100];
int main() {
scanf("%s", st);
int n = strlen(st);
for (int i = 1; i <= n; ++i) {
if (n % i == 0) {
char c = st[i - 1];
if (c >= 'a')
st[i - 1] = c - 'a' + 'A';
}
}
printf("%s", st);
return 0;
}
```
**判断题**
( ) 1. 输入的字符串只能由小写字母或大写字母组成。
( ) 2. 若将第 8 行的“`i=1`”改为“`i=0`”,程序运行时会发生错误。
( ) 3. 若将第 8 行的“`i<=n`”改为“`i*i<=n`”,程序运行结果不会改变。
( ) 4. 若输入的字符串全部由大写字母组成,那么输出的字符串就跟输入的字符串一样。
**选择题**
( ) 5. 若输入的字符串长度为 18,那么输入的字符串跟输出的字符串相比,至多有多少个字符不同?
A. 18 B. 6 C. 10 D. 1
( ) 6. 若输入的字符串长度为\_\_\_\_\_\_,那么输入的字符串跟输出的字符串相比,至多有36个字符不同。
A. 36 B. 100000 C. 1 D. 128
**(2)**
```cpp
#include <cstdio>
using namespace std;
int n, m;
int a[100], b[100];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
a[i] = b[i] = 0;
for (int i = 1; i <= m; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (a[x] < y && b[y] < x) {
if (a[x] > 0)
b[a[x]] = 0;
if (b[y] > 0)
a[b[y]] = 0;
a[x] = y;
b[y] = x;
}
}
int ans = 0;
for (int i = 1; i <= n; ++i) {
if (a[i] == 0)
++ans;
if (b[i] == 0)
++ans;
}
printf("%d\n", ans);
return 0;
}
```
假设输入的 n 和 m 都是正整数,x 和 y 都是在[1,n]的范围内的整数,完成下面的判断题和单选题:
**判断题**
( ) 1. 当 m>0 时,输出的值一定小于 2n。
( ) 2. 执行完第 27 行的“`++ans`”时,ans 一定是偶数。
( ) 3. `a[i]`和 `b[i]`不可能同时大于 0。
( ) 4. 若程序执行到第 13 行时,x 总是小于 y,那么第 15 行不会被执行。
**选择题**
( ) 5. 若 m 个 x 两两不同,且 m 个 y 两两不同,则输出的值为?
A. 2n-2m B. 2n+2 C. 2n-2 D. 2n
( ) 6. 若 m 个 x 两两不同,且 m 个 y 都相等,则输出的值为?
A. 2n-2 B. 2n C. 2m D. 2n-2m
**(3)**
```cpp
#include <iostream>
using namespace std;
const int maxn = 10000;
int n;
int a[maxn];
int b[maxn];
int f(int l, int r, int depth) {
if (l > r)
return 0;
int min_val = maxn, mink;
for (int i = l; i <= r; ++i) {
if (min_val > a[i]) {
min_val = a[i];
mink = i;
}
}
int lres = f(l, mink - 1, depth + 1);
int rres = f(mink + 1, r, depth + 1);
return lres + rres + depth * b[mink];
}
int main() {
cin >> n;
for (int i = 0; i < n; ++i)
cin >> a[i];
for (int i = 0; i < n; ++i)
cin >> b[i];
cout << f(0, n - 1, 1) << endl;
return 0;
}
```
**判断题**
( ) 1. 如果 a 数组有重复的数字,则程序运行时会发生错误。
( ) 2. 如果 b 数组全为 0,则输出为 0。
**选择题**
( ) 3. 当 n=100 时,最坏情况下,与第 12 行的比较运算执行的次数最接近的是?
A. 5000 B. 600 C. 6 D. 100
( ) 4. 当 n=100 时,最好情况下,与第 12 行的比较运算执行的次数最接近的是?
A. 100 B. 6 C. 5000 D. 600
( ) 5. 当 n=10 时,若 b 数组满足,对任意 0≤i<n,都有 b[i]=i+1,那么输出最大为?
A. 386 B. 383 C. 384 D. 385
( ) 6. (4 分)当 n=100 时,若 b 数组满足,对任意 0≤i<n,都有 b[i]=1,那么输出最小为?
A. 582 B. 580 C. 579 D. 581
---
#### **三、完善程序(单选题,每小题 3 分,共计 30 分)**
**(1) (矩阵变幻)** 有一个奇幻的矩阵,在不停地变幻,其变幻方式为:数字 0 变成矩阵 $\begin{bmatrix} 0 & 0 \\ 0 & 1 \end{bmatrix}$,数字 1 变成矩阵 $\begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}$。最初该矩阵只有一个元素 0,变幻n 次后,矩阵会变成什么样?
输入一行一个不超过 10 的正整数 n。输出变幻 n 次后的矩阵。试补全程序。
提示:“`<<`”表示二进制左移运算符;“`^`”表示二进制异或运算符。
```cpp
#include <cstdio>
using namespace std;
int n;
const int max_size = 1 << 10;
int res[max_size][max_size];
void recursive(int x, int y, int n, int t) {
if (n == 0) {
res[x][y] = __(1)__;
return;
}
int step = 1 << (n - 1);
recursive(__(2)__, n - 1, t);
recursive(x, y + step, n - 1, t);
recursive(x + step, y, n - 1, t);
recursive(__(3)__, n - 1, !t);
}
int main() {
scanf("%d", &n);
recursive(0, 0, __(4)__);
int size = __(5)__;
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++)
printf("%d ", res[i][j]);
puts("");
}
return 0;
}
```
( ) 1. ①处应填
A. `n % 2` B. `0` C. `t` D. `1`
( ) 2. ②处应填
A. `x - step, y - step`
B. `x, y - step`
C. `x - step, y`
D. `x, y`
( ) 3. ③处应填
A. `x - step, y - step`
B. `x + step, y + step`
C. `x - step, y`
D. `x, y - step`
( ) 4. ④处应填
A. `n - 1, n % 2`
B. `n, 0`
C. `n, n % 2`
D. `n - 1, 0`
( ) 5. ⑤处应填
A. `1 << (n + 1)`
B. `1 << n`
C. `n + 1`
D. `1 << (n - 1)`
**(2) (计数排序)** 计数排序是一个广泛使用的排序方法。下面的程序使用双关键字计数排序,对 n 对 10000 以内的整数,从小到大排序。
例如有三对整数 (3, 4), (2, 4), (3, 3),那么排序之后应该是 (2, 4), (3, 3), (3, 4)。
输入第一行为 n,接下来 n 行,第 i 行有两个数 a[i] 和 b[i],分别表示第 i 对整数的第一关键字和第二关键字。
提示:应先对第二关键字排序,再对第一关键字排序。数组 `ord[]` 存储第二关键字排序的结果,数组 `res[]` 存储双关键字排序的结果。
```cpp
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 10000000;
const int maxs = 10000;
int n;
unsigned a[maxn], b[maxn], res[maxn], ord[maxn];
unsigned cnt[maxs + 1];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i)
scanf("%u%u", &a[i], &b[i]);
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; ++i)
__(1)__; // 利用 cnt 数组统计数量
for (int i = 1; i <= maxs; ++i)
cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; --i)
__(2)__; // 记录初步排序结果
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < n; ++i)
__(3)__; // 利用 cnt 数组统计数量
for (int i = 1; i <= maxs; ++i)
cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; --i)
__(4)__; // 记录最终排序结果
for (int i = 0; i < n; i++)
printf("%u %u\n", ⑤);
return 0;
}
```
( ) 1. ① 处应填
A. `++cnt[i]`
B. `++cnt[b[i]]`
C. `++cnt[a[i] * maxs + b[i]]`
D. `++cnt[a[i]]`
( ) 2. ② 处应填
A. `ord[--cnt[a[i]]] = i`
B. `ord[--cnt[b[i]]] = a[i]`
C. `ord[--cnt[a[i]]] = b[i]`
D. `ord[--cnt[b[i]]] = i`
( ) 3. ③ 处应填
A. `++cnt[b[ord[i]]]`
B. `++cnt[a[i] * maxs + b[i]]`
C. `++cnt[a[ord[i]]]`
D. `++cnt[i]`
( ) 4. ④ 处应填
A. `res[--cnt[a[ord[i]]]] = ord[i]`
B. `res[--cnt[b[ord[i]]]] = ord[i]`
C. `res[--cnt[b[i]]] = ord[i]`
D. `res[--cnt[a[i]]] = ord[i]`
( ) 5. ⑤处应填
A. `a[i], b[i]`
B. `a[res[i]], b[res[i]]`
C. `a[ord[res[i]]], b[ord[res[i]]]`
D. `a[res[ord[i]]], b[res[ord[i]]]`
@@ -0,0 +1,113 @@
### 一、单项选择题
**1. A**
**解析**:`printf` 是 C 语言标准库中的函数,属于面向过程的特性。而类的封装、继承(基类与派生类)和成员函数均是 C++ 的面向对象特性。
**2. C**
**解析**:对于选项 C(3 4 6 5 2 1),进栈顺序必然为 6, 5, 4, 3。此时栈内元素为 6, 5, 4, 3(3在栈顶)。弹出 3 和 4 后,栈顶元素为 5。要想弹出 6,必须先弹出 5,因此 6 不可能在 5 之前出栈,该序列非法。
**3. D**
**解析**:`p` 和 `q` 是指针变量,分别保存 `x` 和 `y` 的地址。`p = q` 将 `q` 的值(即 `y` 的地址)赋给 `p`,因此 `p` 指向了 `y` 的地址。
**4. C**
**解析**:数组和链表都可以排序;它们存储信息的量取决于内存,没有绝对的“谁能存更多”;数组在普通声明时大小是固定的(静态分配),而链表可以通过动态内存分配随时调整大小。
**5. B**
**解析**:根据队列 Q 的出队顺序(e2, e4, e3, e6, e5, e1),这也是它们进入队列 Q 的顺序,同时也是它们从栈 S 出栈的顺序。模拟栈 S 的操作:
- e1 进栈 (S: e1)
- e2 进栈 (S: e1, e2) -> e2 出栈 (S: e1)
- e3 进栈 (S: e1, e3)
- e4 进栈 (S: e1, e3, e4) -> e4 出栈 (S: e1, e3) -> e3 出栈 (S: e1)
- e5 进栈 (S: e1, e5)
- e6 进栈 (S: e1, e5, e6) -> e6 出栈 (S: e1, e5) -> e5 出栈 (S: e1) -> e1 出栈 (S: 空)
在此过程中,栈 S 同时容纳的最多元素个数为 3。
**6. B**
**解析**:前缀表达式(波兰式)将操作符放在操作数前面。`a+(b-c)*d` 对应的语法树根节点是 `+`,左子树是 `a`,右子树是 `*`。转化为前缀表达式为 `+ a * - b c d`。
**7. B**
**解析**:构造哈夫曼树:10(a)和15(b)合并为25;16(d)和25合并为41;29(e)和30(c)合并为59;41和59合并为100(根)。字母 d (16) 位于根结点的左子树的左子树,深度为 2,故编码长度为 2。
**8. C**
**解析**:完全二叉树中,节点 `i` 的父节点为 `i/2`,左子节点为 `2i`,右子节点为 `2i+1`。节点 9 的父节点为 4,4 的左子节点是 8,所以 9 的兄弟节点是 8。节点 9 的右子节点为 `2*9+1 = 19`。
**9. B**
**解析**:要使有向图连通(在图论强连通语境下),最少需要形成一个环,即 N 个顶点至少需要 N 条有向边。因此邻接矩阵中至少存在 N 个非零元素。
**10. D**
**解析**:栈和队列可以通过互相模拟来实现。例如,使用两个栈可以实现一个队列的操作。D选项错误。
**11. D**
**解析**:在双向循环链表中 `p` 后插入 `s`,需要四步操作,顺序需保证未更新的指针不丢失:
1. `s->next = p->next;`
2. `p->next->prev = s;` (或 `s->next->prev = s;`)
3. `s->prev = p;`
4. `p->next = s;`
**12. B**
**解析**:简单选择排序是不稳定的。例如序列 [2, 2*, 1],第一轮选择最小的 1 与第一个 2 交换,变成 [1, 2*, 2],两个 2 的相对顺序发生了改变。
**13. C**
**解析**:八进制数 $(32.1)_8$ = $3 \times 8^1 + 2 \times 8^0 + 1 \times 8^{-1}$ = $24 + 2 + 0.125 = 26.125$。
**14. B**
**解析**:字符串 `abcab` 的子串有:长度为1的 a,b,c(3);长度为2的 ab,bc,ca(3);长度为3的 abc,bca,cab(3);长度为4的 abca,bcab(2);长度为5的 abcab(1);还有1个空串,合计 13 个内容互不相同的子串。
**15. B**
**解析**:递归的基本定义就是函数直接或间接地调用自身来解决问题。
---
### 二、阅读程序
**(1) 位运算(Morton Code 交错)**
*程序分析:该程序利用位运算将 `x` 和 `y` 的二进制位交错插入,实现空间降维(Z-order curve)。*
**16. √**:去掉 `unsigned` 后,变量变为有符号 `short`。由于输入均是不超过 15(`1111`)的非负数,进行左移和按位与运算时始终在低 8 位操作,符号位不受影响,结果不变。
**17. ×**:若改为 `char`,最后 `z` 也是 `char` 类型。当使用 `cout << z` 输出时,C++ 会将 `char` 作为**字符**(ASCII)输出,而不是输出对应的整数数字,导致输出行为发生巨大改变。
**18. ×**:如 x=1, y=1 时,输出显然不为 0。
**19. ×**:输入 `2 2`,此时 `x=2(0010)`,交错后 `x=4(0100)`;同理 `y=4(0100)`。`z = 4 | (4 << 1) = 4 | 8 = 12`,并非 10。
**20. ×**:计算同上,输出为 12。
**21. B**:当输入 `13 8` 时:13 二进制是 `1101`,展开后为 `01010001`,即 `x=81`;8 二进制是 `1000`,展开后为 `01000000`,即 `y=64`。`y<<1 = 128`,`z = 81 | 128 = 209`。
**(2) 动态规划(扔鸡蛋问题)**
*程序分析:这是经典的扔鸡蛋问题。`f` 是递归求解,`g` 是对应的 DP 递推求解。*
**22. ×**:利用递归树递推其函数调用次数:执行 `min` 的次数等于 `for` 循环迭代总次数。计算得出 $C(7, 3) = 448$ 次,而不是 449 次。
**23. √**:两个函数实现完全等价的状态转移方程,一个为纯递归,另一个为自底向上的动态规划,所以在不考虑超时的理论情况下,两者输出结果恒等。
**24. √**:观察代码 `if (m == 1) return n;`,当 m 为 1 时,函数直接返回 n。
**25. C**:`g(n, m)` 有三层 `for` 循环:第一层到 $n$,第二层到 $m$,第三层到 $i$ (即受限于 $n$)。时间复杂度为 $O(n \times m \times n) = O(n^2m)$。
**26. C**:输入 `20 2`,表示 2 个鸡蛋测 20 层楼。公式为求满足 $k(k+1)/2 \ge 20$ 的最小 $k$。当 $k=6$ 时,得 21,满足条件,因此答案为 6。
**27. B**:鸡蛋数 $m=100$ 足以覆盖楼层数 $n=100$ 的二分查找消耗。只需执行标准的二分查找,最坏情况需 $\lceil \log_2(101) \rceil = 7$ 次。
**(3) 二分查找与牛顿迭代法求平方根**
*程序分析:`solve1` 通过二分查找求 $\lfloor \sqrt{n} \rfloor$;`solve2` 通过牛顿迭代法逼近精确的 $\sqrt{n}$。*
**28. √**:`solve1` 的 `while` 循环折半查找需要 $O(\log n)$,`solve2` 进行 `k` 次循环需要 $O(k)$,总时间复杂度为 $O(\log n + k)$。
**29. √**:9801 是 99 的平方,`solve1` 返回 99,`solve2` 第一轮迭代公式 $x = (99 + 9801/99)/2 = 99$,故直接输出 99。
**30. ×**:C++ 中浮点数 `double` 存在精度限制。对于非完全平方数,无论迭代多少次,`ans * ans` 由于精度丢失,难以做到在二进制浮点下与原整数 `n` 绝对全等(`==` 通常为 `false`)。
**31. ×**:输入 $n$ 最大为 47000。此时 `mid` 最大约 23500。`mid * mid` 最大为 $23500^2 = 552,250,000$。该值远小于 32 位带符号整型上限 $2^{31}-1 = 2,147,483,647$,不会发生溢出。
**32. C**:$n=2, k=1$。`solve1(2)` 返回 1。`solve2` 迭代一次:$x = (1 + 2/1) / 2 = 1.5$。
**33. B**:牛顿迭代法收敛极快,经过 10 次迭代,求出的解高度逼近于 $\sqrt{3}$,其值为 1.73205...。
**34. A**:$n=256, k=11$。256 为 16 的平方,`solve1` 准确返回 16。带入 `solve2` 每次迭代结果均为 16 本身,等于 16。
---
### 三、完善程序
**(1) 枚举因数**
*寻找 `n` 所有的正因数。先找出所有 $<\sqrt{n}$ 的因数存入数组 `fac`,然后输出,再判断是否有 $\sqrt{n}$ 这个特定因数,最后倒序输出 $> \sqrt{n}$ 的配对因数。*
**35. A**:判断 `i` 是否是 `n` 的因数,条件为 `n % i == 0`。
**36. B**:首先顺序输出前半部分的因数,保存在 `fac` 数组中,故直接输出 `fac[k]`。
**37. C**:判断完全平方数的情况,即当 `i` 的平方刚好等于 `n` 时,`i` 就是处于中间的那一个因数(比如 `n=9` 时的 `3`),条件为 `i * i == n`。
**38. D**:若成立,需要单独输出该平方根因数,即 `i`。
**39. A**:最后通过逆序遍历 `fac` 数组,输出与之配对的较大的因数,即 `n / fac[k]`。
**(2) 洪水填充**
*经典的 BFS (广度优先搜索) 算法应用,在 8x8 棋盘上做颜色的联通块覆盖。*
**40. A**:入队前检查当前遍历到的像素,它必须在未修改之前与原中心点的颜色保持一致,所以 `image[r][c] == prev_color`。
**41. B**:找到起始点 `cur` 后,应立刻将其颜色标记修改为给定的新颜色 `new_color`,避免以后重复入队。`image[cur.r][cur.c] = new_color`。
**42. C**:定义 `points[4]` 分别表示上下左右四个邻居。现给出了上 `r-1`、右 `c+1`、左 `c-1`,缺失的那个自然是下方的坐标,即 `Point(pt.r + 1, pt.c)`。
**43. D**:在寻找到满足拓展条件的邻居点 `p` 时,需将其染色以作为已访问的标记:`image[p.r][p.c] = new_color`。
**44. A**:染色完毕后,将该符合条件的邻居节点 `p` 压入队列,以进行下一步的外层搜索扩展,即 `queue.push(p)`。
@@ -0,0 +1,472 @@
<center><font size="6"><b>2023 CCF 非专业级别软件能力认证第一轮</b></font></center>
<center><font size="4"><b>(CSP-S1) 提高级 C++语言试题</b></font></center>
<center><font size="4"><b>认证时间:2023年9月16日 14:30~16:30</b></font></center>
**考生注意事项:**
- 试题纸共有13 页,答题纸共有1页,满分 100 分。请在答题纸上作答,写在试题纸上的一律无效。
- 不得使用任何电子设备(如计算器、手机、电子词典等)或查阅任何书籍资料。
---
#### **一、单项选择题(共15题,每题2分,共计 30分:每题有且仅有一个正确选项)**
( ) 1. 在 Linux 系统终端中,以下哪个命令用于创建一个新的目录?
A. newdir
B. mkdir
C. create
D. mkfolder
( ) 2. 0, 1, 2, 3, 4中选取4个数字,能组成多少个不同四位数。(注:最小的四位数是1000, 最大的四位数是9999。)
A. 96
B. 18
C. 120
D. 84
( ) 3. 假设n是图的顶点的个数,m是图的边的个数,为求解某一问题有下面四种不同时间复杂度的算法。对于m = O(n)的稀疏图而言,下面的四个选项,哪一项的渐近时间复杂度最小。
A. O(m log n / log log n)
B. O(n² + m)
C. O(n²/log m + m log n)
D. O(m + n log n)
( ) 4. 假设有n根柱子,需要按照以下规则依次放置编号为1、2、3、……的圆环:每根柱子的底部固定,顶部可以放入圆环;每次从柱子顶部放入圆环时,需要保证任何两个相邻圆环的编号之和是一个完全平方数。请计算当有4根柱子时,最多可以放置多少个圆环。
A. 7
B. 9
C. 11
D. 5
( ) 5. 以下对数据结构的表述不恰当的一项是:( )。
A. 队列是一种先进先出(FIFO)的线性结构
B. 哈夫曼树的构造过程主要是为了实现图的深度优先搜索
C. 散列表是一种通过散列函数将关键字映射到存储位置的数据结构
D. 二叉树是一种每个结点最多有两个子结点的树结构
( ) 6. 以下连通无向图中,( )一定可以用不超过两种颜色进行染色。
A. 完全三叉树
B. 平面图
C. 边双连通图
D. 欧拉图
( ) 7. 最长公共子序列长度常常用来衡量两个序列的相似度。其定义如下:给定两个序列X= {x₁, x₂, ..., xₘ}和Y = {y₁, y₂, ..., yₙ},最长公共子序列(LCS)问题的目标是找到一个最长的新序列 Z = {z₁, z₂, ..., zₖ},使得序列Z既是序列X的子序列,又是序列Y的子序列,且序列Z的长度k在满足上述条件的序列里是最大的。则序列“ABCAAAABA”和“ABABCBABA”的最长公共子序列长度为()。
A. 4
B. 5
C. 6
D. 7
( ) 8. 一位玩家正在玩一个特殊的掷骰子的游戏,游戏要求连续掷两次骰子,收益规则如下:玩家第一次掷出点x,得到 2x 元;第二次掷出y点,当y=x 时玩家会失去之前得到的2x 元, 而当 y≠x 时玩家能保住第一次获得的 2x 元。上述x,y∈{1,2,3,4,5,6}。假设骰子出任意一点的概率均为1/6,玩家连续掷两次骰子后,所有可能情形下收益的平均值是多少?
A. 7元
B. 35/6元
C. 6元
D. 49/6元
( ) 9. 假设我们有以下的C++代码:
```cpp
int a = 5, b = 3, c = 4;
bool res = a & b || c ^ b && a | c;
```
​ 请问,`res` 的值是什么?()
​ > 提示:在C++中,逻辑运算的优先级从高到低依次为:逻辑非(!)、逻辑与(&&)、逻辑或(||)。位运算的优先级从高到低依次为:位非(~)、位与(&)、位异或(^)、位或(|)。同时,双目位运算的优先级高于双目逻辑运算;逻辑非和位非优先级相同,且高于所有双目运算符。
​ A. true
​ B. false
​ C. 1
​ D. 0
( ) 10. 假设快速排序算法的输入是一个长度为n的已排序数组,且该快速排序算法在分治过程总是选择第一个元素作为基准元素。以下哪个选项描述的是在这种情况下的快速排序行为?()
A. 快速排序对于此类输入的表现最好,因为数组已经排序。
B. 快速排序对于此类输入的时间复杂度是 O(n log n)。
C. 快速排序对于此类输入的时间复杂度是 O(n²)。
D. 快速排序无法对此类数组进行排序,因为数组已经排序。
( ) 11. 以下哪个命令,能将一个名为“main.cpp”的C++源文件,编译并生成一个名为“main”的可执行文件?
A. `g++ -o main main.cpp`
B. `g++ -o main.cpp main`
C. `g++ main -o main.cpp`
D. `g++ main.cpp -o main.cpp`
( ) 12. 在图论中,树的重心是树上的一个结点,以该结点为根时,使得其所有的子树中结点数最多的子树的结点数最少。一棵树可能有多个重心。请问下面哪种树一定只有一个重心?
A. 4个结点的树
B. 6个结点的树
C. 7个结点的树
D. 8个结点的树
( ) 13. 如图是一张包含6个顶点的有向图,但顶点间不存在拓扑序。如果要删除其中一条边,使这6个顶点能进行拓扑排序,请问总共有多少条边可以作为候选的被删除边?( )
![image-20260630220127908](C:\Users\xiaolinquan\AppData\Roaming\Typora\typora-user-images\image-20260630220127908.png)
A. 1
B. 2
C. 3
D. 4
( ) 14. ![image-20260630220414094](C:\Users\xiaolinquan\AppData\Roaming\Typora\typora-user-images\image-20260630220414094.png)
A. 10
B. 11
C. 12
D. 13
( ) 15. 现在用如下代码来计算xⁿ,其时间复杂度为()。
```cpp
double quick_power(double x, unsigned n) {
if (n == 0) return 1;
if (n == 1) return x;
return quick_power(x, n / 2)
* quick_power(x, n / 2)
* ((n & 1) ? x : 1);
}
```
​ A. O(n)
​ B. O(1)
​ C. O(log n)
​ D. O(n log n)
---
#### **二、阅读程序(程序输入不超过数组或字符串定义的范围:判断题正确填√,错误填×;除特殊说明外,判断题1.5分,选择题3分,共计40分)**
**(1)**
```cpp
#include <iostream>
using namespace std;
unsigned short f(unsigned short x) {
x ^= x << 6;
x ^= x >> 8;
return x;
}
int main() {
unsigned short x;
cin >> x;
unsigned short y = f(x);
cout << y << endl;
return 0;
}
```
假设输入的x是不超过65535 的自然数,完成下面的判断题和单选题。
**判断题**
( ) 16. 当输入非零时,输出一定不为零。
( ) 17. (2 分)将f函数的输入参数的类型改为unsigned int,程序的输出不变。
( ) 18. 当输入为“65535”时,输出为“63”。
( ) 19. 当输入为“1”时,输出为“64”。
**单选题**
( ) 20. 当输入为“512”时,输出为()。
A. "33280"
B. "33410"
C. "33106"
D. "33346"
( ) 21. 当输入为“64”时,执行完第5行后x的值为( )。
A. "8256"
B. "4130"
C. "4128"
D. "4160"
**(2)**
```cpp
#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;
long long solve1(int n) {
vector<bool> p(n + 1, true);
vector<long long> f(n + 1, 0), g(n + 1, 0);
f[1] = 1;
for (int i = 2; i * i <= n; i++) {
if (p[i]) {
vector<int> d;
for (long long k = i; k <= n; k *= i) d.push_back(k); // 译者注:k类型应为long long
reverse(d.begin(), d.end());
for (int k : d) {
for (int j = k; j <= n; j += k) {
if (p[j]) {
p[j] = false;
f[j] = i;
g[j] = k;
}
}
}
}
}
for (int i = sqrt(n) + 1; i <= n; i++) {
if (p[i]) {
f[i] = i;
g[i] = i;
}
}
long long sum = 1;
for (int i = 2; i <= n; i++) {
f[i] = f[i / g[i]] * (g[i] * f[i] - 1) / (f[i] - 1);
sum += f[i];
}
return sum;
}
long long solve2(int n) {
long long sum = 0;
for (int i = 1; i <= n; i++) {
sum += i * (n / i);
}
return sum;
}
int main() {
int n;
cin >> n;
cout << solve1(n) << endl;
cout << solve2(n) << endl;
return 0;
}
```
假设输入的n是不超过1000000 的自然数,完成下面的判断题和单选题:
**判断题**
( ) 22. 将第15 行删去,输出不变。
( ) 23. 当输入为“10”时,输出的第一行大于第二行。
( ) 24. (2分)当输入为“1000”时,输出的第一行与第二行相等。
**单选题**
( ) 25. `solve1(n)`的时间复杂度为( )。
A. O(n log² n)
B. O(n)
C. O(n log n)
D. O(n log log n)
( ) 26. `solve2(n)`的时间复杂度为( )。
A. O(n²)
B. O(n)
C. O(n log n)
D. O(n√n)
( ) 27. 输入为“5”时,输出的第二行为( )
A. "20"
B. "21"
C. "22"
D. "23"
**(3)**
```cpp
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
bool f0(vector<int>& a, int m, int k) {
int s = 0;
for (int i = 0, j = 0; i < a.size(); i++) {
while (a[i] - a[j] > m) j++;
s += i - j;
}
return s >= k;
}
int f(vector<int>& a, int k) {
sort(a.begin(), a.end());
int g = 0;
int h = a.back() - a[0];
while (g < h) {
int m = g + (h - g) / 2;
if (f0(a, m, k)) {
h = m;
} else {
g = m + 1;
}
}
return g;
}
int main() {
int n, k;
cin >> n >> k;
vector<int> a(n, 0);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
cout << f(a, k) << endl;
return 0;
}
```
假设输入总是合法的且 |a[i]| ≤ 10⁹, n ≤ 100000 和 1 ≤ k ≤ n(n-1)/2,完成下面的判断题和单选题:
**判断题**
( ) 28. 将第24 行的“m”改为“m - 1”,输出有可能不变,而剩下情况为少1。
( ) 29. 将第22 行的“`g + (h - g) / 2`”改为“`(h + g) >> 1`", 输出不变。
( ) 30. 当输入为“5 7 2 -4 5 1 -3”,输出为“5”。
**单选题**
( ) 31. 设数组中最大值减最小值加1 为A,则f函数的时间复杂度为()。
A. O(n log A)
B. O(n² log A)
C. O(n log (nA))
D. O(n log n)
( ) 32. 将第 10 行中的“>”替换为“>=”,那么原输出与现输出的大小关系为()。
A. 一定小于
B. 一定小于等于且不一定小于
C. 一定大于等于且不一定大于
D. 以上三种情况都不对
( ) 33. 当输入为“5 8 2 -5 3 8 -12”时,输出为()。
A. "13"
B. "14"
C. "8"
D. "15"
---
#### **三、完善程序(单选题,每小题3分,共计 30分)**
**(1) (第k 小路径)** 给定一张 n 个点 m 条边的有向无环图,顶点编号从 0 到 n-1。对于一条路径,我们定义“路径序列”为该路径从起点出发依次经过的顶点编号构成的序列。求所有至少包含一个点的简单路径中,“路径序列”字典序第k 小的路径。保证存在至少k 条路径。上述参数满足 1 ≤ n, m ≤ 10⁵ 和 1 ≤ k ≤ 10¹⁸。
在程序中,我们求出从每个点出发的路径数量。超过10¹⁸的数都用 10¹⁸ 表示。然后我们根据 k 的值和每个顶点的路径数量,确定路径的起点,然后可以类似地依次求出路径中的每个点。试补全程序。
```cpp
#include <iostream>
#include <algorithm>
#include <vector>
const int MAXN = 100000;
const long long LIM = 1000000000000000001LL;
int n, m, deg[MAXN];
std::vector<int> E[MAXN];
long long k, f[MAXN];
int next(std::vector<int> cand, long long &k) {
std::sort(cand.begin(), cand.end());
for (int u : cand) {
if (①) return u;
k -= f[u];
}
return -1;
}
int main() {
std::cin >> n >> m >> k;
for (int i = 0; i < m; ++i) {
int u, v;
std::cin >> u >> v; // 一条从u到v的边
E[u].push_back(v);
++deg[v];
}
std::vector<int> Q;
for (int i = 0; i < n; ++i)
if (!deg[i]) Q.push_back(i);
for (int i = 0; i < n; ++i) { // 译者注:此处应为 i < Q.size()
int u = Q[i];
for (int v : E[u]) {
--deg[v];
if (②) Q.push_back(v);
}
}
std::reverse(Q.begin(), Q.end());
for (int u : Q) {
f[u] = 1;
for (int v : E[u]) f[u] = ③;
}
int u = next(Q, k);
std::cout << u << std::endl;
while (④) {
⑤;
u = next(E[u], k);
std::cout << u << std::endl;
}
return 0;
}
```
( ) 34. ①处应填
A. `k >= f[u]`
B. `k <= f[u]`
C. `k > f[u]`
D. `k < f[u]`
( ) 35. ②处应填
A. `deg[v] == 1`
B. `deg[v] == 0`
C. `deg[v] > 1`
D. `deg[v] > 0`
( ) 36. ③处应填
A. `std::min(f[u] + f[v], LIM)`
B. `std::min(f[u] + f[v] + 1, LIM)`
C. `std::min(f[u] * f[v], LIM)`
D. `std::min(f[u] * (f[v] + 1), LIM)`
( ) 37. ④处应填
A. `u != -1`
B. `!E[u].empty()`
C. `k > 0`
D. `k > 1`
( ) 38. ⑤处应填
A. `k += f[u]`
B. `k -= f[u]`
C. `--k`
D. `++k`
**(2) (最大值之和)** 给定整数序列 a₀, a₁, ..., aₙ₋₁,求该序列所有非空连续子序列的最大值之和。上述参数满足 1≤n≤10⁵ 和 1≤|aᵢ|≤10⁹。
一个序列的非空连续子序列可以用两个下标 l 和 r (其中 0≤l≤r<n)表示,对应的序列为 aₗ, aₗ₊₁, ..., aᵣ。两个非空连续子序列不同,当且仅当下标不同。
例如,当原序列为 `[1,2,1,2]`时,要计算子序列 `[1]`, `[2]`, `[1]`, `[2]`, `[1,2]`, `[2,1]`, `[1,2]`, `[1,2,1]`, `[2,1,2]`, `[1,2,1,2]` 的最大值之和,答案为 18。
解决该问题有许多算法,以下程序使用分治算法,时间复杂度 O(n log n)。试补全程序。
```cpp
#include <iostream>
#include <algorithm>
#include <vector>
const int MAXN = 100000;
int n;
int a[MAXN];
long long ans;
void solve(int l, int r) {
if (l + 1 == r) {
ans += a[l];
return;
}
int mid = (l + r) >> 1;
std::vector<int> pre(a + mid, a + r);
for (int i = 1; i < r - mid; ++i) ①;
std::vector<long long> sum(r - mid + 1);
for (int i = 0; i < r - mid; ++i) sum[i + 1] = sum[i] + pre[i];
for (int i = mid - 1, j = mid, max_val = 0; i >= l; --i) { // 译者注:max应为max_val
while (j < r && ②) ++j;
max_val = std::max(max_val, a[i]);
ans += ③;
ans += ④;
}
solve(l, mid);
solve(mid, r);
}
int main() {
std::cin >> n;
for (int i = 0; i < n; ++i) std::cin >> a[i];
⑤;
std::cout << ans << std::endl;
return 0;
}
```
( ) 39. ①处应填
A. `pre[i] = std::max(pre[i - 1], a[i - 1])`
B. `pre[i + 1] = std::max(pre[i], pre[i + 1])`
C. `pre[i] = std::max(pre[i - 1], a[i])`
D. `pre[i] = std::max(pre[i], pre[i - 1])`
( ) 40. ②处应填
A. `a[j] < max_val`
B. `a[j] < a[i]`
C. `pre[j - mid] < max_val`
D. `pre[j - mid] > max_val`
( ) 41. ③处应填
A. `(long long)(j - mid) * max_val`
B. `(long long)(j - mid) * (l - 1) * max_val`
C. `sum[j - mid]`
D. `sum[j - mid] * (i - 1)`
( ) 42. ④处应填
A. `(long long)(r - j) * max_val`
B. `(long long)(r - j) * (mid - i) * max_val`
C. `sum[r - mid] - sum[j - mid]`
D. `(sum[r - mid] - sum[j - mid]) * (mid - i)`
( ) 43. ⑤处应填
A. `solve(0, n)`
B. `solve(0, n - 1)`
C. `solve(1, n)`
D. `solve(1, n - 1)`
@@ -0,0 +1,61 @@
### **2024 CCF 非专业级别软件能力认证第一轮 (CSP-J1) 参考答案**
#### **一、单项选择题**
1. **C**
2. **A** (原式为 $(12_{10} - 10_{10}) * 13_{10} - 13_{10} = 2 * 13 - 13 = 13$)
3. **B** (A选2人,BC各1人: C(4,2)×C(3,1)×C(3,1)=54; B选2人,AC各1人: C(4,1)×C(3,2)×C(3,1)=36; C选2人,AB各1人: C(4,1)×C(3,1)×C(3,2)=36。总计54+36+36=126)
4. **D** (格雷码的特性是相邻两个编码只有一位不同)
5. **D** (1MB = 1024 * 1024 Bytes = 1024 * 1024 * 8 bits = 8388608 bits)
6. **C** (`struct` 是构造数据类型,不是基本数据类型)
7. **D** (`repeat-until` 是 Pascal 等语言的语法)
8. **B** (字符'a'的ASCII码是97,97 + 13 = 110,110对应的字符是'n')
9. **B** (2⁹ = 512, 2¹⁰ = 1024。所以最多比较10次)
10. **A** (Notepad是记事本应用软件)
11. **B** (图论基本定理:每条边连接两个顶点,为总度数贡献2)
12. **A** (通过前序和中序遍历结果可以唯一确定一棵二叉树,从而得到其后序遍历)
13. **D** (栈的特性是后进先出,可逐项模拟验证,D项无法实现)
14. **A** (捆绑法:将3个女生视为一个整体,与5个男生进行全排列,有 `P(6,6)` 种方法。3个女生内部也有 `P(3,3)` 种排列。总数为 `6! * 3! = 720 * 6 = 4320`)
15. **B**
---
#### **二、阅读程序**
**(1)**
16. **√** (10以内的质数是2, 3, 5, 7,共4个,和为17)
17. **×** (判断逻辑依然正确,20以内的质数有8个,输出不会变为6)
18. **√**
19. **B** (1到50的质数之和为328)
20. **A** (修改后,任何大于1的数 `p` 在循环到 `i=p` 时都会因 `p % p == 0` 而被误判为非质数,导致结果错误)
**(2)**
21. **√** (模拟DP过程:dp[1]=10, dp[2]=min(10,0)+15=15, dp[3]=min(15,10)+20=30。返回min(dp[3], dp[2])即min(30,15)=15)
22. **×** (不会产生编译错误,但可能会因访问数组负数下标而产生运行时错误)
23. **×** (程序计算的是到达楼顶的最小花费路径,不一定是单个最小元素)
24. **A** (模拟DP过程,最终返回min(dp[10], dp[9]) = min(6, 104) = 6)
25. **B** (模拟DP过程,最终返回min(dp[7], dp[6]) = min(45, 30) = 30)
26. **A** (修改后模拟计算:dp[1]=5, dp[2]=dp[1]+cost[0]=5+5=10, dp[3]=dp[2]+cost[1]=10+10=20。返回min(20,10)=10)
**(3)**
27. **×** (`customFunction(2, 3)` 返回 8。题目问的是函数的返回值,而不是最终输出。最终输出 `pow(8, 2)` 是64)
28. **√** (b为负数,`b-1` 永远不会等于0,导致无限递归)
29. **√** (b越大,递归深度越大,运行时间越长)
30. **B** (该函数计算 `a * (b + 1)`,即 `5 * (4 + 1) = 25`)
31. **C** (`customFunction(3, 3)` 返回 `3 * (3 + 1) = 12`。程序最终输出 `pow(12, 2)` 即 144)
32. **D** (修改后递归计算:f(3,3) = 3+f(2,2) = 3+2+f(1,1) = 3+2+1+f(0,0) = 3+2+1+0=6。最终输出`pow(6,2)`即36)
---
#### **三、完善程序**
**(1) 判断平方数**
33. **A** (从1开始检查)
34. **B** (循环的上界是原数的平方根,没有必要超过它)
35. **C** (判断`i`的平方是否等于`num`)
36. **C** (如果找到,说明是平方数,返回`true`)
37. **D** (如果循环结束都没找到,说明不是平方数,返回`false`)
**(2) 汉诺塔问题**
38. **B** (递归的边界条件:当只剩一个盘子时,直接移动)
39. **B** (对应 `move(src, tgt)`)
40. **B** (第一步:将 `i-1` 个盘子从 `src` 经由 `tgt` 移到 `tmp`)
41. **B** (第三步的盘子数量为 `i-1`)
42. **B** (第三步:将 `i-1` 个盘子从 `tmp` 经由 `src` 移到 `tgt`)
@@ -0,0 +1,299 @@
<center><font size="6"><b>2024 CCF 非专业级别软件能力认证第一轮</b></font></center>
<center><font size="4"><b>(CSP-J1) 入门级 C++语言试题</b></font></center>
<center><font size="4"><b>认证时间:2024 年 9 月 16 日 09:30~11:30</b></font></center>
**考生注意事项:**
- 试题纸共有 10 页,答题纸共有 1 页,满分 100 分。请在答题纸上作答,写在试题纸上的一律无效。
- 不得使用任何电子设备(如计算器、手机、电子词典等)或查阅任何书籍资料。
---
#### **一、 单项选择题(共 15 题, 每题 2 分, 共计 30 分; 每题有且有一个正确选项)**
( ) 1. 32位 `int` 类型的存储范围是
A. -2147483647 ~ +2147483647
B. -2147483647 ~ +2147483648
C. -2147483648 ~ +2147483647
D. -2147483648 ~ +2147483648
( ) 2. 计算 $(14_8 - 1010_2) * D_{16} - 1101_2$ 的结果,并选择答案的十进制值
A. 13 B. 14 C. 15 D. 16
( ) 3. 某公司有 10 名员工,分为 3 个部门:A 部门有 4 名员工,B 部门有 3 名员工,C部门有 3 名员工。现需要从这 10 名员工中选出 4 名组成一个工作组,且每个部门至少要有 1 人。问有多少种选择方式?
A. 120 B. 126 C. 132 D. 238
( ) 4. 以下哪个序列对应数组0至8的4位二进制格雷码(Gray code)
A. 0000, 0001, 0011, 0010, 0110, 0111, 0101, 1000
B. 0000, 0001, 0011, 0010, 0110, 0111, 0100, 0101
C. 0000, 0001, 0011, 0010, 0100, 0101, 0111, 0110
D. 0000, 0001, 0011, 0010, 0110, 0111, 0101, 0100
( ) 5. 记 1KB 为 1024 字节(byte),1MB 为 1024KB,那么 1MB 是多少二进制位(bit)?
A. 1000000 B. 1048576 C. 8000000 D. 8388608
( ) 6. 以下哪个不是C++中的基本数据类型?
A. int B. float C. struct D. char
( ) 7. 以下哪个不是C++中的循环语句?
A. for B. while C. do-while D. repeat-until
( ) 8. 在C/C++中,`(char)('a' + 13)` 与下面的哪一个值相等
A. 'm' B. 'n' C. 'z' D. '3'
( ) 9. 假设有序表中有1000个元素,则用二分法查找元素最多需要比较\_\_\_\_次
A. 25 B. 10 C. 7 D. 1
( ) 10. 下面哪一个不是操作系统名字
A. Notepad B. Linux C. Windows D. macOS
( ) 11. 在无向图中,所有顶点的度数之和等于
A. 图的边数 B. 图的边数的两倍 C. 图的顶点数 D. 图的顶点数的两倍
( ) 12. 已知二叉树的前序遍历为 `[A,B,D,E,C,F,G]`,中序遍历为 `[D,B,E,A,F,C,G]`,求二叉树的后序遍历的结果是
A. `[D,E,B,F,G,C,A]` B. `[D,E,B,F,G,A,C]` C. `[D,B,E,F,G,C,A]` D. `[D,E,B,F,G,A,C]`
( ) 13. 给定一个空栈,支持入栈和出栈操作。若入栈操作的元素依次是1,2,3,4,5,6,其中1最先入栈,6最后入栈,下面哪种出栈顺序是不可能的
A. 6 5 4 3 2 1 B. 1 6 5 4 3 2 C. 2 4 6 5 3 1 D. 1 3 5 2 4 6
( ) 14. 有5个男生和3个女生站成一排,规定3个女生必须相邻,问有多少种不同的排列方式?
A. 4320 种 B. 5040 种 C. 3600 种 D. 2880 种
( ) 15. 编译器的主要作用是什么?
A. 直接执行源代码 B. 将源代码转换为机器代码
C. 进行代码调试 D. 管理程序运行时的内存
---
#### **二、阅读程序 (程序输入不超过数组或字符串定义的范围,判断题正确填 √,错误填 ×; 除特殊说明外,判断题 1.5 分,选择题 3 分,共计 40 分)**
**(1)**
```cpp
1 #include <iostream>
2 using namespace std;
3
4 bool isPrime(int n) {
5 if (n <= 1) {
6 return false;
7 }
8 for (int i = 2; i * i <= n; i++) {
9 if (n % i == 0) {
10 return false;
11 }
12 }
13 return true;
14 }
15
16 int countPrimes(int n) {
17 int count = 0;
18 for (int i = 2; i <= n; i++) {
19 if (isPrime(i)) {
20 count++;
21 }
22 }
23 return count;
24 }
25
26 int sumPrimes(int n) {
27 int sum = 0;
28 for (int i = 2; i <= n; i++) {
29 if (isPrime(i)) {
30 sum += i;
31 }
32 }
33 return sum;
34 }
35
36 int main() {
37 int x;
38 cin >> x;
39 cout << countPrimes(x) << " " << sumPrimes(x) << endl;
40 return 0;
41 }
```
**判断题**
( ) 16. 当输入为“10”时,程序的第一个输出为“4”,第二个输出为“17”。
( ) 17. 若将 `isPrime()` 函数中的条件改为 `i <= n/2`,输入“20”时,`countPrimes(20)` 的输出将变为“6”。
( ) 18. `sumPrimes` 函数计算的是从2到n之间的所有素数之和。
**单选题**
( ) 19. 当输入为“50”时,`sumPrimes(50)` 的输出为
A. 1060 B. 328 C. 381 D. 275
( ) 20. 如果将 `for(int i = 2; i * i <= n; i++)` 改为 `for(int i = 2; i <= n; i++)`,输入“10”时,程序的输出
A. 将不能正确计算10以内素数个数及其和
B. 仍然输出“4”和“17”
C. 输出“3”和“10”
D. 输出结果不变,但运行时间更短
**(2)**
```cpp
1 #include <iostream>
2 #include <vector>
3 using namespace std;
4
5 int compute(vector<int> &cost) {
6 int n = cost.size();
7 vector<int> dp(n + 1, 0);
8 dp[1] = cost[0];
9
10 for (int i = 2; i <= n; i++) {
11 dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i - 1];
12 }
13 return min(dp[n], dp[n - 1]);
14 }
15
16 int main() {
17 int n;
18 cin >> n;
19 vector<int> cost(n);
20 for (int i = 0; i < n; i++) {
21 cin >> cost[i];
22 }
23 cout << compute(cost) << endl;
24 return 0;
25 }
```
**判断题**
( ) 21. 当输入的 `cost` 数组为 `{10, 15, 20}` 时,程序的输出为15。
( ) 22. 如果将 `dp[i-1]` 改为 `dp[i-3]`,程序可能会产生编译错误。
( ) 23. (2分) 程序总是输出 `cost` 数组中的最小的元素。
**单选题**
( ) 24. 当输入的 `cost` 数组为 `{1, 100, 1, 1, 1, 100, 1, 1, 100, 1}` 时,程序的输出为
A. 6 B. 7 C. 8 D. 9
( ) 25. (4分) 如果输入的 `cost` 数组为 `{10, 15, 30, 5, 5, 10, 20}`,程序的输出为
A. 25 B. 30 C. 35 D. 40
( ) 26. 若将代码中的 `min(dp[i - 1], dp[i - 2]) + cost[i - 1]` 修改为 `dp[i - 1] + cost[i - 2]`,输入 `cost` 数组为 `{5, 10, 15}` 时,程序的输出为
A. 10 B. 15 C. 20 D. 25
**(3)**
```cpp
1 #include <iostream>
2 #include <cmath>
3 using namespace std;
4
5 int customFunction(int a, int b) {
6 if (b == 0) {
7 return a;
8 }
9 return a + customFunction(a, b - 1);
10 }
11
12 int main() {
13 int x, y;
14 cin >> x >> y;
15 int result = customFunction(x, y);
16 cout << pow(result, 2) << endl;
17 return 0;
18 }
```
**判断题**
( ) 27. 当输入为“2 3”时,`customFunction(2, 3)` 的返回值为“64”。
( ) 28. 当 `b` 为负数时,`customFunction(a, b)` 会陷入无限递归。
( ) 29. 当 `b` 的值越大,程序的运行时间越长。
**单选题**
( ) 30. 当输入为“5 4”时,`customFunction(5, 4)` 的返回值为
A. 5 B. 25 C. 250 D. 625
( ) 31. 如果输入 `x=3` 和 `y=3`,则程序的最终输出为
A. 27 B. 81 C. 144 D. 256
( ) 32. (4分) 若将 `customFunction` 函数改为 `return a + customFunction(a - 1, b - 1);` 并输入“3 3”,则程序的最终输出为
A. 9 B. 16 C. 25 D. 36
---
#### **三、完善程序 (程序输入不超过数组或字符串定义的范围; 每题 3 分,共计 30 分)**
**(1) (判断平方数)问题:** 给定一个正整数n,判断这个数是不是完全平方数,即存在一个正整数x,使得x的平方等于n。试补全程序。
```cpp
#include <iostream>
#include <cmath>
using namespace std;
bool isSquare(int num) {
int i = __(1)__;
int bound = __(2)__;
for (; i <= bound; ++i) {
if (__(3)__) {
return __(4)__;
}
}
return __(5)__;
}
int main() {
int n;
cin >> n;
if (isSquare(n)) {
cout << n << " is a Square number" << endl;
} else {
cout << n << " is not a Square number" << endl;
}
}
```
( ) 33. (1)处应填
A. 1 B. 2 C. 3 D. 4
( ) 34. (2)处应填
A. `(int)floor(sqrt(num) - 1)`
B. `(int)floor(sqrt(num))`
C. `floor(sqrt(num / 2)) - 1`
D. `floor(sqrt(num / 2))`
( ) 35. (3)处应填
A. `num == 2 * i` B. `num == 2 + i`
C. `num == i * i` D. `num == i`
( ) 36. (4)处应填
A. `num == 2 + i` B. `num == 2 * i` C. `true` D. `false`
( ) 37. (5)处应填
A. `num == i * i` B. `num != 2 * i` C. `true` D. `false`
**(2) (汉诺塔问题)** 给定三根柱子,分别标记为A、B和C。初始状态下,柱子A上有若干个圆盘,这些圆盘从上到下按从小到大的顺序排列。任务是将这些圆盘全部移到柱子C上,且必须保持原有顺序不变。在移动过程中,需要遵守以下规则:
1. 只能从一根柱子的顶部取出圆盘,并将其放入另一根柱子的顶部。
2. 每次只能移动一个圆盘。
3. 小圆盘必须始终在大圆盘之上。
试补全程序。
```cpp
#include <iostream>
using namespace std;
void move(char src, char tgt) {
cout << "从柱子" << src << "挪到柱子" << tgt << endl;
}
void dfs(int i, char src, char tmp, char tgt) {
if (i == __(1)__) {
move(__(2)__);
return;
}
dfs(i - 1, __(3)__);
move(src, tgt);
dfs(__(4)__, __(5)__);
}
int main() {
int n;
cin >> n;
dfs(n, 'A', 'B', 'C');
}
```
( ) 38. (1)处应填
A. 0 B. 1 C. 2 D. 3
( ) 39. (2)处应填
A. `src, tmp` B. `src, tgt` C. `tmp, tgt` D. `tgt, tmp`
( ) 40. (3)处应填
A. `src, tmp, tgt` B. `src, tgt, tmp` C. `tgt, tmp, src` D. `tgt, src, tmp`
( ) 41. (4)处应填
A. `i` B. `i - 1` C. `tmp` D. `src`
( ) 42. (5)处应填
A. `src, tmp, tgt` B. `tmp, src, tgt` C. `src, tgt, tmp` D. `tgt, src, tmp`
@@ -0,0 +1,903 @@
# CSP-J 2026 第一轮试卷:参考答案与详细解析
> 根据用户提供的《CSP-J-2026-试卷与答案.pdf》整理,题号及选项对应该文件。本文对全部 43 题逐题推导,所得答案与文件卷末参考答案一致。判断题中 A 表示正确,B 表示错误。本文沿用所提供文件的标题,不另行认定其官方来源。
## 一、答案速查
| 题号 | 答案 | 题号 | 答案 | 题号 | 答案 |
|---|---|---|---|---|---|
| 1 | B | 16 | A(正确) | 31 | B |
| 2 | D | 17 | B(错误) | 32 | D |
| 3 | C | 18 | A(正确) | 33 | C |
| 4 | C | 19 | A | 34 | D |
| 5 | B | 20 | C | 35 | B |
| 6 | D | 21 | C | 36 | D |
| 7 | D | 22 | A(正确) | 37 | B |
| 8 | C | 23 | B(错误) | 38 | C |
| 9 | B | 24 | B(错误) | 39 | B |
| 10 | A | 25 | B | 40 | D |
| 11 | A | 26 | A | 41 | C |
| 12 | D | 27 | C | 42 | A |
| 13 | C | 28 | B(错误) | 43 | D |
| 14 | A | 29 | A(正确) | | |
| 15 | B | 30 | A(正确) | | |
分值:第 1~15 题每题 2 分;第 16 题 1 分;第 17、18、22、23、24、28、29、30 题每题 1.5 分;其余每题 3 分,共 100 分。
## 二、单项选择题(第 1~15 题)
### 第 1 题:精确存储大整数
**答案:B,`long long`。**
需要精确存储的整数是 $10^{18}+1$。
按竞赛中通常采用的类型规格,32 位有符号 `int` 的最大值为 $2^{31}-1=2147483647$,无法容纳这个数;64 位有符号 `long long` 的最大值为 $2^{63}-1\approx9.22\times10^{18}$,能够精确存储。
`float` 和 `double` 虽然可以表示数量级很大的数,但有效精度有限。常见的 IEEE 754 `double` 有 53 位二进制有效精度,能够连续精确表示的非负整数范围只到 $2^{53}$。在 $10^{18}$ 附近,相邻可表示数的间距是 128,不能精确表示 $10^{18}+1$;`float` 的精度更低。
**易错点:表示范围大,不等于能精确存储范围内的每一个整数。**
### 第 2 题:十六进制转八进制
**答案:D,1365。**
先转为十进制:
$$
(2F5)_{16}=2\times16^2+15\times16+5=757。
$$
再检查:
$$
(1365)_8=1\times8^3+3\times8^2+6\times8+5=757。
$$
也可以利用二进制中转:每个十六进制位转成 4 位二进制,再从右向左按 3 位分组。
```text
2F5₁₆ = 0010 1111 0101₂
= 001 011 110 101₂
= 1 3 6 5₈
```
### 第 3 题:整数除法与取余
**答案:C,7。**
`a=7`、`b=3`,整数除法 `7/3=2`,取余 `7%3=1`。
```cpp
a / b * b + a % b
= 2 * 3 + 1
= 7
```
正整数除法满足:被除数 = 商 × 除数 + 余数。
### 第 4 题:判断合法出栈序列
**答案:C,3、1、2、4。**
栈遵循“后进先出”。要先弹出 3,必须先依次压入 1、2、3。弹出 3 后,栈中从底到顶是 1、2,此时必须先弹出 2,才能弹出 1,因此不可能紧接着输出 1。
其他选项均可实现:
- A:压入 1、2,弹出 2;压入 3、4,依次弹出 4、3、1。
- B:每压入一个数就立即弹出。
- D:压入并弹出 1;压入 2、3、4,再依次弹出 4、3、2。
### 第 5 题:完全二叉树的叶子数
**答案:B,50。**
将完全二叉树的结点按层序编号为 1~100。编号为 $i$ 的结点,其左孩子编号为 $2i$。
当 $2i\le100$ 时有孩子,即编号 1~50 的结点不是叶子;编号 51~100 的结点都是叶子,共 50 个。
一般地,含 $N$ 个结点的完全二叉树叶子数为:
$$
N-\left\lfloor\frac N2\right\rfloor=\left\lceil\frac N2\right\rceil。
$$
### 第 6 题:3 或 5 的倍数之和
**答案:D,2418。**
条件中的 `||` 表示满足任意一个条件即可,因此应求 1~100 中所有 3 或 5 的倍数之和。
3 的倍数之和:
$$
3(1+2+\cdots+33)=3\times\frac{33\times34}{2}=1683。
$$
5 的倍数之和:
$$
5(1+2+\cdots+20)=1050。
$$
15 的倍数被重复计算,需要减去:
$$
15(1+2+\cdots+6)=315。
$$
最终结果为 $1683+1050-315=2418$。这就是容斥原理。
### 第 7 题:每次上 1、2、3 级台阶
**答案:D,81。**
设 $f(i)$ 为到达第 $i$ 级台阶的方法数。按照最后一步走了几级分类:
$$
f(i)=f(i-1)+f(i-2)+f(i-3)。
$$
边界为 $f(0)=1$,表示不走也是到达地面的一种方式;也可以直接从 $f(1)=1,f(2)=2,f(3)=4$ 开始推。
| 台阶数 $i$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 方法数 $f(i)$ | 1 | 1 | 2 | 4 | 7 | 13 | 24 | 44 | 81 |
### 第 8 题:BFS 的入队次序
**答案:C,14 个。**
坐标按 `(行,列)` 表示,起点为 `(0,0)`,终点为 `(3,4)`。必须同时遵守“队首先出”和邻居按“上、下、左、右”检查。
首次入队的完整顺序,直到 E 入队为止:
| 入队编号 | 格子坐标 | 由哪个格子发现 |
|---|---|---|
| 1 | (0,0),S | 初始入队 |
| 2 | (1,0) | (0,0) |
| 3 | (0,1) | (0,0) |
| 4 | (2,0) | (1,0) |
| 5 | (1,1) | (1,0) |
| 6 | (0,2) | (0,1) |
| 7 | (2,1) | (2,0) |
| 8 | (1,2) | (1,1) |
| 9 | (2,2) | (2,1) |
| 10 | (3,2) | (2,2) |
| 11 | (4,2) | (3,2) |
| 12 | (3,3) | (3,2) |
| 13 | (4,1) | (4,2) |
| 14 | (3,4),E | (3,3) |
关键细节:处理 `(3,2)` 时,先发现下面的 `(4,2)`,再发现右边的 `(3,3)`。所以在 `(3,3)` 发现 E 之前,`(4,2)` 已经出队,并使 `(4,1)` 入队。
**易错点:题目统计累计入队过多少格,不是当前队列长度,也不是最短路径上的格子数。**
### 第 9 题:最大公约数条件
**答案:B,6 个。**
因为 $\gcd(n,60)=6$,所以令 $n=6t$,则:
$$
\gcd(6t,60)=6\gcd(t,10)=6,
$$
即 $\gcd(t,10)=1$。由 $1\le n\le100$,可得 $1\le t\le16$。
在 1~16 中排除 2 或 5 的倍数,剩下:
$$
t=1,3,7,9,11,13。
$$
对应 $n=6,18,42,54,66,78$,共 6 个。
### 第 10 题:最少硬币数
**答案:A,3 枚。**
可以用 $4+4+1=9$,因此最多需要 3 枚。
1 枚显然不行;2 枚可能组成的金额为 $2,5,7,8,10,12$,没有 9,因此最少为 3 枚。
**易错点:每次选最大面值的贪心策略在这里不成立。**按该策略会选 $6+1+1+1$,需要 4 枚。
### 第 11 题:指针与数组下标
**答案:A,`14,13`。**
初始数组为 `[1,3,5,7,9]`,`p=a+2`,即 `p` 指向 `a[2]`。
第一条赋值:
```cpp
*(p - 1) = p[0] + p[2];
```
等价于 `a[1]=a[2]+a[4]=5+9=14`。
第二条赋值:
```cpp
p[1] = *(a + 1) - a[0];
```
等价于 `a[3]=a[1]-a[0]=14-1=13`。注意这里的 `a[1]` 已被上一条语句修改。
所以输出 `14,13`。
### 第 12 题:二分查找的最坏比较次数
**答案:D,10 次。**
每比较一个中间元素,就将候选区间缩小到大约原来的一半。按照题目通常的计数方式,一次与中间元素的比较算一次:
$$
\lfloor\log_2 1000\rfloor+1=10。
$$
也可以观察最坏情况下的非空候选区间长度:
```text
1000 → 500 → 250 → 125 → 62 → 31 → 15 → 7 → 3 → 1
```
共需要检查 10 个中间元素。注意题目不是统计具体代码里 `==`、`<` 等运算符的执行总次数。
### 第 13 题:由前缀和求原数组
**答案:C,58。**
已知 $S[i]=3i^2+i$,利用:
$$
a[i]=S[i]-S[i-1]。
$$
因此:
$$
a[10]=(3\times10^2+10)-(3\times9^2+9)=310-252=58。
$$
不要把 `S[10]` 直接当成 `a[10]`:前者是前十项之和。
### 第 14 题:到所有点的距离和最小
**答案:A,37。**
数轴上到各点距离之和,在中位数处取得最小值。七个点已经按从小到大排序,其中位数为第 4 个数 7。
取 $P=7$:
$$
6+4+3+0+3+8+13=37。
$$
也可以配对证明:对 `(1,20)`、`(3,15)`、`(4,10)`,距离和分别至少为 19、12、6;`P=7` 同时达到这三个下界,且到中间点 7 的距离为 0,所以最小值为 $19+12+6=37$。
### 第 15 题:度数和与边数
**答案:B,18。**
无向图中每条边给两个端点各贡献 1 的度数,因此度数总和等于边数的 2 倍。
$$
2|E|=4\times3+(10-4)\times4=36,\qquad |E|=18。
$$
## 三、阅读程序(第 16~33 题)
### 第一组:除以 2 与二进制数位(第 16~21 题)
#### 程序整体含义
初始 `x=1,y=1`。每轮循环无论奇偶,`x` 都增加 1;只有当前 `n` 为奇数时,`y` 才增加 1。随后执行 `n/=2`。
对非负整数而言,反复执行 `n%2`、`n/=2`,相当于从低位到高位依次读取二进制数位。
- `x` = 1 + 循环次数。对正数,这就是 1 + 二进制位数。
- `y` = 1 + 原数二进制中 1 的个数。
- 输入 0 时不进入循环,输出 `1 1`。
#### 第 16 题
**答案:A,正确。**
输入 3,二进制为 `11`。
| 时刻 | 本轮开始的 n | 操作后的 x | 操作后的 y | n/=2 后 |
|---|---|---|---|---|
| 初始 | 3 | 1 | 1 | — |
| 第 1 轮 | 3,奇数 | 2 | 2 | 1 |
| 第 2 轮 | 1,奇数 | 3 | 3 | 0 |
最终输出 `3 3`。
#### 第 17 题
**答案:B,错误。**
删除奇数分支中的 `++x` 后,`x` 只在处理二进制位 0 时增加,`y` 只在处理二进制位 1 时增加。0 的数量与 1 的数量不一定相等。
例如输入 `1`,唯一一轮是奇数分支,只增加 `y`,输出为 `1 2`,两个数不相等。
#### 第 18 题
**答案:A,正确。**
原程序每轮一定增加 `x`,增加 `y` 时也一定增加 `x`,而二者初始相等,所以始终有 `x>=y`。
从二进制角度看,1 的数量不可能超过总位数。输入 0 时二者均为 1,结论也成立。
#### 第 19 题
**答案:A,陷入死循环。**
当条件改为 `while(n>=0)`,`n` 变成 0 后仍满足循环条件,而 `0/2` 永远是 0,无法退出。
按题目算法层面的讨论,应选死循环。严格按 C++ 语言语义,若无限运行使有符号变量 `x` 溢出,还会涉及未定义行为,但这不改变本题要求识别的循环条件错误。
#### 第 20 题
**答案:C,`4 3`。**
输入 6,二进制为 `110`,共有 3 位,其中有两个 1。所以 `x=1+3=4`,`y=1+2=3`。
逐轮也可得到:
```text
初始:n=6,x=1,y=1
第1轮:6为偶数,x=2,y=1,n=3
第2轮:3为奇数,x=3,y=2,n=1
第3轮:1为奇数,x=4,y=3,n=0
```
#### 第 21 题
**答案:C,31 次。**
第二个数恰好为 2,要求原数二进制中恰好有一个 1,即原数为 2 的非负整数次幂。
在 $0\le n\le2^{31}-1$ 范围内,这些数是:
$$
2^0,2^1,\ldots,2^{30},
$$
共 31 个。0 的输出第二个数为 1,不符合条件;$2^{31}$ 超出范围。
### 第二组:高精度加法及输出细节(第 22~27 题)
#### 程序整体含义
输入字符串逆序保存到数组中:`a[0]`、`b[0]` 是个位,`a[1]`、`b[1]` 是十位,以此类推。数组为全局变量,未赋值的位置初始为 0。
令 `L=max(a_len,b_len)`。程序从 `i=0` 算到 `i=L`,多计算一位以容纳最高位进位,然后固定输出 `c[L]` 到 `c[0]`。
**原程序总是输出 L+1 个字符,不会主动去除前导零。**正常加法中每个 `c[i]` 都在 0~9 之间。
#### 第 22 题
**答案:A,正确。**
`123+456=579`,但 `L=3`,程序输出 `c[3],c[2],c[1],c[0]`。最高位 `c[3]=0`,所以输出 `0579`。
#### 第 23 题
**答案:B,错误。**
即使输入没有前导零,只要最高位没有产生进位,多输出的那一位仍然是 0。例如 `123 456` 的输出就是 `0579`,足以否定“一定不会含有前导零”。
#### 第 24 题
**答案:B,错误。**
删除 `+carry[i]` 会使进位无法参与下一位的计算,但结果不一定严格变小。
例如输入 `1 2`,本来就没有进位,修改前后都输出 `03`,结果相同。因此“一定比原来的结果小”不成立。
#### 第 25 题
**答案:B,`013023`。**
计算 $12345+678=13023$,原输入最大长度为 5,所以输出 6 位,最前面补出一个 0。
| 数位 | 两个数的本位及进位 | 本位结果 | 向高位进位 |
|---|---|---|---|
| 个位 | 5+8+0=13 | 3 | 1 |
| 十位 | 4+7+1=12 | 2 | 1 |
| 百位 | 3+6+1=10 | 0 | 1 |
| 千位 | 2+0+1=3 | 3 | 0 |
| 万位 | 1+0+0=1 | 1 | 0 |
| 额外最高位 | 0+0+0=0 | 0 | 0 |
从高位到低位输出为 `013023`。
#### 第 26 题
**答案:A,`01010`。**
将进位条件改为 `c[i]>10` 后,恰好等于 10 时不会进位,也不会减去 10。
对输入 `95 15`:
| i | 计算结果 | 是否大于 10 | 最终 c[i] | 下一位收到的进位 |
|---|---|---|---|---|
| 0 | 5+5+0=10 | 否 | 10 | 0 |
| 1 | 9+1+0=10 | 否 | 10 | 0 |
| 2 | 0+0+0=0 | 否 | 0 | 0 |
输出顺序是 `0`、`10`、`10`,直接连接后为 `01010`。
**易错点:数组元素是整数,不保证始终只有一位。`cout << 10` 会输出两个字符。**
#### 第 27 题
**答案:C,长度一定为 n+1,且第一个字符为 `'0'`。**
两个输入均为 n 位数,所以 `L=n`,原程序固定输出 n+1 位。由于和小于 $10^n$,不存在进入第 n 位的进位,因此 `c[n]=0`,输出的第一个字符一定为 `'0'`。
### 第三组:质数判断与深度优先搜索(第 28~33 题)
#### 程序整体含义
`check_prime(x)` 判断 x 是否为质数。若 x 是合数,则一定有一个不超过 $\sqrt{x}$ 的因子,因此试除到 `i*i<=x` 即可。
`search_result(x)` 的逻辑为:
1. 如果 x 不是质数,立即停止这条搜索分支。
2. 如果 x 是质数且 `x>=n`,输出 x,并停止向后添加数字。
3. 如果 x 是质数且 `x<n`,依次尝试在其末尾添加 0~9。
所以只有每个非空十进制前缀都是质数的数,才能到达输出位置。遍历方式是深度优先搜索,并不按数值大小排序。
#### 第 28 题
**答案:B,错误。**
输入 10 时,只有起始数字 2、3、5、7 可以继续扩展。一旦得到两位质数,就达到阈值并输出。
全部输出为:
```text
23
29
31
37
53
59
71
73
79
```
共 9 行,不是 10 行。比如 11 虽然是质数,但首位 1 不是质数,对应分支早已被剪去。
#### 第 29 题
**答案:A,正确。**
主函数一定调用 `search_result(5)`。5 是质数,且当 `n<=5` 时满足 `5>=n`,所以该调用会直接输出 5。
#### 第 30 题
**答案:A,正确。**
把添加数字范围改为 `1,3,5,7,9`,只是跳过了末位为偶数的扩展。
扩展得到的数至少有两位,如果末位是 0、2、4、6、8,则它是大于 2 的偶数,必定不是质数。在原程序中,这些分支也会立即返回,既不会输出,也不会继续扩展。
因此删除这些无效分支后,输出内容和顺序都不变。
#### 第 31 题
**答案:B,29。**
输入 24 时,先从首位 2 开始搜索:
- 20、21、22 不是质数,返回。
- 23 是质数,但小于 24,所以继续扩展。
- 230~239 中只有 233、239 是质数,依次输出这两个数。
- 返回两位数这一层,24~28 均非质数,随后访问 29。
- 29 是质数且不小于 24,因此第 3 行输出 29。
前几行是:
```text
233
239
29
31
37
...
```
**易错点:深度优先会先完成 23 的整条分支,再访问 29。**
#### 第 32 题
**答案:D。**
对于输出的任意一个至少两位的数 x,它必然由上一层 `search_result(x/10)` 添加末位得到。上一层只有通过质数检查,才能继续递归。因此删去 x 的末位后得到的数必定是质数。
其余选项:
- A 错:输入 24 时,233、239 出现在 29 前面,并非从小到大。
- B 错:输入 23 时输出 9 行,输入 24 时输出 10 行;原来输出的 23 被两个后代 233、239 替代,行数增加。
- C 错:例如输入 10 时会输出 29、31、59、71、79,个位并不只可能是 3 或 7。
#### 第 33 题
**答案:C,14 行。**
输入 200 时,一位和两位的质数前缀都小于阈值。可继续扩展的两位前缀就是第 28 题列出的 9 个数。
| 两位质数前缀 | 添加一位后仍为质数的数 | 数量 |
|---|---|---|
| 23 | 233、239 | 2 |
| 29 | 293 | 1 |
| 31 | 311、313、317 | 3 |
| 37 | 373、379 | 2 |
| 53 | 无 | 0 |
| 59 | 593、599 | 2 |
| 71 | 719 | 1 |
| 73 | 733、739 | 2 |
| 79 | 797 | 1 |
这些三位数均不小于 200,所以输出后停止扩展。总行数为 $2+1+3+2+0+2+1+2+1=14$。
## 四、完善程序(第 34~43 题)
### 第一组:将 mn 进制转换成 n 进制(第 34~38 题)
#### 1. 数组究竟表示什么
这里 $mn$ 表示 $m\times n$。数组 b 以低位在前的顺序,保存已读入部分所代表数值的 n 进制表示:
$$
V=b[0]+b[1]n+b[2]n^2+\cdots+b[len-1]n^{len-1}。
$$
`b[0]` 是个位,`b[len-1]` 是最高位。每轮进位结束后,各位都在 0~n-1 之间;进位前可以暂时超出这个范围。
#### 2. 每读入一位,要执行哪种运算
从高到低读入原数的新数位 x,相当于:
$$
V_{新}=V_{旧}\times(mn)+x。
$$
这与读十进制数时“旧数乘 10 再加新数字”是同一个道理。**当前用 n 进制保存数值,不会改变原输入的进制是 mn 这一事实。**
#### 第 34 题:①
**答案:D,`b[j - 1] * m`。**
```cpp
for (int j = len; j >= 1; j--)
b[j] = b[j - 1] * m;
```
这行同时做了两件事:将数字从第 j-1 位放到第 j 位,并将数字本身乘以 m。
假设旧数位是 c,它原来代表 $c n^{j-1}$,现在代表:
$$
(cm)n^j=(c n^{j-1})\times mn。
$$
**为什么不能再写 `* n`?**因为位置从 j-1 变成 j,位权已经从 $n^{j-1}$ 变成 $n^j$,相当于自动乘了一次 n。就像十进制的 3 从个位移到十位,数字仍为 3,但代表的数值已经是 30。
若写成 `b[j]=b[j-1]*m*n`,结合位置变化,实际乘的是 $mn^2$,多乘了一次 n。
**为什么倒序移动?**从高位向低位赋值,能够保留还没被读取的旧数位;若从低位往高位操作,会提前覆盖后面需要使用的数据。
#### 第 35 题:②
**答案:B,`x`。**
整体高移一位后,最低位空出,相当于为 0。将新读入的 x 加到最低位,直接写:
```cpp
b[0] = x;
```
x 可能大于等于 n,但后面的循环会统一处理进位。
#### 第 36 题:③
**答案:D,`b[j] / n`。**
n 进制中,每满 n 就向高一位进 1,所以需要向高一位增加 `b[j]/n`:
```cpp
b[j + 1] += b[j] / n;
```
必须使用 `+=`,因为高一位可能已有数值,进位应该在原有值上累加。
#### 第 37 题:④
**答案:B,`b[j] % n`。**
进位后,本位留下除以 n 的余数:
```cpp
b[j] = b[j] % n;
```
商送到高位,余数留在本位。进位循环由低位向高位进行,能继续处理进位引起的连锁变化。
如果向当前最高位之外又进了一位,`len++` 会让循环继续处理这个新数位。
#### 第 38 题:⑤
**答案:C,`len > 1 && b[len - 1] == 0`。**
```cpp
while (len > 1 && b[len - 1] == 0)
len--;
```
每次读入都先增加 len,但实际数值未必需要这么多位,因此最后需要删除最高位多余的零。
- 应检查 `b[len-1]`,因为它是最高位;`b[0]` 是最低位,末尾的零不能随意删除。
- 必须限制 `len>1`,保证 A=0 时仍保留一位 0。如果使用 `len>0`,可能把唯一的一位零也删掉,导致没有输出。
#### 3. 完整示例
取 n=2、m=3,输入的原数是六进制 `1 4`:
$$
(14)_6=1\times6+4=10=(1010)_2。
$$
读完首位 1 后,有效数位是 `[1]`。实际数组可能还带有最高位的零,这些零不影响数值,最后会统一去除。
读入下一位 4,移动并乘以 m,再写入最低位,得到尚未进位的有效部分 `[4,3]`,表示 $4+3\times2=10$。
```text
[4,3] 第0位为4,向高位进2,本位留0
[0,5] 第1位为5,向高位进2,本位留1
[0,1,2] 第2位为2,向高位进1,本位留0
[0,1,0,1] 所有数位均合法
```
数组低位在前,倒序输出就是 `1 0 1 0`。
#### 4. 补全后的完整程序
```cpp
#include <iostream>
constexpr int N = 100005;
long long b[N];
int main() {
long long n, m, d;
std::cin >> n >> m >> d;
int len = 1;
for (int i = 0; i < d; i++) {
long long x;
std::cin >> x;
for (int j = len; j >= 1; j--)
b[j] = b[j - 1] * m;
b[0] = x;
len++;
for (int j = 0; j < len; j++) {
if (b[j] >= n) {
b[j + 1] += b[j] / n;
b[j] %= n;
if (j + 1 == len) len++;
}
}
}
while (len > 1 && b[len - 1] == 0) len--;
for (int i = len - 1; i >= 0; i--)
std::cout << b[i] << ' ';
return 0;
}
```
理解程序时,抓住“读入一位 → 旧数乘 mn 并加 x → 按 n 进制整理进位”。题干虽然称为“逐位除以 n”,但整个读入过程用上述递推式来解释更清楚。
### 第二组:枚举连续分段,使平均值极差最小(第 39~43 题)
#### 1. 题意与递归状态
字符串中的每个十六进制字符分别代表一个整数。例如 `016A` 对应 `[0,1,6,10]`,不是把整串当成一个十六进制整数参与运算。
在 n-1 个内部间隙中选择至少一个切分位置,将序列分成至少两段。对每段求平均值,目标是让“最大段平均值 - 最小段平均值”尽可能小。
函数:
```cpp
split(int l, int cnt, double mnb, double mxb)
```
各参数含义如下:
| 参数 | 含义 |
|---|---|
| l | 下一段从第 l 个数开始 |
| cnt | 已选的内部切分位置数量,即切了几刀 |
| mnb | 已确定各段平均值的最小值 |
| mxb | 已确定各段平均值的最大值 |
每次枚举当前段的右端点 r,当前段为 `[l,r]`,然后递归处理从 r+1 开始的剩余部分。
当 `l>n` 时,所有数已经分完。只有 `cnt>0` 才满足至少切一刀的要求;此时用 `mxb-mnb` 更新最优答案。
#### 第 39 题:①,十六进制字符转数值
**答案:B,`c - (c < 'A' ? '0' : 'A' - 10)`。**
在题设 ASCII 编码和字符范围下:
- `'0'`~`'9'` 均小于 `'A'`,用 `c-'0'` 得到 0~9。
- `'A'`~`'F'` 用 `c-'A'+10` 得到 10~15。
选项 B 中,字母分支是:
```cpp
c - ('A' - 10) // 等价于 c - 'A' + 10
```
例如 `'A'` 得到 10,`'F'` 得到 15。
其他选项:A 使用 `c<'9'`,会把字符 `'9'` 错分到字母分支;C 把两个分支颠倒;D 将字母分支中的加 10 错写成减 10。
#### 第 40 题:②,枚举当前段的右端点
**答案:D,`int r = l; r <= n; ++r`。**
段 `[l,r]` 至少包含一个元素,因此最小右端点为 l;最后一段必须能包含第 n 个元素,所以最大右端点为 n。每个位置都可能是右端点,必须逐个枚举。
- A 从 l+1 开始,会漏掉长度为 1 的段。
- B 限制 r<n,无法正常选出结束于 n 的最后一段。
- C 每次增加 2,会遗漏部分段长。
循环中的 `sum+=a[r]` 不断扩展当前段,使 sum 始终等于 `a[l]+...+a[r]`。
#### 第 41 题:③,正确计算平均值
**答案:C,`sum * 1.0 / (r - l + 1)`。**
区间 `[l,r]` 共包含 `r-l+1` 个元素,平均值应为总和除以元素个数。
`sum*1.0` 先将运算转换为浮点运算,避免整数除法丢失小数部分。
例如总和为 3、长度为 2:
```cpp
3 / 2 * 1.0 // 先进行整数除法,结果为 1.0
3 * 1.0 / 2 // 进行浮点除法,结果为 1.5
```
因此 A 错。B 的分母和运算位置错误,且 r=l 时分母为 0;D 少算了当前段最后一个元素。
#### 第 42 题:④,递归参数更新
**答案:A。**
```cpp
r + 1, cnt + (r < n), min(mnb, nwb), max(mxb, nwb)
```
四个参数分别解释如下:
1. 下一段从 r+1 开始,保证不重叠、不遗漏。
2. 只有 r<n 时,r 后面才是一个内部切分位置,才应增加 cnt。
3. 新的最小平均值是 `min(mnb,nwb)`。
4. 新的最大平均值是 `max(mxb,nwb)`。
**为什么不能用 `cnt+(r<=n)`?**循环内总有 r<=n,这会连整个序列末尾也算作一次切分。这样不切分的整段 `[1,n]` 也能通过 `cnt>0` 的检查,而唯一一段的最大和最小平均值相等,极差为 0,会让程序错误地对所有输入都得到 0。
例如输入 `2 09`,唯一合法方案是 `[0] | [9]`,正确极差为 9;不切分时极差为 0,但这不符合题意。
#### 第 43 题:⑤,递归初始状态
**答案:D,`1, 0, 1e100, -1e100`。**
```cpp
split(1, 0, 1e100, -1e100);
```
- 数组从下标 1 开始保存数据,所以 l=1。
- 尚未选择内部切分位置,所以 cnt=0。
- 最小值初始设为很大的数,这样遇到第一个平均值时,`min` 会选中它。
- 最大值初始设为很小的数,这样第一次 `max` 也会选中真实平均值。
实际平均值在 0~15 之间,所以这两个初值足以充当正、负无穷的替代值。
#### 2. 手算示例:016A
数值序列为 `[0,1,6,10]`,共有 3 个内部间隙,至少切一刀,所以合法方案共 $2^3-1=7$ 个。
| 分段方案 | 各段平均值 | 极差 |
|---|---|---|
| `[0] \| [1,6,10]` | 0、17/3 | 17/3 |
| `[0,1] \| [6,10]` | 1/2、8 | 15/2 |
| `[0,1,6] \| [10]` | 7/3、10 | 23/3 |
| `[0] \| [1] \| [6,10]` | 0、1、8 | 8 |
| `[0] \| [1,6] \| [10]` | 0、7/2、10 | 10 |
| `[0,1] \| [6] \| [10]` | 1/2、6、10 | 19/2 |
| `[0] \| [1] \| [6] \| [10]` | 0、1、6、10 | 10 |
最小极差为 $17/3$,保留六位小数输出 `5.666667`。
#### 3. 补全后的完整程序
```cpp
#include <algorithm>
#include <iomanip>
#include <iostream>
using namespace std;
constexpr int N = 25;
int n, a[N];
char s[N];
double ans = 1e100;
int value(char c) {
return c - (c < 'A' ? '0' : 'A' - 10);
}
void split(int l, int cnt, double mnb, double mxb) {
if (l > n) {
if (cnt == 0) return;
ans = min(ans, mxb - mnb);
return;
}
int sum = 0;
for (int r = l; r <= n; ++r) {
sum += a[r];
double nwb = sum * 1.0 / (r - l + 1);
split(r + 1, cnt + (r < n),
min(mnb, nwb), max(mxb, nwb));
}
}
int main() {
cin >> n >> (s + 1);
for (int i = 1; i <= n; ++i)
a[i] = value(s[i]);
split(1, 0, 1e100, -1e100);
cout << fixed << setprecision(6) << ans;
return 0;
}
```
代码按常见竞赛 C++14/C++17 环境书写;读取 `s+1` 的语句含义与原题一致。原文件在输出语句处结束了代码展示,这里补齐了 `return 0;` 和主函数结束括号。
#### 4. 为什么枚举不会漏解
任意一个合法连续分段方案,都唯一确定第一段右端点、第二段右端点,直到最后一段右端点 n。递归在每层遍历所有可能的右端点,因此每个方案都能被访问一次,也不会重复。
n-1 个内部间隙各有“切”或“不切”两种选择,共 $2^{n-1}$ 个完整方案,排除一刀不切后为 $2^{n-1}-1$ 个合法方案。代码通过累计 sum 和维护最值,使每条递归边的工作量为常数,总时间复杂度为 $O(2^n)$,递归栈空间为 $O(n)$。n≤20 时适合直接枚举。
## 五、复习重点
| 知识点 | 对应题号 | 需要掌握的结论或方法 |
|---|---|---|
| 整数范围与浮点精度 | 1 | 存得下某数量级,不代表能精确表示每个整数 |
| 进制与数位权重 | 2、16~21、34~38 | 除 2 读取二进制位;移动数位改变位权 |
| 栈、树、图 | 4、5、8、15 | 后进先出、完全二叉树编号、BFS 队列顺序、握手定理 |
| 递推与计数 | 6、7、9 | 容斥、按最后一步分类、最大公约数化简 |
| 最优性判断 | 10、14 | 贪心需要依据;距离和最小取中位数 |
| 数组与指针 | 11 | 指针偏移对应数组下标,注意赋值顺序 |
| 二分与前缀和 | 12、13 | 区间反复减半;原项等于相邻前缀和之差 |
| 高精度加法 | 22~27 | 低位先算、进位条件、固定输出长度 |
| DFS 与剪枝 | 28~33 | 区分递归访问顺序和数值顺序,跟踪提前返回 |
| 连续分段枚举 | 39~43 | 状态含义、内部切分计数、浮点除法、最值初值 |
做程序题时,先确定变量含义与循环不变量,再手算一个小例子;看到“一定”“所有”等断言时,优先检查 0、最小输入、无进位和边界恰好相等等情形。
@@ -0,0 +1,980 @@
# CSP-J 第二轮 核心知识讲义
## 一、要考试的范围有哪些
C++ 基础知识,基本的算法:枚举、模拟、排序、贪心、前缀和、差分、二分、搜索和动态规划、高精度运算,数据结构:栈、队列、树与常用数学知识。
## 二、C++ 基础知识
### 1. 数据类型与溢出
常见竞赛环境中的 32 位 `int` 最大值约为 21 亿,64 位 `long long` 最大值约为 $9.22\times10^{18}$。输入能装下,不代表计算结果也能装下。例如两个数都不超过十亿,各自能放进 `int`,相乘却可能达到 $10^{18}$。
```cpp
int a = 1000000000, b = 1000000000;
long long product = 1LL * a * b; // 或者(long long)a * b
```
这里的 `1LL` 是 `long long` 类型的整数 1,让右侧乘法从一开始就按 `long long` 计算。如果只写 `long long product = a * b;`,右侧可能先按 `int` 运算而溢出,再把错误结果赋给左侧变量。左侧类型再大,也无法补回已经丢失的结果。
累加时也要这样估计。十万个数,每个最多十亿,总和可达 $10^{14}$,因此答案和保存中间结果的前缀和数组,都需要足够大的类型。
整数类型要防溢出,`double` 则要留意近似误差。计数、循环次数,以及能换成整数单位的金额,尽量用整数处理。比较两个近似值时,可以用 `if(abs(a - b) < eps)` 判断它们是否足够接近。`eps` 是允许误差,例如 `1e-5`,具体取多大要看题目要求和数值范围。
### 2. 整数除法
两个整数相除,结果会向 0 截断,例如 `5 / 2` 得到 2。需要小数时,让其中一个数先变成浮点数:
```cpp
double x = 1 / 2; // 结果为 0
double y = 1.0 / 2; // 结果为 0.5
```
对非负整数 a 和正整数 b,向上取整常用下面两种方式:
```cpp
long long x = (a + b - 1) / b; // 整数方式,保证加法不溢出
long long y = ceil(1.0 * a / b); // 浮点方式,适用于数值和精度允许时
```
`ceil` 会把结果向上取整,a、b 都是整数时,要先乘 `1.0` 让除法产生小数;直接写 `ceil(a / b)`,里面已经完成了整数除法。如果 a 或 b 本来就是浮点数,才可以直接写 `ceil(a / b)`。
### 3. 条件与运算符
比较两个值时,用 `==` 判断相等,`!=` 判断不相等,大小比较使用 `<`、`<=`、`>`、`>=`。其中最容易写错的是等号:`=` 用来赋值,判断相等要写两个等号。
多个条件可以用 `&&` 表示“都成立”,用 `||` 表示“至少一个成立”,用 `!` 表示“取反”。例如判断 x 是否在闭区间内,要写 `l <= x && x <= r`,不能写成连续比较 `l <= x <= r`。
访问数组时,把边界判断放在前面:
```cpp
if (pos >= 0 && pos < n && a[pos] == target) {
// pos 合法,并且找到了目标
}
```
这样写,`&&` 前面的条件一旦不成立,后面就不再计算。下标越界时,程序不会继续访问这个数组位置。
### 4. 数组与 vector
数组通过下标访问元素。声明 `int a[100] = {};`,就建立了 100 个初值为 0 的位置,合法下标为 0~99。采用 1~n 编号时,至少开 n+1 个位置。
需要随时添加或删除末尾元素时,可以使用长度能够调整的数组 `vector`。常见的建立方式如下:
```cpp
vector<int> a; // 空容器,还没有元素
vector<int> b(5); // 5 个元素,初值都是 0
vector<int> c(5, -1); // 5 个元素,初值都是 -1
```
这几种写法建立出的长度不同:`b` 有 5 个元素,可以访问下标 0~4;`a` 还是空的,要先添加元素,才能访问 `a[0]`。添加、删除和查询长度时,常用下面这些操作:
| 操作 | 写法 | 含义 |
| --- | --- | --- |
| 在末尾添加 | `v.push_back(x)` | 新增一个值为 x 的元素,长度增加 1 |
| 按下标访问 | `v[i]` | 读取或修改已有的第 i 项,下标从 0 开始 |
| 访问最后一个元素 | `v.back()` | 读取或修改最后一个元素,容器必须非空 |
| 删除最后一个元素 | `v.pop_back()` | 长度减少 1,不返回被删除的元素,容器必须非空 |
| 查询长度 | `v.size()` | 得到当前元素个数 |
| 判断是否为空 | `v.empty()` | 没有元素时返回真 |
| 删除全部元素 | `v.clear()` | 长度变成 0,原来的下标都不能再使用 |
例如从空容器开始,依次 `push_back(2)`、`push_back(4)`,内容就变成 `{2,4}`;再执行 `pop_back()`,就只剩 `{2}`。
读入 n 个数,可以先建立 n 个元素,再按下标读入:
```cpp
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
```
读入后,就可以按下标逐个访问。循环中用 `size()` 取得当前长度:
```cpp
for (int i = 0; i < a.size(); i++) {
cout << a[i] << ' ';
}
```
只需要依次查看元素时,也可以写 `for (int x : a)`;要排序整个容器,就用 `sort(a.begin(), a.end())`。注意读入时要分清“放进已有位置”和“在末尾追加”:如果初始化为 `vector<int> a(n)` 代表着已经有 n 个元素,再用 `push_back` 添加 n 个数,长度就会变成 2n。
### 5. 字符和字符串
字符串同样可以按下标逐个处理,不过取出来的是字符。数字字符 `'7'` 与整数 `7` 不同,已知字符 `c` 是数字时,用 `c - '0'` 才能得到对应的数值。
```cpp
string s;
cin >> s;
int n = s.size();
for (int i = 0; i < n; i++) {
// 处理 s[i]
}
```
字符串有效字符的下标为 0~n−1。`cin >> s` 读取一个不含空白的字符串;要读取含空格的一整行,就用 `getline(cin, s)`。
#### 例:读入一个整数和下一行文字
第一行给出整数 n,第二行是可能含空格的文字。在这种数字后面紧接换行的情况,就需要在 `cin >> n` 后加一次 `cin.ignore()`,再读下一行:
```cpp
int n;
string s;
cin >> n;
cin.ignore();
getline(cin, s);
```
之所以多这一步,是因为 `cin >> n` 读走数字后,会留下行末换行。`getline` 直接遇到它就结束读取,于是得到空串;`cin.ignore()` 默认丢弃一个字符,在这里正好取走换行。数字后若还有多个空格,可改用 `cin.ignore(INT_MAX, '\n');` 丢弃本行剩余内容。连续用 `getline` 读多行时,上一次已经取走了换行,不用再额外调用 `ignore()`。
除了读入方式,还要分清楚字符串的比较规则:字符串的比较是按字典序比较,在比较时,`"10"` 会排在 `"2"` 前面,和整数的大小顺序不同。
### 6. 循环与函数
`break` 退出最近的一层循环;`continue` 跳过本轮剩下的操作。对于 `for`,执行 `continue` 后仍会执行第三部分的更新;对于 `while`,则直接回到条件判断,写在后面的更新语句可能被跳过。
`return` 结束当前函数。在 `main` 中执行 `return 0;` 会结束整个程序,在搜索函数里执行它则只是返回上一层调用。
如果函数里改过一个值,回到调用处却没变,可以先查传参方式。普通的值参数保存的是副本,通过引用参数才能直接修改对应的原变量。递归时也一样,每一层的局部变量各自独立,全局数组则由各层共用,修改后是否恢复要看它的用途。
自增自减运算放进一个表达式时,`i++` 代表的先用后加,`++i` 代表的先加后用。
### 7. 输入输出
使用 `cin`、`cout` 时,可以在正式读写前设置:
```cpp
ios::sync_with_stdio(false);
cin.tie(nullptr);
```
关闭同步后,读取数据的速率会大大加快,但是就要统一使用 `cin/cout` 来输入输出,避免与 `scanf`、`printf` 交错使用。一般换行用 `'\n'`,不必每次都用会刷新缓冲区的 `endl`。
对于复赛的题目,明确要求了要使用文件输入输出,就要按题面给出的文件名设置,例如:
```cpp
freopen("task.in", "r", stdin);
freopen("task.out", "w", stdout);
```
这里的 `task` 是示意名称,必须替换成题面要求的名称,并放在正式读写之前。
## 三、通过数据范围推测算法
语法能写对之后,还要判断算法能否在规定时间内跑完。看到 n 的上限,可以先按下面的范围筛选。这里按 C++、约 1~2 秒、循环体只做简单操作来粗估;接近上限时,仍要计算实际工作量。大量输出、长字符串比较和多组数据,都会增加耗时。
| 方法 | 大致工作量 | 通常可考虑的 n 范围 |
| --- | --- | --- |
| 枚举所有子集 | $2^n$ | 通常先考虑 $n\le20$;每个子集还要遍历 n 项时,应按 $n2^n$ 再估算 |
| 枚举所有排列 | $n!$ | 通常先考虑 $n\le10$;若每个方案都要逐项处理或输出,范围还需缩小 |
| 三层各枚举 n 次 | $n^3$ | 通常先考虑 $n\le200$,此时约八百万次简单操作 |
| 枚举所有数对 | $n^2$ | 通常先考虑 $n\le3000$;循环体很简单且时间允许时,可再评估到约 5000 |
| 排序 | $n\log n$ | 普通整数排序通常可考虑 $n\le10^6$ |
| 一次遍历、前缀和 | $n$ | 简单线性处理通常可考虑 $n\le10^7$,还要确认输入量与内存是否允许 |
例如 n 的上限是十万,枚举所有数对会达到百亿量级,就要想办法用排序、前缀和等方法减少重复计算。若 n 只有 20,直接枚举各种选择也可能跑得完,指数级搜索就值得考虑。
估算时别漏掉外层调用:一次判断扫描 n 个数,二分又调用多次,两部分的工作量就要相乘。内存也要一起算,二维数组的总元素个数是两维相乘,两边看着都不大,乘起来也可能占用很多空间。
## 四、枚举与模拟
### 1. 枚举
数据范围允许时,可以把可能的情况逐个检查,这就是枚举。写循环前,先确定枚举的对象和范围,再把题目的合法条件放进去。
#### 例:统计和为目标值的数对
给定 n 个整数和目标值 target,统计有多少对不同位置的元素之和等于 target。同一对位置只算一次,交换两个位置的顺序不产生新方案。
这道题枚举的是两个位置。让第二个位置始终在第一个位置之后,就能排除同一个位置,也避免交换顺序后重复计算:
```cpp
long long ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (a[i] + a[j] == target) ans++;
}
}
```
代码中让 `j` 从 `i+1` 开始,因此 `(i,j)` 和 `(j,i)` 只会统计其中一对。
题目说“两个数”时,要分清是在选两个位置,还是选两个不同数值。相同数值出现在不同位置,是否可以同时选,要由题意决定。
对于暂时想不到优化方法得题目,可以先写出小范围暴力。之后优化时,用它核对结果;比赛中也可能拿到对应数据范围的分数,具体得分要看测试点或子任务如何组织。
### 2. 模拟
模拟题直接按照题目规则执行即可,但要注意题目得操作要求/顺序,看看应该先判断还是先修改,改过的值会不会影响本轮后面的判断。例如装物品不能超重,就应先检查加入后的总重量,再决定是否加入。
如果题目规定“同时发生变化”,就要先保留旧状态,再统一更新。否则边扫描边修改时,后面的位置可能读到本轮刚改过的新值,执行出来就和题意不同了。
举个简单例子:在一排格子中,每一轮让中间各格的数变成左右相邻两格的数之和,两端保持不变,所有格子同时变化。例如原来的数是:`1 2 3 4`。
如果直接从左往右修改原数组:
```cpp
for (int i = 1; i < n - 1; i++) {
a[i] = a[i - 1] + a[i + 1];
}
```
第二格先变成了 4。接下来计算第三格时,读到的第二格已经是新值,于是算成 `4 + 4 = 8`,最终得到:`1 4 8 4`
这就不符合“同时变化”的要求了。正确做法是用另一个数组保存本轮结果,**始终从旧数组读取,向新数组写入**,这样,计算第三格时读取的仍是旧值 2,不会受到第二格新值的影响。
## 五、排序与贪心
### 1. 排序
当原有顺序不影响答案时,可以考虑先排序,让大小关系变得清楚。常用的升序写法如下:
```cpp
sort(a + 1, a + n + 1); // 排序 a[1] 到 a[n]
sort(v.begin(), v.end()); // 排序整个 vector
```
`sort` 的范围是左闭右开:包含第一个参数指向的位置,不包含第二个参数指向的位置。要排序下标 1~n,就把右端写成 `a+n+1`;少写这个 `+1`,最后一个元素就没有参与排序。
#### 例:按成绩和编号排序
给定若干学生的成绩与编号,要求按成绩从高到低排列;成绩相同的,编号较小者在前。那么就要自己书写比较函数:
```cpp
struct Student { int score, id; };
bool cmp(const Student& a, const Student& b) {
if (a.score != b.score) return a.score > b.score;
return a.id < b.id;
}
sort(v.begin(), v.end(), cmp);
```
这个比较函数先看成绩,成绩相同才看编号。比较时使用严格的先后关系,不能写成 `a.score >= b.score`;两个元素完全相同时,比较结果应为假。
整数的取值范围很小时,也可以先用计数数组记录出现次数,再按数值顺序输出。但是开数组前要看值域有多大,值太大就不适合直接作为数组下标。
### 2. 贪心算法
有些题排序之后,还需要决定按什么规则选择,这时可以考虑贪心。它在每一步作出选择,因此需要说明:这一步选完,为什么仍能得到整体最优的结果?
#### 例:安排尽可能多的活动
给定若干活动的开始、结束时间,选择尽可能多的互不冲突活动,求最多能选几个。本例允许一个活动结束时,下一个活动立刻开始,且活动时长为正。
先按结束时间从小到大排序,每次选择当前能接上的活动。
结束得越早,留给后面的时间越多。把最优方案中第一个活动换成结束最早的可选活动,后面的活动仍然接得上;之后对剩余活动继续这样选择,就有了这个策略的依据。
```cpp
int count = 0, last = 0; // 此处约定区间起点均不小于 0
for (int i = 1; i <= n; i++) {
if (seg[i].l >= last) {
count++;
last = seg[i].r;
}
}
```
## 六、前缀和与差分
### 1. 前缀和
同一个数组上反复求区间和,每次从头相加会做很多重复工作。可以先记下前面各项的总和:令 `s[i]` 表示前 i 个数的和,`s[0]=0`,逐项累加得到 $s[i]=s[i-1]+a[i]$。
查询闭区间时,去掉左端点之前的部分:$\operatorname{sum}(l,r)=s[r]-s[l-1]$。
```cpp
vector<long long> s(n + 1, 0);
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
// 查询闭区间 [l,r]:s[r] - s[l - 1]
```
例如数组为 2、−1、4、3,要查询第 2~4 项的和。前缀和依次为 0、2、1、5、8,因此区间 `[2,4]` 的和是 `8-2=6`。去掉的是第 l 项之前的部分,所以减 `l-1` 对应的前缀和。
预处理扫描一遍,之后每次查询只做一次减法。它适合数组不变、查询很多的情况;原数组修改后,旧前缀和也可能失效。
### 2. 差分
如果题目要反复给一段数反复执行区间修改得操作,那么可以记录相邻元素的差,把整段修改变成两个位置的修改。这里从下标 0 开始:定义 `d[0]=a[0]`,对于 `i>=1`,令 `d[i]=a[i]-a[i-1]`。把这些差从头累加,就能还原原数组。
给闭区间 `[l,r]` 全部加 x,只改两处:
```cpp
d[l] += x;
d[r + 1] -= x;
```
还原时,增量从 `l` 处开始生效,到 `r+1` 处被抵消,所以中间整段都会增加 x。等全部修改完成后,再统一求前缀和,得到最终数组:
```cpp
long long value = 0;
for (int i = 0; i < n; i++) {
value += d[i];
a[i] = value;
}
```
## 七、二分
### 1. 二分查找
给定升序数组和目标值 x,找到第一个大于或等于 x 的元素下标;如果不存在,就返回 `n+1`。
由于数组 `a[1..n]` 已经升序排列,小于 x 的元素都在前面,不小于 x 的都在后面,两部分之间只有一道分界。每次检查中间位置,就能确定接下来该往哪一半找。
```cpp
int l = 1, r = n, ans = n + 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] >= x) {
ans = mid; // 先记下这个满足条件的位置
r = mid - 1; // 再去左边找更靠前的位置
} else {
l = mid + 1;
}
}
// ans==n+1 表示不存在,否则 ans 就是所求位置
```
当 `mid` 满足条件,先把它记进 ans,再去左半边找更靠前的位置;不满足时,就去右半边。ans 始终留着已经找到的结果,后面只有找到更早的位置时才替换它。
例如数组为 `2 4 4 7`,查找第一个不小于 4 的位置,最终得到 ans=2。要判断 x 是否确实存在,可以检查 `ans <= n && a[ans] == x`,先确认下标合法,再读取元素。
这里使用闭区间 `[l,r]`,循环条件是 `l <= r`。检查过 mid 后,用 `mid-1` 或 `mid+1` 缩小范围,不再重复检查这个位置。
### 2. 二分答案
二分也可以用来找最优答案。先尝试一个值,判断它能否满足要求;如果可行与不可行之间只有一道分界,就可以不断缩小答案范围。
#### 例:分段的最大段和
给定一个包含 n 个正整数的序列,以及一个正整数 k,其中 $1\le k\le n$。请在不改变元素顺序的前提下,将整个序列分成不超过 k 个非空连续段。每个元素必须恰好属于其中一段,不能遗漏或重复使用。
对于一种分段方案,先分别计算每一段的元素之和,再取这些段和中的最大值,作为该方案的结果。请找到一种分段方案,使这个最大值尽可能小,并输出它的最小值。例如,序列为 `7 2 5 10 8`,k=2。若分成 `7 2 5` 和 `10 8` 两段,段和分别为 14、18,最大段和为 18。其他合法方案都无法让最大段和小于 18,因此答案为 18。
那么就可以假设允许的最大段和是 `limit`,可以从左到右尽量往当前段加入元素,装不下就新开一段。如果这样需要的段数不超过 k,就说明这个上限可行。
分析题目发现,如果上限越大,限制越宽松:某个值已经可行,更大的值也可行。存在着单调性,因此可以二分“第一个可行值”。
确定了单调性,再找搜索范围。一个元素不能拆开,所以答案至少是最大元素;把整串作为一段,则得到总和这个上界。代码中用 `a[1..n]` 保存正整数,约定 `1<=k<=n`,相关变量已准备好。
先写判断函数,看看上限 limit 是否能让段数不超过 k:
```cpp
bool check(long long limit) {
int count = 1;
long long sum = 0;
for (int i = 1; i <= n; i++) {
if (a[i] > limit) return false;
if (sum + a[i] > limit) {
count++;
sum = a[i];
} else {
sum += a[i];
}
}
return count <= k;
}
```
再记录已经找到的可行答案,继续尝试更小的上限:
```cpp
long long l = 0, r = 0;
for (int i = 1; i <= n; i++) {
l = max(l, (long long)a[i]);
r += a[i];
}
long long ans = r;
while (l <= r) {
long long mid = l + (r - l) / 2;
if (check(mid)) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
// ans 是最大段和能达到的最小值
```
`check(mid)` 为真,说明 mid 能用,先存下来,再向左找;为假,说明上限太小,就向右找。
对于题目出现需要求“最大的最小”的时候,就可以确认单调性,再决定是否通过二分去解决。
## 八、栈、队列与搜索
### 1. 栈
栈只在一端放入、取出元素,这一端叫栈顶。像一摞盘子,通常先取走最上面、也就是最后放上去的那个。
依次压入 2、5、7,此时栈顶是 7;弹出一次后,栈顶变成 5,再弹出才轮到 2。这就是“后进先出”。
| 操作 | `stack<int> st` 的写法 | 要记住的事 |
| --- | --- | --- |
| 压入元素 | `st.push(x)` | x 成为新栈顶 |
| 读取栈顶 | `st.top()` | 只读取,不删除 |
| 弹出栈顶 | `st.pop()` | 只删除,不返回元素 |
| 判断是否为空 | `st.empty()` | 空栈不能读取或弹出 |
| 元素个数 | `st.size()` | 表示当前还留在栈中的数量 |
需要取出并保存栈顶时,先读再删:
```cpp
if (!st.empty()) {
int x = st.top();
st.pop();
// 使用 x
}
```
#### 例:判断括号是否匹配
给定一个只包含 `(`、`)`、`[`、`]` 的字符串,判断括号能否按正确的类型和嵌套顺序全部配对。
右括号要先和最近一个尚未配对的左括号匹配,这正好符合栈的顺序。遇到左括号就入栈,遇到右括号就检查栈顶。
例如 `([])`:先压入 `(`,再压入 `[`;遇到 `]` 时,配对并弹出 `[`;遇到 `)` 时,再弹出 `(`。处理完后栈为空,匹配成功。
`([)]` 则在读到 `)` 时出错:栈顶是 `[`,类型对不上,也不能越过它去找更下面的 `(`。
```cpp
bool matched(const string& s) {
stack<char> st;
for (char c : s) {
if (c == '(' || c == '[') {
st.push(c);
} else {
if (st.empty()) return false;
char t = st.top();
if ((c == ')' && t != '(') ||
(c == ']' && t != '[')) return false;
st.pop();
}
}
return st.empty();
}
```
除了中途检查空栈和括号类型,结束时还要确认栈为空。比如 `(()` 中途没有冲突,却留下了一个左括号,所以函数最后还要判断一次是否配完。
撤销最近一次操作、处理嵌套结构时,也常会用到栈。递归调用有类似的返回顺序:后调用的那一层先执行完,再回到调用它的位置。
### 2. 队列
队列在队尾加入元素,在队首取出元素。依次加入 2、5、7,处理顺序就是 2、5、7,这叫“先进先出”。
| 操作 | `queue<int> q` 的写法 | 要记住的事 |
| --- | --- | --- |
| 入队 | `q.push(x)` | 加在队尾 |
| 读取队首 | `q.front()` | 取得下一个待处理元素 |
| 读取队尾 | `q.back()` | 取得最后加入的元素 |
| 出队 | `q.pop()` | 删除队首,不返回元素 |
| 判断是否为空 | `q.empty()` | 读队首、队尾或出队前确认非空 |
| 元素个数 | `q.size()` | 当前队列长度 |
#### 例:循环报数
编号 1~n 的人围成一圈,从 1 开始报数,报到 k 的人出队,再从下一个人重新报 1。要求输出所有人的出队顺序,其中 n、k 均为正整数。
可以把围成一圈的报数过程放进队列:没数到 k 的人先移到队尾,报到 k 的人输出后删除。比如 1、2、3、4、5 每次数到第 3 人出队,就先移动前两个人,再删除当前队首。
```cpp
queue<int> q;
for (int i = 1; i <= n; i++) q.push(i);
while (!q.empty()) {
for (int i = 1; i < k; i++) {
int x = q.front();
q.pop();
q.push(x);
}
cout << q.front() << ' ';
q.pop();
}
```
`n=5`、`k=3` 时,出队顺序为 `3 1 5 2 4`。每轮只移动 k−1 次,紧接着的队首才是第 k 人,多移动一次就会错位。
这份写法的操作次数约为 $nk$,适合规模允许时使用。k 很大时,要留意反复移到队尾的开销。
按到达顺序办理任务,也可以用队列。BFS 就借助队列,让早发现、距离近的状态先被处理。
### 3. DFS 与回溯
如果每一步都有几种选择,可以沿着一种选择继续尝试,走到完成方案或无法继续时,再返回尝试其他选择,这就是 DFS 的过程。分支层数随输入变化时,用递归比写很多层固定循环更方便。
回溯中最容易漏掉的是恢复现场:如果这一步修改了后续分支共用的路径或标记,返回后要记得撤销修改。
#### 例一:选数凑目标和
给定 n 个整数和目标和 target,每个位置最多选一次,统计选出的数之和等于 target 的方案数。按位置区分选法,允许空集。
例如三个数为 2、3、5,目标和为 5,有两种选法:选 2 和 3,或者只选 5。
处理一个位置时,只有“选”和“不选”两个分支。可以进行搜索,令 `dfs(pos, sum)` 表示前面的位置已经决定完,现在处理第 pos 个数,当前和为 sum。
```cpp
int n;
long long target, ans = 0;
vector<long long> a; // 使用 a[1..n]
void dfs(int pos, long long sum) {
if (pos == n + 1) {
if (sum == target) ans++;
return;
}
sum += a[pos]; // 选当前位置
dfs(pos + 1, sum);
sum -= a[pos]; // 恢复本层的和,再尝试不选
dfs(pos + 1, sum);
}
// 读入数据、令 ans=0 后,调用 dfs(1, 0)
```
等每个位置都决定完,再检查和是否等于目标,这样一组选择只会统计一次。若题目要求至少选一个数,还要记录是否已经选择过元素。
代码先把当前数加进本层的 sum,试完“选”的分支后再减回来,接着尝试“不选”。下一层按值接收参数,对它自己局部变量的修改不会传回本层。另一种写法是直接传入 `sum + a[pos]`,这样本层的和没有改变,就省去了加减操作。
如果还要记录具体选了哪些数,可以用数组或 `vector` 保存路径:选时加入,递归回来后删掉。n 个位置各有两种选择,总方案数为 $2^n$,只能用于较小规模。所有数非负时,和超过目标后可以停止该分支;有负数时,后面可能把和减回来,不能这样剪枝。
#### 例二:选数组合
从整数 1~n 中选出 k 个不同的数,输出所有组合,每个组合只输出一次。这里约定 `1≤k≤n`,组合内部按递增顺序输出。
例如从 1、2、3、4 中选两个数,`1 3` 和 `3 1` 是同一种组合。让选出的数字始终递增,就只会生成一次。
```cpp
int n, k;
vector<int> path;
void choose(int start) {
if (path.size() == k) {
for (int x : path) cout << x << ' ';
cout << '\n';
return;
}
for (int x = start; x <= n; x++) {
path.push_back(x);
choose(x + 1);
path.pop_back();
}
}
// 约定 1<=k<=n,初始路径为空,调用 choose(1)
```
代码用 `start` 限定下一次可选的最小值。选了 2,下一层就从 3 开始,既不会重复选 2,也就不会回头产生顺序颠倒的组合,因此不用额外记录哪些数已经用过。
与前一个例子的局部和不同,`path` 是各层递归时共用的,所以它是一个全局变量,那么 加入后尝试深入 和 回溯后撤销 的操作就要配套:`push_back` 记录选择,返回后用 `pop_back` 撤销。比如试完 `1 2`,退回到路径只含 1 的状态,才能接着试 `1 3`。
#### 例三:迷宫路径计数
给定一个网格、起点和终点,只能上下左右移动,不能经过障碍,也不能在同一条路径中重复经过格子。求满足这些条件的路径总数。
因为每一步都可以尝试向上、下、左、右移动,因此可以用递归搜索:先决定当前这一层处理所在的格子,然后递归到下一层处理准备走到的相邻格子。如果到达终点,就说明找到了一条完整路径,将答案加 1,并结束这条分支,不再从终点继续走。不过尝试一个方向之前,要先检查下一格是否在网格范围内、是否为障碍,以及是否已经在当前路径中经过。只有这三个检查都通过,才能继续递归。
且为了避免同一条路径重复经过格子,进入一个格子时,要把它标记为“已经经过”。例如从起点向右走了一格,这一格就加入了当前路径;接下来继续搜索时,不能再走回它。
当这个格子后面的所有走法都尝试完,就要返回上一层,并取消它的访问标记。因为题目限制的是“同一条路径不能重复经过格子”,其他路径仍然可以经过这里。如果返回时不取消标记,后续路径就可能被错误地拦住,导致少算。
因此,整个搜索过程可以理解为:**进入格子并标记,尝试四个方向,搜索结束后取消标记,再返回上一层。** 每次返回,都恢复到进入这一格之前的状态,方便继续尝试其他走法,这个过程就叫作回溯。
下面约定行列从 0 开始,`grid` 是全局字符串数组,`'#'` 表示障碍;`vis` 大小为 n×m,初始全为 0,起终点均合法且可通行:
```cpp
int n, m, tx, ty;
long long ways = 0;
vector<string> grid;
vector<vector<int>> vis;
int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
void walk(int x, int y) {
if (x == tx && y == ty) {
ways++;
return;
}
vis[x][y] = 1;
for (int d = 0; d < 4; d++) {
int nx = x + dx[d], ny = y + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (grid[nx][ny] == '#' || vis[nx][ny]) continue;
walk(nx, ny);
}
vis[x][y] = 0;
}
// ways=0,vis 清零后,调用 walk(sx, sy)
```
如果把最后的恢复语句删掉,某条路径走过的格子会挡住其他路径,统计就可能漏掉。相反,在同一条路径还没结束时就取消标记,又会允许绕圈重复进入。
### 4. BFS
BFS 改用队列安排访问顺序,先处理距离起点近的状态,再处理更远的状态。它适合无权图或每一步代价相同的最少步数问题;当边权不同时,就不能直接沿用这种最短路判断。
一层里的点处理完,才轮到下一层,所以第一次发现一个点时,就已经找到了到达它的最少边数。
#### 例:求无权图中的最少步数
给定一个无权图和起点 s,求从 s 到各点最少经过多少条边;不可达的点记为 −1。
通过邻接表 `g[u]` 保存 u 的邻居,`dist` 长度为 `n+1` 且全部初始化为 `-1`:
```cpp
queue<int> q;
dist[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : g[u]) {
if (dist[v] != -1) continue;
dist[v] = dist[u] + 1;
q.push(v);
}
}
```
代码在入队时设置距离,同时完成访问标记,因此同一个点不会被反复加入队列。起点距离为 0,不可达的点则一直保持 −1,两者含义不同。
对于二维网格中的 BFS 也是同样的过程,只是一个状态改成行、列。扩展邻格时,需要依次检查:坐标合法、不是障碍、尚未访问。
### 6. 图的存储与连通块
前面的 BFS 通过邻居列表查找下一步能到哪里。存图时,邻接表只记录实际存在的边,通常适合边较少的图;邻接矩阵则直接记录两点是否相连,查边方便,但空间约为点数的平方。
无向边 `u-v` 用邻接表存储时,通常要加入两个方向。遍历无向图时必须防止沿边反复返回,使用访问标记,或在确实为树的条件下跳过父节点。
统计连通块时,从每个尚未访问的点开始一次 DFS 或 BFS,每次新启动的遍历对应一个新连通块。网格中的连通区域也可以这样做,这就是常见的泛洪填充。
## 九、动态规划
### 1. 一维 DP
#### 例:爬台阶
有 n 级台阶,每次可以上 1 级或 2 级,求恰好走到第 n 级的不同走法数。步子的先后顺序不同,算不同走法。
令 `dp[i]` 表示恰好到达第 i 级的方法数。
考虑最后一步:要么从第 i−1 级走 1 级,要么从第 i−2 级走 2 级。两种走法不会重复,加起来就是 $dp[i]=dp[i-1]+dp[i-2]$。
起点设为 `dp[0]=1`,表示“还没走”这一种起始方案,另外有 `dp[1]=1`。接着从小到大计算,每次用到的前两项都已经算好。
走法数增长很快,要估计结果范围。可以先考虑 `long long`,但它也有上限;题目要求取模时,应在递推过程中及时取模,而不能只对结果取模。
### 2. 最长上升子序列
给定一个数组,可以删去一些元素,但保留剩余元素的相对顺序,求严格上升子序列的最大长度。
这次把终点固定在第 i 个数:令 `dp[i]` 表示“以第 i 个数结尾的最长严格上升子序列长度”。它前面可以接上一个位置更早、数值更小的元素,因此逐个尝试前面的 j。初值设为 1,表示只选当前这个数。
```cpp
int ans = 0;
for (int i = 1; i <= n; i++) {
dp[i] = 1;
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1);
}
ans = max(ans, dp[i]);
}
```
最长的那一段子序列可能在任何位置结束,所以答案要取所有 `dp[i]` 的最大值,不能只看 `dp[n]`。严格上升用 `<`,允许相等的不下降版本才使用 `<=`。
子序列可以跳过元素,连续子数组则不能。读题时先分清这两个要求。上面写法的工作量为平方级,使用前要看 n 的范围。
### 3. 0/1 背包
给定容量 V 和 n 件物品,第 i 件的体积为 `w[i]`、价值为 `v[i]`。每件最多选一次,求总体积不超过 V 时的最大总价值,允许一件也不选。
令 `dp[j]` 表示已考虑的物品中,总体积不超过 j 时的最大价值。
初始全为 0,表示还没选物品。处理当前物品时,不拿就保留原来的最大价值;拿一次,就从扣除当前体积后的状态转移过来,再加上它的价值:
```cpp
vector<long long> dp(V + 1, 0);
for (int i = 1; i <= n; i++) { // 考虑前 i 件物品
for (int j = V; j >= w[i]; j--) { // 考虑前 i 件物品的情况下,背包容量为 j 时能获得的最大价值
dp[j] = max(dp[j], dp[j - w[i]] + v[i]); // 对于第 i 件物品,要么拿,要么不拿取最大值
}
}
```
这里的容量从大到小更新。计算 `dp[j]` 时,较小容量的 `dp[j-w[i]]` 还没被本件物品更新,用的仍是上一轮结果(考虑前 i - 1 件物品的情况下,背包容量为 j 时能获得的最大价值),因此每件物品只会选一次。
如果从小到大更新,刚刚拿过本件物品的状态就可能再次参与转移,相当于又拿了一次。这正是下一种背包允许的情况。
答案为 `dp[V]`。
### 4. 完全背包
给定容量 V 和 n 种物品,每种体积为正、数量不限,求总体积不超过 V 时的最大总价值。
每种物品既然可以重复选择,就允许继续使用本轮已经更新过的状态(考虑前 i 件物品的情况下,背包容量为 j 时能获得的最大价值),而这个已经被更新过的状态就有可能已经拿了第 i 件物品了,符合可以重复拿取的定义,那么沿用上一节的状态定义,把容量改为正序枚举即可:
```cpp
for (int i = 1; i <= n; i++) {
for (int j = w[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
```
记不清循环方向时,可以思考依赖的来源状态:它必须来自上一轮,还是允许本轮重复使用?
## 十、数学与位运算
### 1. 最大公约数与最小公倍数
求两个非负整数的最大公约数,可以反复用“除数、余数”替换当前两个数,直到余数为 0。这样做不会改变最大公约数。
```cpp
long long gcd(long long a, long long b) { // a、b 非负
while (b != 0) {
long long r = a % b;
a = b;
b = r;
}
return a;
}
```
求出最大公约数后,还可以用它计算正整数 a、b 的最小公倍数 lcm:
$$\operatorname{lcm}(a,b)=\frac{a}{\gcd(a,b)}\times b$$
### 2. 质数判断与因数分解
判断一个整数 n 是否为质数,先排除小于 2 的数,再尝试 2 到平方根之间的除数。只要找到一个能整除它的数,就可以确定它是合数:
```cpp
bool isPrime(int n) {
if (n < 2) return false;
for (int d = 2; 1LL * d * d <= n; d++) {
if (n % d == 0) return false;
}
return true;
}
```
循环条件保留等号,才能检查到 9、25 这样的完全平方数的平方根。需要判断很多个数时,可以考虑筛法,减少反复试除的工作。
同样的试除思路也能用来分解质因数:找到一个因数后,反复除尽,并记录除过几次。剩余数变小,试除范围也随之缩小;结束时若还剩一个大于 1 的数,它本身就是一个质因数。
### 3. 位运算
位运算处理的是整数的二进制位。先把数位对齐,再对同一位置上的 0、1 分别计算。例如 5 写成 `101`,3 写成 `011`,就可以按下面的规则逐位运算。
| 运算 | 每一位的规则 | 例子 |
| --- | --- | --- |
| 按位与 `&` | 两位都是 1,结果才是 1 | `5 & 3` 得到 `001`,即 1 |
| 按位或 `|` | 两位至少有一个 1,结果就是 1 | `5 | 3` 得到 `111`,即 7 |
| 按位异或 `^` | 两位不同得 1,相同得 0 | `5 ^ 3` 得到 `110`,即 6 |
| 按位取反 `~` | 每一位的 0、1 互换 | 对类型的全部位取反,不只处理写出来的几位 |
| 左移 `<<`、右移 `>>` | 把二进制位向左或向右移动 | 对非负整数,`5 << 1` 为 10,`5 >> 1` 为 2 |
左移时低位补 0;非负整数右移时高位补 0,移出的低位丢弃。移位次数必须在类型允许范围内,左移后的值也要能表示。
这些规则可以用来检查某一位。`x & 1` 只看最低位,结果为 1 就是奇数;`(x >> k) & 1` 先把第 k 位移到最低位,再取出它,最低位编号为 0。异或还满足两个常用性质:相同数异或为 0,一个数与 0 异或仍是它自己。
`^` 表示异或,不是乘方。若只有一个数出现奇数次,其余数都出现偶数次,把所有数异或起来,相同的数会两两抵消,最后就剩下那个数。
## 十一、高精度运算
整数超过 `long long` 的范围时,可以用字符串保存,按照手算竖式的规则逐位计算。下面的输入均为非负整数,减法结果可以为负数。
### 1. 数位存储
按平常写数的顺序保存,例如 5079 存成字符串“5079”。计算前去掉多余前导零,全为零时保留一个 0。
加、减、乘法可以先反转字符串,让下标 0 对应个位、下标 1 对应十位,算完后再反转结果。除法从最高位开始,不需要反转。
字符串中存的是字符,参与计算时要转成对应数字,算出的数位再转回字符保存。
### 2. 高精度加法
要求计算两个非负大整数的和。
1. 反转两个字符串,在较短串末尾补零,使对应数位对齐。
2. 从个位开始,将两个对应数位与上一位的进位相加。
3. 总和除以 10 的余数作为本位,整数商作为下一位的进位。
4. 所有数位算完后,若仍有进位,就追加到结果末尾,最后反转结果。
例如 $999+1$,每一位都会产生进位,原有数位算完后还要补上最高位的 1,得到 1000。
### 3. 高精度减法
要求计算两个非负大整数的差,结果可以为负数。
1. 先比较大小:去掉前导零后,位数多的更大;位数相同,从最高位开始比较第一个不同的数位。
2. 若被减数较小,交换两数并记录负号,之后用较大的数减较小的数。
3. 反转两个字符串,给较短串末尾补零。从个位开始,计算对应数位的差,再扣除上一位的借位。
4. 本位不够减时加 10,并向更高位借 1;下一轮计算要扣掉这次借位。
5. 去掉倒序结果末尾的多余零,至少保留一位,再反转结果,按需添加负号。
例如 $1000-1=999$,要连续借位;$1-1000=-999$,先算差的绝对值,再加负号。两数相等时结果为 0。
### 4. 高精度乘法
要求计算两个非负大整数的积,采用“先累加乘积,再统一进位”的方法。
1. 任意一个数为 0,结果直接为 0。否则反转两个字符串,让下标 0 对应个位。
2. n 位数乘 m 位数,积最多 n+m 位,准备这么多个位置的整数数组,初值全为 0。
3. 每一对数位分别相乘,第 i 位与第 j 位的乘积累加到第 i+j 位,暂不进位。同一位置会收到多个乘积,累加值可以大于 9。
4. 从低位到高位统一进位:当前累加值加上上一位的进位,除以 10 的余数作为本位,整数商继续向高位传递。
5. 处理完剩余进位,去掉倒序结果末尾的多余零,再反转结果。
例如 $23\times47$,个位先累加 $3\times7=21$,十位累加 $3\times4+2\times7=26$,百位累加 $2\times4=8$。统一进位后,个位留 1、进 2,十位得到 28、留 8、进 2,百位得到 10、留 0、进 1,最终得到 1081。
两组数位分别遍历,时间复杂度为 $O(nm)$。
### 5. 高精度除以小整数
要求用非负大整数除以正整数 b,求整数商和余数。大整数用字符串保存,b 能放进 `int`。
手算除法时,先看前面的数够除几次,再留下余数、落下下一位。因此字符串也从最高位开始,向右逐位计算。
1. 余数初始为 0,从左到右依次读取数位。
2. 把上一轮余数乘 10,再加上当前数位,组成这一轮待除的数。
3. 用它除以 b,整数商作为当前一位商,余数留给下一轮。
4. 全部数位处理完后,只去掉商开头多余的零,中间的零必须保留。最后留下的余数就是最终余数。
例如计算 1234 除以 12:
| 读入数位 | 上轮余数乘 10,再加当前位 | 本位商 | 新余数 |
| --- | --- | --- | --- |
| 1 | $0\times10+1=1$ | 0 | 1 |
| 2 | $1\times10+2=12$ | 1 | 0 |
| 3 | $0\times10+3=3$ | 0 | 3 |
| 4 | $3\times10+4=34$ | 2 | 10 |
依次得到 0102,去掉开头多余的零,商为 102,余数为 10。商十位上的 0 不能省略,结果满足 $1234=12\times102+10$。
被除数小于 b 时,商为 0,余数为被除数;被除数为 0 时,商和余数均为 0。余数乘 10 的中间值可能超出 `int`,要用 `long long` 保存。整个过程扫描一次字符串,时间复杂度为 $O(n)$。
## 十二、树
### 1. 树的基本概念
树是连通且没有环的无向图。n 个节点的树有 n−1 条边,任意两个不同节点之间只有一条不重复经过节点的路径。
给树选定一个根后,就可以从根往下确定父子关系。下图以 1 为根:
<img src="C:\Users\xiaolinquan\AppData\Roaming\Typora\typora-user-images\image-20261016220317516.png" alt="image-20261016220317516" style="zoom:40%;" />
| 名称 | 怎样理解 | 图中的例子 |
| --- | --- | --- |
| 根节点 | 选定的起点,没有父节点 | 1 |
| 父节点、子节点 | 相邻两点中,靠近根的是父,远离根的是子 | 2 是 4、5 的父节点 |
| 兄弟节点 | 具有同一个父节点 | 4 和 5 |
| 叶节点 | 有根树中没有子节点的节点 | 4、5、6、7 |
| 子树 | 一个节点和它的全部后代构成的树 | 以 2 为根的子树含 2、4、5 |
| 深度 | 从根到该节点经过的边数,根深度为 0 | 2 的深度为 1,4 的深度为 2 |
例如,节点 5 的深度为 2,以 2 为根的子树包含 2、4、5。这里根的深度为 0;有些题目把根所在层记为第 1 层,要注意区分。
### 2. 树的几种存储方式
树可以用编号和数组、邻接表或动态节点保存。本节重点掌握动态创建节点的方法,前两种方式了解基本形式即可。
#### 用编号和数组保存二叉树
二叉树的每个节点最多有两个孩子,按编号保存时,可以用结构体数组记录左右孩子:
```cpp
struct TreeNode {
int left;
int right;
};
TreeNode tree[8] = {}; // 下标 1~7 对应节点,初值全为 0
tree[1].left = 2; tree[1].right = 3;
tree[2].left = 4; tree[2].right = 5;
tree[3].left = 6; tree[3].right = 7;
```
数组下标表示节点编号,`left`、`right` 保存孩子编号,0 表示没有孩子。例如 `tree[2].right` 的值为 5,表示节点 2 的右孩子是 5。
#### 用邻接表保存相邻关系
普通树可以用邻接表保存相邻节点。`neighbors[node]` 保存与 node 直接相连的节点。无向边要在两端各保存一次:
```cpp
vector<vector<int>> neighbors(n + 1);
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
neighbors[u].push_back(v);
neighbors[v].push_back(u);
}
```
### 3. 动态创建节点
动态存储时,用结构体保存节点的值和两个孩子的地址,需要新节点时用 `new` 创建,再连接到树上。
以二叉树为例,一个节点可以这样定义:
```cpp
struct Node {
int value;
Node* left;
Node* right;
};
```
`value` 保存节点的值,`left`、`right` 指向左右孩子,`nullptr` 表示没有孩子。创建根节点:
```cpp
Node* root = new Node{1, nullptr, nullptr};
```
`new` 返回新节点的地址,`root` 保存这个地址。创建并连接左孩子:
```cpp
Node* child = new Node{2, nullptr, nullptr};
root->left = child;
```
`->` 用来通过指针访问成员。也可以直接写成 `root->left = new Node{2, nullptr, nullptr};`。
#### 例一:动态建立一棵二叉树
按上面的二叉树图创建 7 个节点,并输出节点 5 的值:
```cpp
Node* root = new Node{1, nullptr, nullptr};
root->left = new Node{2, nullptr, nullptr}; root->right = new Node{3, nullptr, nullptr};
root->left->left = new Node{4, nullptr, nullptr};
root->left->right = new Node{5, nullptr, nullptr};
root->right->left = new Node{6, nullptr, nullptr};
root->right->right = new Node{7, nullptr, nullptr};
cout << root->left->right->value; // 输出 5
```
`root->left->right->value` 表示根的左孩子的右孩子的值,也就是节点 5 的值。但是访问成员前,要保证前面的指针不是 `nullptr`。
### 4. 二叉树的遍历
常见的二叉树遍历如下:
| 遍历方式 | 处理顺序 |
| --- | --- |
| 前序 | 根、左子树、右子树 |
| 中序 | 左子树、根、右子树 |
| 后序 | 左子树、右子树、根 |
#### 例二:遍历动态建立的二叉树
对例一建立的二叉树,前序序列为 `1 2 4 5 3 6 7`。前序遍历如下:
```cpp
void preOrder(Node* node) {
if (node == nullptr) return;
cout << node->value << ' ';
preOrder(node->left);
preOrder(node->right);
}
// 建好树后调用 preOrder(root),输出 1 2 4 5 3 6 7
```
中序序列为 `4 2 5 1 6 3 7`,把输出语句放在两次递归之间;后序序列为 `4 5 2 6 7 3 1`,把输出语句放在两次递归之后。空指针要先判断。
层序遍历使用队列,输出顺序为 `1 2 3 4 5 6 7`。
@@ -0,0 +1,150 @@
<center><font size="6"><b>NOIP/CSP 初赛专题训练</b></font></center>
<center><font size="5"><b>1.1 进制转换 答案</b></font></center>
### 答案速查
| 题号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **答案** | D | C | B | C | B | B | B | B | 101 | B |
| 题号 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **答案** | B | A | C | B | B | $(0.101)_2$ | A | 0.75 | B | A |
| 题号 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **答案** | B | $(111111.1)_2$ | B | $(1\text{E}5)_{16}$ | C | C | B | 45 | B | B |
| 题号 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **答案** | B | C | 5 | A | 4 | B | B | C | B | 4 |
---
### 详细解析
#### 题型一:位权与数码规则
1. **答案:D**
七进制数的基数为 7,允许使用的数码是 $0 \sim 6$,因此 $7$ 是不允许出现的。
2. **答案:C**
五进制数允许使用的数码是 $0 \sim 4$。选项 C 中的 $(5104)_5$ 包含了数码 $5$,因此不合法。
3. **答案:B**
$(101101)_2 = 1 \times 2^5 + 0 \times 2^4 + 1 \times 2^3 + 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 = 32 + 8 + 4 + 1 = 45$。
4. **答案:C**
$(347)_8 = 3 \times 8^2 + 4 \times 8^1 + 7 \times 8^0 = 3 \times 64 + 32 + 7 = 192 + 32 + 7 = 231$。
5. **答案:B**
$(2\text{AF})_{16} = 2 \times 16^2 + 10 \times 16^1 + 15 \times 16^0 = 2 \times 256 + 160 + 15 = 512 + 160 + 15 = 687$。
#### 题型二:整数进制转换
6. **答案:B**
$94 = 64 + 16 + 8 + 4 + 2 = 2^6 + 2^4 + 2^3 + 2^2 + 2^1 = (1011110)_2$。
7. **答案:B**
采用除 8 取余法:$156 \div 8 = 19$ 余 4;$19 \div 8 = 2$ 余 3;$2 \div 8 = 0$ 余 2。倒序排列余数得到 $(234)_8$。
8. **答案:B**
采用除 16 取余法:$1000 \div 16 = 62$ 余 8;$62 \div 16 = 3$ 余 14(十六进制中为 $\text{E}$);$3 \div 16 = 0$ 余 3。倒序得到 $(3\text{E}8)_{16}$。
9. **答案:101**
$(1100101)_2 = 2^6 + 2^5 + 2^2 + 2^0 = 64 + 32 + 4 + 1 = 101$。
10. **答案:B**
将二进制数从右向左每三位划分为一组:$(100000)_2 \rightarrow 100\ 000 \rightarrow (40)_8$。
11. **答案:B**
将二进制数从右向左每四位划分为一组,不足补前导零:$(111010111)_2 \rightarrow 0001\ 1101\ 0111 \rightarrow (1\text{D}7)_{16}$。
12. **答案:A**
将十六进制数的每一位展开为四位二进制数:$7 \rightarrow 0111$,$\text{B} \rightarrow 1011$,$4 \rightarrow 0100$。合并得 $(0111\ 1011\ 0100)_2$。
#### 题型三:小数进制转换
13. **答案:C**
$(101.101)_2 = 2^2 + 2^0 + 2^{-1} + 2^{-3} = 4 + 1 + 0.5 + 0.125 = 5.625$。
14. **答案:B**
$(17.4)_8 = 1 \times 8^1 + 7 \times 8^0 + 4 \times 8^{-1} = 8 + 7 + 0.5 = 15.5$。
15. **答案:B**
$(\text{A}.\text{C})_{16} = 10 \times 16^0 + 12 \times 16^{-1} = 10 + 12 / 16 = 10 + 0.75 = 10.75$。
16. **答案:$(0.101)_2$**
小数部分乘 2 取整法:$0.625 \times 2 = 1.25$(取 1),$0.25 \times 2 = 0.5$(取 0),$0.5 \times 2 = 1.0$(取 1)。故二进制为 $(0.101)_2$。
17. **答案:A**
整数部分:$13 = 8 + 4 + 1 = (1101)_2$;小数部分:$0.375 = 3/8 = 1/4 + 1/8 = 2^{-2} + 2^{-3} = (0.011)_2$。合并为 $(1101.011)_2$。
18. **答案:0.75**
$(0.3)_4 = 3 \times 4^{-1} = 3 / 4 = 0.75$。
#### 题型四:二、八、十六进制快速互转
19. **答案:B**
从右向左每三位分组,不足补零:$011\ 010\ 110 \rightarrow (326)_8$。
20. **答案:A**
每一位八进制数码展开为三位二进制:$5 \rightarrow 101$,$7 \rightarrow 111$,$2 \rightarrow 010$。合并为 $(101111010)_2$。
21. **答案:B**
先转为二进制:$(9\text{E})_{16} \rightarrow 1001\ 1110$;再三位分组转为八进制:$010\ 011\ 110 \rightarrow (236)_8$。
22. **答案:$(111111.1)_2$**
展开为二进制:$(3\text{F}.8)_{16} \rightarrow 0011\ 1111 . 1000$。省略前导零和末尾零后得到 $(111111.1)_2$。
23. **答案:B**
以小数点为界,向左和向右分别每四位分组,不足补零:$0010\ 1011 . 0110 \rightarrow (2\text{B}.6)_{16}$。
24. **答案:$(1\text{E}5)_{16}$**
先转二进制:$(745)_8 \rightarrow 111\ 100\ 101$;再四位分组转十六进制:$0001\ 1110\ 0101 \rightarrow (1\text{E}5)_{16}$。
#### 题型五:混合进制运算
25. **答案:C**
先将各数转为十六进制以方便运算:$(101101)_2 = (2\text{D})_{16}$,$(37)_8 = (1\text{F})_{16}$。相加:$(2\text{D})_{16} + (1\text{F})_{16} = (4\text{C})_{16}$。
26. **答案:C**
可以统一转为十进制计算:$(2\text{A})_{16} = 42$,$(1011)_2 = 11$;$42 - 11 = 31$。将 $31$ 转为八进制即 $3 \times 8 + 7 = (37)_8$。
27. **答案:B**
转为十进制:$(123)_4 = 16 + 8 + 3 = 27$,$(11)_2 = 3$;相乘 $27 \times 3 = 81$。将 $81$ 除以 4 取余得四进制表示 $(1101)_4$。
28. **答案:45**
统一转为十进制:$(1111)_2 = 15$,$(17)_8 = 15$,$(\text{F})_{16} = 15$。相加得 $15 + 15 + 15 = 45$。
29. **答案:B**
直接在二进制下做除法即可,相当于十进制中的 $16 \div 2 = 8$,对应的二进制即为 $(1000)_2$。
30. **答案:B**
转为十进制运算:$(3\text{E}8)_{16} = 3 \times 256 + 14 \times 16 + 8 = 1000$;$(1750)_8 = 1 \times 512 + 7 \times 64 + 5 \times 8 = 1000$。相加得 $2000$。$2000 \div 16 = 125$ 余 $0$,$125 \div 16 = 7$ 余 $13$($\text{D}$),得 $(7\text{D}0)_{16}$。
#### 题型六:未知进制、位数与表示范围
31. **答案:B**
根据位权展开构建方程:$1 \times b^2 + 2 \times b^1 + 1 \times b^0 = 16$,即 $(b+1)^2 = 16$,解得 $b = 3$。
32. **答案:C**
根据位权展开:$2 \times b + 3 = 17$,解得 $2b = 14$,即 $b = 7$。
33. **答案:5**
根据位权展开:$1 \times b^2 + 1 \times b + 0 = 30$,即 $b(b+1) = 30$,因为 $5 \times 6 = 30$,故基数 $b = 5$。
34. **答案:A**
观察个位数相加:$4 + 3 = 7$。但结果个位变成了 $2$,说明产生了进位,故基数 $b = 7 - 2 = 5$。代入十位数验证:$2 + 1 + 1\text{(进位)} = 4$,成立。
35. **答案:4**
十进制中 $(10000)_2 = 16$。而左侧 $(100)_b = b^2$。因此 $b^2 = 16$,解得基数 $b = 4$。
36. **答案:B**
根据位权展开方程:$5x + 2 = 17$,解得 $5x = 15$,即 $x = 3$。
37. **答案:B**
因为 $1023 = 2^{10} - 1$,即二进制下表现为 $10$ 个连续的数码 $1$,因此至少需要 $10$ 位二进制数码。
38. **答案:C**
六位三进制数的最大值为 $(222222)_3$,它恰好等于 $3^6 - 1 = 729 - 1 = 728$。
39. **答案:B**
不超过四位的十六进制数,其最大值为 $(\text{FFFF})_{16} = 16^4 - 1 = 65535$。因为题目强调是“正整数”,取值范围从 $1$ 到 $65535$,共可表示 $65535$ 个不同的正整数。
40. **答案:4**
根据带小数的位权展开方程:$1 \times b^1 + 2 \times b^0 + 1 \times b^{-1} = 6.25$,即 $b + 2 + \frac{1}{b} = 6.25$,整理得 $b + \frac{1}{b} = 4.25 = 4 + \frac{1}{4}$。显然基数 $b = 4$。
@@ -0,0 +1,146 @@
<center><font size="6"><b>NOIP/CSP 初赛专题训练</b></font></center>
<center><font size="5"><b>1.1 进制转换</b></font></center>
### 姓名:\_\_\_\_\_\_\_\_\_\_\_\_
---
### 题型一:位权与数码规则
1. ( )在七进制数中,不允许出现的数码是。
A. 0 B. 5 C. 6 D. 7
2. ( )下列写法中,不能作为五进制数的是。
A. (1423)5 B. (4032)5 C. (5104)5 D. (2240)5
3. ( )二进制数 $(101101)_2$ 对应的十进制数是。
A. 41 B. 45 C. 49 D. 53
4. ( )八进制数 $(347)_8$ 对应的十进制数是。
A. 223 B. 227 C. 231 D. 239
5. ( )十六进制数 $(2\text{AF})_{16}$ 对应的十进制数是。
A. 671 B. 687 C. 703 D. 719
---
### 题型二:整数进制转换
6. ( )十进制数 94 的二进制表示是。
A. $(1011010)_2$ B. $(1011110)_2$
C. $(1101110)_2$ D. $(1111010)_2$
7. ( )十进制数 156 的八进制表示是。
A. $(224)_8$ B. $(234)_8$ C. $(244)_8$ D. $(264)_8$
8. ( )十进制数 1000 的十六进制表示是。
A. $(3\text{D}8)_{16}$ B. $(3\text{E}8)_{16}$
C. $(3\text{F}8)_{16}$ D. $(4\text{E}8)_{16}$
9. 把二进制数 $(1100101)_2$ 转换成十进制数,结果为 \_\_\_\_\_\_\_\_。
10. ( )二进制数 $(100000)_2$ 转换成八进制数是。
A. $(20)_8$ B. $(40)_8$ C. $(100)_8$ D. $(400)_8$
11. ( )二进制数 $(111010111)_2$ 转换成十六进制数是。
A. $(1\text{C}7)_{16}$ B. $(1\text{D}7)_{16}$
C. $(3\text{A}7)_{16}$ D. $(\text{D}7)_{16}$
12. ( )十六进制数 $(7\text{B}4)_{16}$ 的二进制表示是。
A. $(0111\ 1011\ 0100)_2$ B. $(0111\ 1010\ 0100)_2$
C. $(1011\ 0117\ 0100)_2$ D. $(0111\ 1011\ 1000)_2$
---
### 题型三:小数进制转换
13. ( )二进制小数 $(101.101)_2$ 对应的十进制数是。
A. 5.125 B. 5.5 C. 5.625 D. 5.75
14. ( )八进制小数 $(17.4)_8$ 对应的十进制数是。
A. 15.25 B. 15.5 C. 15.625 D. 17.5
15. ( )十六进制小数 $(\text{A}.\text{C})_{16}$ 对应的十进制数是。
A. 10.5 B. 10.75 C. 10.8 D. 10.12
16. 十进制小数 0.625 转换成有限二进制小数为 \_\_\_\_\_\_\_\_。
17. ( )十进制数 13.375 的二进制表示是。
A. $(1101.011)_2$ B. $(1101.101)_2$
C. $(1011.011)_2$ D. $(1100.111)_2$
18. 四进制小数 $(0.3)_4$ 对应的十进制数为 \_\_\_\_\_\_\_\_。
---
### 题型四:二、八、十六进制快速互转
19. ( )二进制数 $(11010110)_2$ 转换成八进制数是。
A. $(316)_8$ B. $(326)_8$ C. $(336)_8$ D. $(346)_8$
20. ( )八进制数 $(572)_8$ 转换成二进制数是。
A. $(101111010)_2$ B. $(101110010)_2$
C. $(111101010)_2$ D. $(101111100)_2$
21. ( )十六进制数 $(9\text{E})_{16}$ 转换成八进制数是。
A. $(226)_8$ B. $(236)_8$ C. $(246)_8$ D. $(256)_8$
22. 省略无意义的前导零和末尾零后,$(3\text{F}.8)_{16}$ 的二进制表示为 \_\_\_\_\_\_\_\_。
23. ( )二进制数 $(101011.011)_2$ 转换成十六进制数是。
A. $(2\text{B}.3)_{16}$ B. $(2\text{B}.6)_{16}$
C. $(5\text{B}.3)_{16}$ D. $(5\text{B}.6)_{16}$
24. 八进制数 $(745)_8$ 转换成十六进制数为 \_\_\_\_\_\_\_\_。
---
### 题型五:混合进制运算
25. ( )$(101101)_2 + (37)_8$ 的结果用十六进制表示为。
A. $(4\text{A})_{16}$ B. $(4\text{B})_{16}$ C. $(4\text{C})_{16}$ D. $(4\text{D})_{16}$
26. ( )$(2\text{A})_{16} - (1011)_2$ 的结果用八进制表示为。
A. $(35)_8$ B. $(36)_8$ C. $(37)_8$ D. $(40)_8$
27. ( )$(123)_4 \times (11)_2$ 的结果用四进制表示为。
A. $(1001)_4$ B. $(1101)_4$ C. $(1110)_4$ D. $(1201)_4$
28. $(1111)_2 + (17)_8 + (\text{F})_{16}$ 的十进制结果为 \_\_\_\_\_\_\_\_。
29. ( )二进制算式 $(10000)_2 \div (10)_2$ 的结果是。
A. $(100)_2$ B. $(1000)_2$ C. $(10000)_2$ D. $(1111)_2$
30. ( )$(3\text{E}8)_{16} + (1750)_8$ 的结果用十六进制表示为。
A. $(7\text{C}0)_{16}$ B. $(7\text{D}0)_{16}$
C. $(7\text{E}0)_{16}$ D. $(800)_{16}$
---
### 题型六:未知进制、位数与表示范围
31. ( )若 $(121)_b = (16)_{10}$,则基数 $b$ 为。
A. 2 B. 3 C. 4 D. 5
32. ( )若 $(23)_b = (17)_{10}$,则基数 $b$ 为。
A. 5 B. 6 C. 7 D. 8
33. 若 $(110)_b = (30)_{10}$,则 $b =$ \_\_\_\_\_\_\_\_。
34. ( )在同一基数 $b$ 下,若 $(24)_b + (13)_b = (42)_b$,则 $b$ 等于。
A. 5 B. 6 C. 7 D. 8
35. 若 $(100)_b = (10000)_2$,则基数 $b =$ \_\_\_\_\_\_\_\_。
36. ( )若 $(x2)_5 = (17)_{10}$,其中 $x$ 是一位五进制数码,则 $x$ 等于。
A. 2 B. 3 C. 4 D. 不存在
37. ( )表示十进制数 1023,至少需要多少位二进制数码?
A. 9 B. 10 C. 11 D. 12
38. ( )六位三进制无符号整数能够表示的最大十进制数是。
A. 242 B. 364 C. 728 D. 729
39. ( )用不超过四位的十六进制数表示正整数,一共可以表示多少个不同的正整数?
A. 65534 B. 65535 C. 65536 D. 65537
40. 若 $(12.1)_b = (6.25)_{10}$,则基数 $b =$ \_\_\_\_\_\_\_\_。
@@ -0,0 +1,79 @@
<center><font size="6"><b>NOIP/CSP 初赛专题训练</b></font></center>
<center><font size="5"><b>1.10 OI 常识 答案</b></font></center>
### 答案速查
| 题号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **答案** | A | B | C | D | A | B | C | D | A | B |
| **题号** | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| **答案** | NOI | D | A | B | C | D | A | `g++` | C | D |
| **题号** | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| **答案** | A | B | C | A | B | D | B |
---
### 详细解析
#### **题型一: 竞赛体系与评测术语**
1. **答案:A**
CSP-J 中的 `J` 是 **Junior**(初级)的缩写。
2. **答案:B**
CSP-S 中的 `S` 是 **Senior**(高级)的缩写。
3. **答案:C**
**AC** 是 **Accepted** 的缩写,表示你的程序通过了该测试点的所有测试用例,答案完全正确。
4. **答案:D**
**WA** 是 **Wrong Answer** 的缩写,表示你的程序在某个测试用例上输出了与标准答案不符的结果。
5. **答案:A**
**TLE** 是 **Time Limit Exceeded** 的缩写,表示你的程序运行时间超过了题目规定的时间限制。
6. **答案:B**
**MLE** 是 **Memory Limit Exceeded** 的缩写,表示你的程序在运行过程中占用的内存超过了题目规定的内存限制。
7. **答案:C**
**RE** 是 **Runtime Error** 的缩写,表示你的程序在运行时发生了错误,例如数组越界、除以零、非法内存访问等,导致程序异常终止。
8. **答案:D**
**CE** 是 **Compile Error** 的缩写,表示你的源代码存在语法错误,无法被编译器成功编译成可执行文件。
9. **答案:A**
OI 赛制(如 CSP-J/S、NOIP)通常采用按测试点或子任务计分的方式。选手每通过一个测试点或完成一个子任务的所有测试点,就能获得相应的分数,最终总分为各部分得分之和。
10. **答案:B**
ACM/ICPC 赛制(国际大学生程序设计竞赛)的排名规则是:首先比较**通过的题目总数**,通过题目数越多的队伍排名越靠前。在通过题目数相同的情况下,再比较**罚时**(Penalty Time),罚时越少的队伍排名越靠前。
11. **答案:NOI**
“全国青少年信息学奥林匹克竞赛”的英文全称是 **N**ational **O**lympiad in **I**nformatics,其缩写为 **NOI**。
12. **答案:D**
CSP-J/S 第一轮(初赛)是笔试,主要考查选手对计算机科学基础知识、算法原理、数据结构以及程序代码阅读理解的能力。
#### **题型二: 编译、命令与竞赛环境**
13. **答案:A**
在 Linux 或类 Unix 环境下,使用 `g++` 编译器编译 C++ 程序的标准命令格式是:`g++ <源文件名> -o <输出可执行文件名>`。
14. **答案:B**
`ls` (list) 命令用于列出当前目录下的文件和子目录。
15. **答案:C**
`pwd` (print working directory) 命令用于显示用户当前所在的工作目录的完整路径。
16. **答案:D**
`cd` (change directory) 命令用于切换当前工作目录。
17. **答案:A**
C++ 语言的源文件通常使用 `.cpp`、`.cc` 或 `.cxx` 作为扩展名,其中 `.cpp` 最为常见。
18. **答案:`g++`**
GNU Compiler Collection (GCC) 中用于编译 C++ 语言的编译器命令通常是 `g++`。
19. **答案:C**
断点是调试过程中的一个重要工具。在代码的某一行设置断点后,当程序执行到该行时会自动暂停,允许开发者检查此时的变量值、调用栈等程序状态,以便定位问题。
20. **答案:D**
“对拍”是一种在竞赛中常用的程序正确性验证方法。它通过编写一个简单但确保正确的暴力算法程序,和一个待测试的、可能更高效的程序,在大量自动生成的随机数据上运行这两个程序,并比较它们的输出结果是否一致。如果不一致,就说明待测程序存在 bug。
#### **题型三: 人物、奖项与学科历史**
21. **答案:A**
图灵奖(A.M. Turing Award)由美国计算机协会(ACM)设立,是公认的计算机科学领域的最高荣誉,常被称为“计算机界的诺贝尔奖”。
22. **答案:B**
本贾尼·斯特劳斯特卢普(Bjarne Stroustrup)于 20 世纪 80 年代初在贝尔实验室领导开发了 C++ 语言,它是在 C 语言的基础上扩展而来的。
23. **答案:C**
克劳德·香农(Claude Shannon)在 1948 年发表了划时代的论文《通信的数学理论》,为信息论和数字通信奠定了坚实的数学基础。
24. **答案:A**
冯·诺依曼体系结构的核心思想是“存储程序控制”,即计算机的指令(程序)和数据都以二进制形式统一存放在存储器中,并可以被 CPU 访问和处理。
25. **答案:B**
林纳斯·托瓦兹(Linus Torvalds)在 1991 年创建了 Linux 操作系统的内核,并将其开源,极大地推动了开源软件和操作系统的发展。
26. **答案:D**
蒂姆·伯纳斯-李(Tim Berners-Lee)在 1989 年发明了万维网(WWW),并开发了世界上第一个网页浏览器和服务器,为互联网的普及奠定了基础。
27. **答案:B**
**IOI** 是 **I**nternational **O**lympiad in **I**nformatics 的缩写,即国际信息学奥林匹克竞赛,是面向全球中最高水平的青少年信息学竞赛。
Loaded 100 of 1789 files, more files were not shown because too many files have changed in this diff. Show more