Initial commit: C++ courseware
This commit is contained in:
commit
4a4ee7c287
1789 files changed
+373489
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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`
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
前缀和与差分
|
||||
@@ -0,0 +1 @@
|
||||
讲解CSPJ的例题,主要以基础题为主
|
||||
@@ -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`。
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
CSPJ2026题目的讲解
|
||||
@@ -0,0 +1 @@
|
||||
讲解类与对象
|
||||
@@ -0,0 +1,393 @@
|
||||
# 第一章 C++ 程序设计基础
|
||||
|
||||
## 本章内容
|
||||
|
||||
- 1.1 程序设计的目标和流程
|
||||
- 1.2 简单数学运算
|
||||
- 1.3 变量与常量
|
||||
|
||||
## 1.1 程序设计的目标和流程
|
||||
|
||||
### 核心知识
|
||||
|
||||
程序设计的目标,是把现实问题中的处理步骤转化为计算机能够准确执行的指令。一个程序通常要完成三件事:
|
||||
|
||||
1. **接收数据**:获得题目给出的输入。
|
||||
2. **处理数据**:按照公式或规则计算。
|
||||
3. **给出结果**:严格按要求输出答案。
|
||||
|
||||
C++ 属于高级语言。我们编写的源代码不能直接由计算机执行,必须先经过编译。完整流程可以概括为:
|
||||
|
||||
```text
|
||||
分析问题 → 设计算法 → 编写源代码 → 编译与链接 → 运行程序 → 测试结果
|
||||
```
|
||||
|
||||
“编译成功”只表示代码符合语法规则并成功生成程序,不表示计算结果一定正确。
|
||||
|
||||
### 关键语法与规则
|
||||
|
||||
最小的 C++ 程序结构:
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
cout << "Hello, World!" << endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
各部分作用:
|
||||
|
||||
| 代码 | 作用 |
|
||||
| --- | --- |
|
||||
| `#include <iostream>` | 引入标准输入输出流库,使程序可以使用 `cin`、`cout` |
|
||||
| `using namespace std;` | 允许直接使用标准命名空间 `std` 中的工具 |
|
||||
| `int main()` | 主函数,也是程序的执行入口 |
|
||||
| `{ ... }` | 标出一个代码块的范围,左右花括号必须配对 |
|
||||
| `cout << ...;` | 向控制台输出数据 |
|
||||
| `return 0;` | 结束 `main`,向系统报告程序正常结束 |
|
||||
|
||||
书写代码时还要注意:
|
||||
|
||||
- C++ 区分大小写,`cout` 与 `Cout` 不是同一个名称。
|
||||
- 代码中的标点应使用英文半角符号。
|
||||
- 大多数语句以英文分号 `;` 结束。
|
||||
- 一个可独立运行的程序必须有且只有一个 `main` 函数。
|
||||
- `#include` 是预处理指令,末尾不写分号。
|
||||
|
||||
注释供程序员阅读,编译器会忽略注释内容:
|
||||
|
||||
```cpp
|
||||
// 单行注释:从 // 开始到本行末尾
|
||||
|
||||
/*
|
||||
块注释:可以跨越多行
|
||||
适合说明一段代码的整体功能
|
||||
*/
|
||||
```
|
||||
|
||||
### 例题
|
||||
|
||||
**题目:** 输出两行文字,第一行输出 `C++ Review`,第二行输出 `Ready!`。
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
cout << "C++ Review" << endl;
|
||||
cout << "Ready!" << endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**说明:** 程序从 `main` 开始执行。`cout` 负责输出,`endl` 负责换行,每条输出语句都以分号结束。
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 把 `cout` 使用的 `<<` 写成 `>>`。
|
||||
- 使用中文分号 `;`、中文引号 `“ ”` 或中文括号。
|
||||
- 漏写分号,或遗漏 `}`,使括号无法配对。
|
||||
- 变量名或工具名拼写错误。
|
||||
- 在 OJ 题目中擅自输出题目没有要求的内容。
|
||||
- 编译器标出的行不一定是真正写错的位置。遇到括号、分号问题时,应同时向前检查。
|
||||
|
||||
## 1.2 简单数学运算
|
||||
|
||||
### 核心知识
|
||||
|
||||
C++ 的基本算术运算符包括:
|
||||
|
||||
| 运算符 | 作用 | 示例 | 结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| `+` | 加法 | `5 + 2` | `7` |
|
||||
| `-` | 减法 | `5 - 2` | `3` |
|
||||
| `*` | 乘法 | `5 * 2` | `10` |
|
||||
| `/` | 除法 | `7 / 2` | `3` |
|
||||
| `%` | 求余 | `7 % 2` | `1` |
|
||||
|
||||
两个整数相除时执行**整数除法**,小数部分直接舍去,不进行四舍五入。只要参与除法的一方是浮点数,才会得到浮点结果。
|
||||
|
||||
### 关键语法与规则
|
||||
|
||||
```cpp
|
||||
cout << 7 / 2; // 3:整数除法
|
||||
cout << 7.0 / 2; // 3.5:浮点除法
|
||||
cout << 14 % 4; // 2:求余
|
||||
cout << (2 + 3) * 4; // 20:括号优先
|
||||
```
|
||||
|
||||
常见优先级从高到低:
|
||||
|
||||
1. 括号 `()`
|
||||
2. 乘、除、取余 `* / %`
|
||||
3. 加、减 `+ -`
|
||||
|
||||
`cout` 中双引号会影响输出含义:
|
||||
|
||||
```cpp
|
||||
cout << "2 + 4"; // 输出文本:2 + 4
|
||||
cout << 2 + 4; // 输出计算结果:6
|
||||
```
|
||||
|
||||
`%` 和整数除法经常配合使用:
|
||||
|
||||
```cpp
|
||||
int n = 479;
|
||||
int ones = n % 10; // 9:提取最右边一位
|
||||
n = n / 10; // 47:去掉最右边一位
|
||||
```
|
||||
|
||||
可以把 `% 10` 记作“个位提取器”,把 `/ 10` 记作“个位削减器”。
|
||||
|
||||
部分数学函数需要引入头文件 `<cmath>`:
|
||||
|
||||
```cpp
|
||||
#include <cmath>
|
||||
|
||||
double x = sqrt(81); // 计算根号 81,结果为 9
|
||||
double y = pow(2, 5); // 计算 2 的 5 次方,结果为 32
|
||||
```
|
||||
|
||||
### 例题
|
||||
|
||||
**题目:** 14 个苹果平均分给 4 名同学,输出每人分到的数量和剩余数量,中间用一个空格分隔。
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
cout << 14 / 4 << " " << 14 % 4;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```text
|
||||
3 2
|
||||
```
|
||||
|
||||
**说明:** `14 / 4` 得到整数商 3,`14 % 4` 得到余数 2。空格属于文本,因此写成 `" "`。
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 误以为 `7 / 2` 会得到 `3.5`。需要写成 `7.0 / 2` 或 `7 / 2.0`。
|
||||
- 对 `double` 使用 `%`。初学阶段把 `%` 理解为整数求余。
|
||||
- 把 `2 ^ 3` 当作 2 的 3 次方。C++ 中 `^` 不是乘方运算。
|
||||
- 忘记括号,导致计算顺序与题意不同。
|
||||
- 把算式放入双引号,程序只会原样输出算式文本。
|
||||
- 输出多个答案时漏掉题目要求的空格或换行。
|
||||
|
||||
## 1.3 变量与常量
|
||||
|
||||
### 核心知识
|
||||
|
||||
变量用于保存输入数据、计算结果和程序运行过程中的中间状态。变量可以理解为内存中带有名称的数据容器。
|
||||
|
||||
使用变量通常包含三个动作:
|
||||
|
||||
1. **定义**:说明变量的类型和名称。
|
||||
2. **初始化或赋值**:把数据保存到变量中。
|
||||
3. **读取或更新**:使用变量中的值,或用新值覆盖旧值。
|
||||
|
||||
`=` 是赋值运算符。程序会先计算等号右边,再把结果保存到左边变量中。它与数学中的“相等”含义不同。
|
||||
|
||||
常量用于保存程序运行期间不允许改变的数据。常量使用 `const` 定义,并且必须在定义时初始化。
|
||||
|
||||
### 关键语法与规则
|
||||
|
||||
变量定义、初始化与更新:
|
||||
|
||||
```cpp
|
||||
int a = 10; // 定义并初始化
|
||||
int b, c; // 同时定义两个 int 变量
|
||||
b = 3; // 给 b 赋值
|
||||
a = a + 5; // 先计算右边,再把 15 赋给 a
|
||||
a += 2; // 等价于 a = a + 2
|
||||
```
|
||||
|
||||
常见的复合赋值(将运算和赋值合并,一种简便写法):
|
||||
|
||||
| 写法 | 等价写法 |
|
||||
| --- | --- |
|
||||
| `a += b;` | `a = a + b;` |
|
||||
| `a -= b;` | `a = a - b;` |
|
||||
| `a *= b;` | `a = a * b;` |
|
||||
| `a /= b;` | `a = a / b;` |
|
||||
| `a %= b;` | `a = a % b;` |
|
||||
|
||||
变量命名规则:
|
||||
|
||||
- 只能由字母、数字和下划线组成。
|
||||
- 不能以数字开头。
|
||||
- 不能使用 `int`、`return`、`if` 等 C++ 关键字。
|
||||
- 同一作用域内不能重复定义同名变量。
|
||||
- 建议变量名见名知意,例如 `score`、`totalMinutes`、`radius`。
|
||||
|
||||
常量定义:
|
||||
|
||||
```cpp
|
||||
const double PI = 3.14159;
|
||||
// PI = 3.14; // 错误:常量不能再次赋值
|
||||
```
|
||||
|
||||
### 典型例题
|
||||
|
||||
**题目:** 输入圆的半径,输出圆的周长和面积。
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
const double PI = 3.14159;
|
||||
double r;
|
||||
cin >> r;
|
||||
|
||||
double length = 2 * PI * r;
|
||||
double area = PI * r * r;
|
||||
|
||||
cout << length << " " << area;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**说明:** 半径和计算结果可能含小数,因此使用 `double`。圆周率在程序运行期间不应改变,因此定义为常量。
|
||||
|
||||
**变量覆盖示例:猴子吃桃倒推。**
|
||||
|
||||
```cpp
|
||||
int peach = 1; // 第四天剩 1 个
|
||||
peach = (peach + 1) * 2; // 倒推第三天
|
||||
peach = (peach + 1) * 2; // 倒推第二天
|
||||
peach = (peach + 1) * 2; // 倒推第一天
|
||||
cout << peach; // 22
|
||||
```
|
||||
|
||||
每次赋值都先使用 `peach` 的旧值完成右侧计算,再用结果覆盖旧值。
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 变量没有定义就使用。
|
||||
- 变量未初始化就参与计算,可能得到不可预测的值。
|
||||
- 把 `=` 当作判断相等。赋值写 `a = 5;`,判断相等写 `a == 5`。
|
||||
- 写 `a + 1;` 却以为 `a` 会自动变化。更新变量应写 `a = a + 1;`、`a += 1;` 或 `a++;`。
|
||||
- 常量没有初始化,或在定义后再次赋值。
|
||||
- 变量名以数字开头、包含中文或使用了关键字。
|
||||
|
||||
## 本章练习
|
||||
|
||||
### 一、基础知识
|
||||
|
||||
1. C++ 程序从哪个地方开始执行?
|
||||
|
||||
<br><br>
|
||||
|
||||
2. `#include <iostream>` 的作用是什么?它的末尾是否需要分号?
|
||||
|
||||
<br><br>
|
||||
|
||||
3. 单行注释和块注释分别怎样书写?
|
||||
|
||||
<br><br>
|
||||
|
||||
4. 分别写出加、减、乘、除、求余对应的运算符。
|
||||
|
||||
<br><br>
|
||||
|
||||
5. 计算下列表达式的结果:
|
||||
- `13 / 5`
|
||||
- `13 % 5`
|
||||
- `13.0 / 5`
|
||||
- `(2 + 3) * 4`
|
||||
|
||||
6. 判断下列变量名是否合法,并说明原因:
|
||||
- `score1`
|
||||
- `1score`
|
||||
- `total_score`
|
||||
- `double`
|
||||
- `studentAge`
|
||||
|
||||
7. `=` 在 C++ 中表示什么?`a = a + 1;` 应怎样理解?
|
||||
|
||||
<br><br>
|
||||
|
||||
8. 为什么 `const double PI = 3.14159;` 必须在定义时给出初始值?
|
||||
|
||||
<br><br>
|
||||
|
||||
### 二、代码阅读
|
||||
|
||||
1. 写出程序的输出结果:
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
cout << "3 + 4" << " " << 3 + 4;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
<br><br><br>
|
||||
|
||||
2. 写出程序的输出结果:
|
||||
|
||||
```cpp
|
||||
int a = 5;
|
||||
a = a + 3;
|
||||
a *= 2;
|
||||
cout << a;
|
||||
```
|
||||
|
||||
<br><br><br>
|
||||
|
||||
3. 找出下面代码中的错误并改正:
|
||||
|
||||
```cpp
|
||||
#include <iostream>;
|
||||
using namespace std;
|
||||
|
||||
int Main {
|
||||
cout >> "Hello";
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
4. 写出 `ones` 和 `n` 的最终值:
|
||||
|
||||
```cpp
|
||||
int n = 628;
|
||||
int ones = n % 10;
|
||||
n = n / 10;
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
### 三、程序设计
|
||||
|
||||
1. **两数运算**
|
||||
|
||||
输入两个整数 `a` 和 `b`,依次输出它们的和、差、积,三个结果之间用一个空格分隔。
|
||||
|
||||
- 样例输入:`8 3`
|
||||
- 样例输出:`11 5 24`
|
||||
|
||||
2. **平均分苹果**
|
||||
|
||||
输入苹果总数 `m` 和学生人数 `n`,输出每人分到的苹果数以及剩余苹果数,中间用一个空格分隔。题目保证 `n > 0`。
|
||||
|
||||
- 样例输入:`17 5`
|
||||
- 样例输出:`3 2`
|
||||
|
||||
3. **圆的周长与面积**
|
||||
|
||||
输入圆的半径 `r`,使用常量 `PI = 3.14159`,输出圆的周长和面积,中间用一个空格分隔。
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,504 @@
|
||||
# 第二章 数据类型与顺序结构程序设计
|
||||
|
||||
## 本章内容
|
||||
|
||||
- 2.1 变量的数据类型
|
||||
- 2.2 变量的输入与输出
|
||||
- 2.3 顺序结构程序设计案例
|
||||
- 2.4 提交评测与错误自查
|
||||
|
||||
## 2.1 变量的数据类型
|
||||
|
||||
### 核心知识
|
||||
|
||||
数据类型决定变量能够保存什么数据、占用多少存储空间,以及参与运算时采用什么规则。基础课程中常用的数据类型包括整数、浮点数、字符和布尔值。
|
||||
|
||||
| 分类 | 类型 | 主要用途 | 示例 |
|
||||
| --- | --- | --- | --- |
|
||||
| 整数 | `short` | 较小范围整数(-128~127) | `short level = 10;` |
|
||||
| 整数 | `int` | 中范围整数,**最常用**(-21亿~21亿) | `int age = 15;` |
|
||||
| 整数 | `long long` | 大范围整数(非常大,万兆以上) | `long long total = 80000000;` |
|
||||
| 浮点数 | `float` | 单精度小数 | `float rate = 0.5;` |
|
||||
| 浮点数 | `double` | 双精度小数,**小数最常用** | `double price = 12.5;` |
|
||||
| 字符 | `char` | 单个字符 | `char grade = 'A';` |
|
||||
| 布尔值 | `bool` | 真或假 | `bool passed = true;` |
|
||||
|
||||
选择类型时要同时考虑数据范围和计算精度。题目可能出现小数时,一般使用 `double`;整数范围可能很大时,需要考虑 `long long`。
|
||||
|
||||
### `char`、转义字符与 ASCII
|
||||
|
||||
`char` 用来保存单个字符,字符值必须使用单引号:
|
||||
|
||||
```cpp
|
||||
char letter = 'A';
|
||||
char digit = '7'; // 数字字符,不是整数 7
|
||||
char op = '*';
|
||||
char blank = ' ';
|
||||
```
|
||||
|
||||
字符串使用双引号。`'A'` 是一个字符,`"A"` 是一个字符串,两者不能混用。
|
||||
|
||||
字符在内存中以整数编码保存。基础题通常使用 ASCII 规律处理英文字母和数字字符:
|
||||
|
||||
| 字符范围 | ASCII 值 |
|
||||
| --- | --- |
|
||||
| `'0'`~`'9'` | 48~57 |
|
||||
| `'A'`~`'Z'` | 65~90 |
|
||||
| `'a'`~`'z'` | 97~122 |
|
||||
|
||||
常用转换:
|
||||
|
||||
```cpp
|
||||
char c = '8';
|
||||
int number = c - '0'; // 数字字符转整数,得到 8
|
||||
|
||||
char upper = 'B';
|
||||
char lower = upper + ('a' - 'A'); // 大写字母转小写字母,得到 'b'
|
||||
|
||||
int code = 65;
|
||||
char letter = (char)code; // 强制转换后得到字符 'A'
|
||||
|
||||
char x = 'A';
|
||||
int ascii = (int)x; // 强制转换后得到整数 65
|
||||
```
|
||||
|
||||
转义序列以反斜杠 `\` 开头:
|
||||
|
||||
| 写法 | 含义 |
|
||||
| --- | --- |
|
||||
| `\n` | 换行 |
|
||||
| `\t` | 水平制表符(Tab键, 一般代表的 4 个空格) |
|
||||
| `\\` | 一个反斜杠 |
|
||||
| `\"` | 一个双引号 |
|
||||
| `\'` | 一个单引号 |
|
||||
|
||||
示例:
|
||||
|
||||
```cpp
|
||||
cout << "小明说:\"what is \\n\"" << '\n';
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```text
|
||||
小明说:"what is \n"
|
||||
```
|
||||
|
||||
### 类型转换
|
||||
|
||||
当表达式中出现不同数据类型时,编译器可能进行自动类型转换。一般而言,整数与浮点数混合运算时,整数会转换为浮点类型:
|
||||
|
||||
```cpp
|
||||
int a = 5;
|
||||
double b = 2.0;
|
||||
double result = a / b; // 2.5
|
||||
```
|
||||
|
||||
把 `double` 转为 `int` 会丢弃小数部分:
|
||||
|
||||
```cpp
|
||||
double value = 3.9;
|
||||
int n = (int)value; // 3,不进行四舍五入
|
||||
```
|
||||
|
||||
如果要转换整个表达式,应把表达式完整括起来:
|
||||
|
||||
```cpp
|
||||
char result = (char)(a + b);
|
||||
```
|
||||
|
||||
### 典型例题
|
||||
|
||||
**题目:** 输入一个大写英文字母,输出对应的小写字母。
|
||||
|
||||
```cpp
|
||||
int main() {
|
||||
char upper;
|
||||
cin >> upper;
|
||||
|
||||
char lower = (upper - 'A') + 'a'; // 也可通过加减 32 来实现, 大写转小写需要加 32
|
||||
cout << lower;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**说明:** ASCII 中大写字母和小写字母各自连续排列,而对应大小写字母的差值则一定相同。
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 把单个字符写成 `"A"`。`char` 值应使用单引号 `'A'`。
|
||||
- 混淆整数 `7` 与字符 `'7'`。`'7' - '0'` 才得到整数 7。
|
||||
- 让 `int` 保存需要保留小数的计算结果。
|
||||
- 强制类型转换前没有考虑数据范围和精度损失。
|
||||
- 直接假设任何字符都能通过加减 32 转换大小写。该规律只适用于对应的英文字母。
|
||||
- 想输出一个反斜杠时只写一个反斜杠。字符串中应写 `"\\"`。
|
||||
|
||||
## 2.2 变量的输入与输出
|
||||
|
||||
### 核心知识
|
||||
|
||||
`cin`、`cout` 是 C++ 的流式输入输出工具,通过 `<iostream>` 使用。`scanf`、`printf` 是格式化输入输出函数,需要添加 `<cstdio>` 使用。
|
||||
|
||||
输入多个数据时,变量的数量、类型和顺序必须与题意一致。
|
||||
|
||||
输出时不仅要保证答案数值正确,还要严格匹配题目要求的空格、换行、小数位数和文字内容。
|
||||
|
||||
### `cin` 与 `cout`
|
||||
|
||||
```cpp
|
||||
int a, b;
|
||||
cin >> a >> b; // 依次读入两个整数
|
||||
cout << a << " " << b; // 输出两个整数,中间一个空格
|
||||
cout << a + b << '\n'; // 输出计算结果并换行
|
||||
```
|
||||
|
||||
使用规则:
|
||||
|
||||
- `cin` 使用提取运算符 `>>`,把输入保存到变量。
|
||||
- `cout` 使用插入运算符 `<<`,把数据发送到控制台。
|
||||
- `cin >> a >> b;` 会跳过前导的空白字符,输入中的空格、Tab、回车通常都能分隔数据。
|
||||
- `cout << "a+b";` 输出文本 `a+b`;`cout << a + b;` 输出计算结果。
|
||||
- `endl` 和 `'\n'` 都能换行。
|
||||
|
||||
### `scanf` 与 `printf`
|
||||
|
||||
```cpp
|
||||
#include <cstdio>
|
||||
|
||||
int age;
|
||||
double height;
|
||||
char grade;
|
||||
|
||||
scanf("%d%lf %c", &age, &height, &grade);
|
||||
printf("%d %.2f %c\n", age, height, grade);
|
||||
```
|
||||
|
||||
常用格式说明符:
|
||||
|
||||
| 数据类型 | `scanf` 读取 | `printf` 输出 |
|
||||
| --- | --- | --- |
|
||||
| `int` | `%d` | `%d` |
|
||||
| `long long` | `%lld` | `%lld` |
|
||||
| `float` | `%f` | `%f` |
|
||||
| `double` | `%lf` | `%f` |
|
||||
| `char` | `%c` | `%c` |
|
||||
|
||||
输出小数位数:
|
||||
|
||||
```cpp
|
||||
double value = 3.1415926;
|
||||
printf("%.2f\n", value); // 3.14
|
||||
printf("%.3f\n", value); // 3.142
|
||||
```
|
||||
|
||||
`scanf` 要把数据写入变量,因此变量名前通常要加取地址符 `&`:
|
||||
|
||||
```cpp
|
||||
int n;
|
||||
scanf("%d", &n);
|
||||
```
|
||||
|
||||
格式字符串中的普通字符必须与输入对应:
|
||||
|
||||
```cpp
|
||||
scanf("%d%d", &a, &b); // 可输入:10 20
|
||||
scanf("%d,%d", &a, &b); // 必须输入:10,20
|
||||
```
|
||||
|
||||
`%c` 默认不会跳过空白字符。需要跳过已有的空格或换行时,可在 `%c` 前留一个空格:
|
||||
|
||||
```cpp
|
||||
char op;
|
||||
scanf(" %c", &op);
|
||||
```
|
||||
|
||||
### 典型例题
|
||||
|
||||
**题目:** 输入总水量 `t`(毫升)和人数 `n`。输出每人分到的水量,保留 3 位小数;第二行输出需要的杯子数,每人 2 个杯子。
|
||||
|
||||
```cpp
|
||||
int main() {
|
||||
double t;
|
||||
int n;
|
||||
scanf("%lf%d", &t, &n);
|
||||
|
||||
printf("%.3f\n", t / n);
|
||||
printf("%d\n", n * 2);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**说明:** `scanf` 读取 `double` 使用 `%lf`;`printf` 输出 `double` 使用 `%f`;`%.3f` 表示保留 3 位小数。
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 把方向写反:`cin` 使用 `>>`,`cout` 使用 `<<`。
|
||||
- `scanf` 漏写 `&`,导致无法把输入正确保存到变量。
|
||||
- 格式说明符与变量类型不匹配。此问题不只是“精度降低”,还可能导致读取失败或未定义行为。
|
||||
- 混淆 `double` 的格式:`scanf` 读取用 `%lf`,`printf` 输出用 `%f`。
|
||||
- `scanf("%d,%d", ...)` 要求输入中真的出现逗号。
|
||||
- 用 `%c` 读取字符时,意外读入了前一次输入留下的空格或换行。
|
||||
- OJ 输出中多了提示语、单位、标点或空格。
|
||||
|
||||
## 2.3 顺序结构程序设计案例
|
||||
|
||||
### 核心知识
|
||||
|
||||
顺序结构程序按照代码书写顺序逐条执行。基础题通常可以分成“输入、处理、输出”三层:
|
||||
|
||||
```cpp
|
||||
// 1. 输入
|
||||
cin >> ...;
|
||||
|
||||
// 2. 处理
|
||||
// 计算公式、更新变量、转换单位
|
||||
|
||||
// 3. 输出
|
||||
cout << ...;
|
||||
```
|
||||
|
||||
面对程序设计题时,可以使用七步法:
|
||||
|
||||
1. **读懂题目**:明确最终要计算什么。
|
||||
2. **理解输入**:确认数据的数量、类型、顺序和单位。
|
||||
3. **设计逻辑思路**:先写公式或处理步骤。
|
||||
4. **翻译成代码**:定义变量,把思路逐步写成 C++ 语句。
|
||||
5. **处理输出**:安排顺序、空格、换行和精度。
|
||||
6. **编译查错**:先解决语法错误。
|
||||
7. **样例验证**:输入样例并对照输出,再补充边界数据。
|
||||
|
||||
常见解题方法包括:
|
||||
|
||||
- 把复杂计算拆成多个中间结果,用变量保存。
|
||||
- 统一不同单位,再进行计算。
|
||||
- 使用临时变量,防止原数据在赋值时被覆盖。
|
||||
- 在关键位置临时输出中间变量,与手算结果逐步核对。
|
||||
|
||||
交换变量时需要第三个变量中转:
|
||||
|
||||
```cpp
|
||||
int a = 3, b = 4;
|
||||
int temp = a;
|
||||
a = b;
|
||||
b = temp;
|
||||
```
|
||||
|
||||
### 例题:小鱼的游泳时间
|
||||
|
||||
**题目:** 小鱼从当天 `a` 时 `b` 分游到 `c` 时 `d` 分。输入四个整数,输出游泳时长的小时数和分钟数。本题保证结束时间不早于开始时间。
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
int a, b, c, d;
|
||||
cin >> a >> b >> c >> d;
|
||||
|
||||
int startMinutes = a * 60 + b;
|
||||
int endMinutes = c * 60 + d;
|
||||
int duration = endMinutes - startMinutes;
|
||||
|
||||
int hours = duration / 60;
|
||||
int minutes = duration % 60;
|
||||
|
||||
cout << hours << " " << minutes;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**思路说明:**
|
||||
|
||||
1. 输入开始时间和结束时间,共 4 个整数。
|
||||
2. 把两个时刻统一换算为分钟。
|
||||
3. 用结束总分钟数减去开始总分钟数。
|
||||
4. 用 `/ 60` 得到小时,用 `% 60` 得到剩余分钟。
|
||||
5. 输入 `12 50 19 10` 时,应输出 `6 20`。
|
||||
|
||||
如果结果错误,可以临时输出中间变量:
|
||||
|
||||
```cpp
|
||||
cout << "start=" << startMinutes << '\n';
|
||||
cout << "end=" << endMinutes << '\n';
|
||||
cout << "duration=" << duration << '\n';
|
||||
```
|
||||
|
||||
提交 OJ 前要删除这些调试输出。
|
||||
|
||||
### 补充案例:按格式反转数字
|
||||
|
||||
输入形如 `123.4` 的固定格式数据,将其输出为 `4.321`。可以把输入看作 4 个字符和一个固定的小数点:
|
||||
|
||||
```cpp
|
||||
#include <cstdio>
|
||||
|
||||
int main() {
|
||||
char a, b, c, d;
|
||||
scanf("%c%c%c.%c", &a, &b, &c, &d);
|
||||
printf("%c.%c%c%c\n", d, c, b, a);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
这里的 `.` 是格式字符串中的普通字符,因此输入中也必须在对应位置出现小数点。
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 把样例数据直接写死,程序无法处理其他合法输入。
|
||||
- 时间差分别计算小时差和分钟差,却没有处理借位。
|
||||
- 交换变量时直接写 `a = b; b = a;`,使原值丢失。
|
||||
- 复杂公式全部写在一行,出错后难以定位。
|
||||
- 只测试样例,不测试整点、临界值、最小值等情况。
|
||||
- 调试输出未删除,导致答案格式不匹配。
|
||||
|
||||
## 2.4 提交评测与错误自查
|
||||
|
||||
### 提交流程
|
||||
|
||||
1. 在本地完成代码并成功编译。
|
||||
2. 输入题目样例,确认输出内容和格式一致。
|
||||
3. 把完整源代码提交到 OJ。
|
||||
4. 根据评测结果定位问题,修改后重新测试和提交。
|
||||
|
||||
OJ 只检查程序输出的字符序列是否符合要求。不要擅自添加输入提示、答案说明或单位。
|
||||
|
||||
### 常见评测结果
|
||||
|
||||
| 结果 | 含义 | 优先检查 |
|
||||
| --- | --- | --- |
|
||||
| **AC(Accepted)** | 通过全部测试数据 | 程序已通过本题评测 |
|
||||
| **CE(Compile Error)** | 编译错误 | 分号、括号、变量名、头文件、中文标点 |
|
||||
| **WA(Wrong Answer)** | 答案错误 | 公式、条件、类型、边界情况、输出格式 |
|
||||
| RE(Runtime Error) | 运行时错误 | 除数为 0、非法访问、错误输入函数用法 |
|
||||
| TLE(Time Limit Exceeded) | 超出时间限制 | 算法效率、循环终止条件 |
|
||||
|
||||
### 错误自查顺序
|
||||
|
||||
#### 出现 CE
|
||||
|
||||
1. 查看编译器给出的第一条错误信息和行号。
|
||||
2. 检查该行以及前一行是否漏写分号。
|
||||
3. 检查 `()`、`{}`、引号是否成对。
|
||||
4. 检查变量是否先定义后使用,名称是否拼写一致。
|
||||
5. 检查是否误用了中文标点或缺少头文件。
|
||||
|
||||
#### 出现 WA
|
||||
|
||||
1. 先检查输出格式:空格、换行、小数位数、文字大小写。
|
||||
2. 用草稿纸手算样例和一组自选数据。
|
||||
3. 分步输出中间变量,找出第一个与手算不一致的位置。
|
||||
4. 检查整数除法、数据类型和运算优先级。
|
||||
5. 检查最小值、最大值、临界值和特殊情况。
|
||||
|
||||
#### 出现 RE(目前较少遇见)
|
||||
|
||||
1. 检查除数是否可能为 0。
|
||||
2. 检查 `scanf` 的格式说明符和 `&`。
|
||||
|
||||
## 本章练习
|
||||
|
||||
### 一、基础知识
|
||||
|
||||
1. 为下列数据选择合适的类型:学生人数、商品价格、考试等级字符、条件真假、较大的整数总数。
|
||||
|
||||
<br><br>
|
||||
|
||||
2. 说明字符 `'7'` 与整数 `7` 的区别。
|
||||
|
||||
<br><br>
|
||||
|
||||
3. 写出 `'0'`、`'A'`、`'a'` 的 ASCII 值。
|
||||
|
||||
<br><br>
|
||||
|
||||
4. 写出以下转换表达式:
|
||||
- 数字字符 `ch` 转为对应的整数数字。
|
||||
- 大写英文字母 `upper` 转为对应的小写字母。
|
||||
- 整数 `code` 强制转换为字符。
|
||||
|
||||
5. 写出下列转义序列的作用:`\n`、`\t`、`\\`、`\"`。
|
||||
|
||||
<br><br>
|
||||
|
||||
7. `scanf` 读取 `int`、`double`、`char` 时分别使用什么格式说明符?
|
||||
|
||||
<br><br>
|
||||
|
||||
8. `printf` 输出 `double` 并保留两位小数时,格式说明符应怎样写?
|
||||
|
||||
<br><br>
|
||||
|
||||
9. `scanf` 中为什么通常要在变量名前加 `&`?
|
||||
|
||||
<br><br>
|
||||
|
||||
10. 分别说明 AC、CE、WA 的含义。
|
||||
|
||||
<br><br>
|
||||
|
||||
### 二、代码阅读与改错
|
||||
|
||||
1. 写出程序的输出结果:
|
||||
|
||||
```cpp
|
||||
char c = '6';
|
||||
int n = c - '0';
|
||||
cout << n + 4;
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
2. 写出程序的输出结果:
|
||||
|
||||
```cpp
|
||||
char upper = 'C';
|
||||
char lower = upper + ('a' - 'A');
|
||||
cout << lower;
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
<br>
|
||||
|
||||
3. 找出格式说明符的错误并改正:
|
||||
|
||||
```cpp
|
||||
int age;
|
||||
double height;
|
||||
scanf("%lf%d", &age, &height);
|
||||
printf("%d %.2d\n", age, height);
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
4. 找出变量交换失败的原因并改正:
|
||||
|
||||
```cpp
|
||||
int a = 3, b = 8;
|
||||
a = b;
|
||||
b = a;
|
||||
cout << a << " " << b;
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
### 三、程序设计
|
||||
|
||||
1. **字符与 ASCII**
|
||||
|
||||
输入一个字符,输出该字符以及它对应的 ASCII 值,中间用一个空格分隔。
|
||||
|
||||
- 样例输入:`A`
|
||||
- 样例输出:`A 65`
|
||||
|
||||
2. **数字字符转整数**
|
||||
|
||||
输入一个 `'0'`~`'9'` 的数字字符,将它转换成整数后加 5,并输出结果。
|
||||
|
||||
- 样例输入:`7`
|
||||
- 样例输出:`12`
|
||||
|
||||
6. **格式化平均值**
|
||||
|
||||
输入三个 `double` 数据,输出它们的平均值,结果需要严格保留 3 位小数。
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,628 @@
|
||||
# 第三章 关系、逻辑与分支结构
|
||||
|
||||
## 本章内容
|
||||
|
||||
- 3.1 关系表达式与逻辑表达式
|
||||
- 3.2 分支语句
|
||||
- 3.3 分支嵌套
|
||||
- 3.4 分支程序设计案例
|
||||
|
||||
## 3.1 关系表达式与逻辑表达式
|
||||
|
||||
### 核心知识
|
||||
|
||||
关系运算用于比较两个值,结果是 `bool` 类型,即 `true` 或 `false`。而逻辑运算符可以把多个真假条件组合成更复杂的判断。
|
||||
|
||||
C++ 在需要判断真假的位置,会把数值 `0` 视为假,把非零值视为真。`cout` 默认把 `true` 输出为 `1`,把 `false` 输出为 `0`。
|
||||
|
||||
### 关系运算符
|
||||
|
||||
| 运算符 | 含义 | 示例 |
|
||||
| --- | --- | --- |
|
||||
| `==` | 等于 | `score == 100` |
|
||||
| `!=` | 不等于 | `x != 0` |
|
||||
| `>` | 大于 | `a > b` |
|
||||
| `<` | 小于 | `a < b` |
|
||||
| `>=` | 大于等于 | `age >= 18` |
|
||||
| `<=` | 小于等于 | `score <= 60` |
|
||||
|
||||
关系表达式示例:
|
||||
|
||||
```cpp
|
||||
bool result1 = (10 > 5); // true
|
||||
bool result2 = (3 == 4); // false
|
||||
bool result3 = (7 != 0); // true
|
||||
```
|
||||
|
||||
### 逻辑运算符
|
||||
|
||||
| 表达式 | 含义 | 何时为真 |
|
||||
| --- | --- | --- |
|
||||
| `A && B` | A 并且 B | A、B 都为真 |
|
||||
| `A || B` | A 或者 B | A、B 至少一个为真 |
|
||||
| `!A` | 不是 A | A 为假 |
|
||||
|
||||
自然语言与 C++ 表达式对照:
|
||||
|
||||
```cpp
|
||||
// x 在闭区间 [10, 20] 内
|
||||
(x >= 10) && (x <= 20)
|
||||
|
||||
// x 小于 0 或大于 100
|
||||
(x < 0) || (x > 100)
|
||||
|
||||
// x 是 3 的倍数,但不是偶数
|
||||
(x % 3 == 0) && (x % 2 != 0)
|
||||
|
||||
// x 不等于 10
|
||||
!(x == 10) // 或者 x != 10
|
||||
```
|
||||
|
||||
构建逻辑表达式时,可以按三个步骤完成:
|
||||
|
||||
1. **拆解条件**:把长规则拆成独立的小判断。
|
||||
2. **寻找连接词**:“并且”对应 `&&`,“或者”对应 `||`,“不”对应 `!`。
|
||||
3. **组合表达式**:使用括号明确每一组条件之间的关系。
|
||||
|
||||
### 运算优先级
|
||||
|
||||
常用优先级从高到低可概括为(**非算关与或**):
|
||||
|
||||
```text
|
||||
括号 → 逻辑非 ! → 算术运算 → 关系运算 → && → ||
|
||||
```
|
||||
|
||||
对于一些复杂表达式应主动使用括号,不要只依赖优先级记忆,例如:
|
||||
|
||||
```cpp
|
||||
(math == 100 || english == 100) && homeworkFinished
|
||||
```
|
||||
|
||||
### 短路求值
|
||||
|
||||
对于 `A && B`:
|
||||
|
||||
- 如果 A 为假,整个表达式一定为假,B 不再执行。
|
||||
|
||||
对于 `A || B`:
|
||||
|
||||
- 如果 A 为真,整个表达式一定为真,B 不再执行。
|
||||
|
||||
```cpp
|
||||
int x = 0;
|
||||
int y = 10;
|
||||
|
||||
bool a = (x != 0) && (100 / x > 2);
|
||||
// 左侧为 false,右侧不执行,避免除以 0
|
||||
|
||||
bool b = (y > 0) || (++y > 20);
|
||||
// 左侧为 true,右侧不执行,y 仍为 10
|
||||
```
|
||||
|
||||
短路既能减少不必要的计算,也能保护右侧表达式不在非法条件下执行。
|
||||
|
||||
### 典型例题:闰年表达式
|
||||
|
||||
闰年规则为:年份能被 4 整除但不能被 100 整除,或者能被 400 整除。
|
||||
|
||||
拆解:
|
||||
|
||||
- A:`year % 4 == 0`
|
||||
- B:`year % 100 != 0`
|
||||
- C:`year % 400 == 0`
|
||||
|
||||
组合:
|
||||
|
||||
```cpp
|
||||
bool leap = ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
|
||||
```
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 判断相等时写成 `=`。`=` 是赋值,`==` 才是比较。
|
||||
- 写成数学连写 `5 < x < 10`。正确写法是 `x > 5 && x < 10`。
|
||||
- 把逻辑与、逻辑或写成 `&`、`|`。应使用 `&&`、`||`。
|
||||
- 区间题漏掉端点的等号。
|
||||
- 复杂条件没有括号,导致实际的计算优先级与题意不同。
|
||||
- 在逻辑表达式右侧使用 `x++` 等有副作用的操作,却忽略短路可能导致它不执行。
|
||||
|
||||
## 3.2 分支语句
|
||||
|
||||
### 核心知识
|
||||
|
||||
分支结构让程序根据条件选择不同的执行路径:
|
||||
|
||||
- `if`:只关心条件成立时需要做什么。
|
||||
- `if-else`:处理二选一。
|
||||
- `if-else if-else`:处理多个互斥选项,只执行首次满足的分支。
|
||||
- 多个独立 `if`:每个条件分别判断,可能执行多个分支。
|
||||
- `switch`:处理同一个整型或字符型表达式的多路等值选择。
|
||||
|
||||
### 单分支和双分支
|
||||
|
||||
```cpp
|
||||
if (condition) {
|
||||
// condition 为 true 时执行
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
if (condition) {
|
||||
// 条件成立时执行
|
||||
} else {
|
||||
// 条件不成立时执行
|
||||
}
|
||||
```
|
||||
|
||||
奇偶判断:
|
||||
|
||||
```cpp
|
||||
if (number % 2 == 0) {
|
||||
cout << "偶数";
|
||||
} else {
|
||||
cout << "奇数";
|
||||
}
|
||||
```
|
||||
|
||||
### 多分支
|
||||
|
||||
```cpp
|
||||
if (condition1) {
|
||||
// 第一种情况
|
||||
} else if (condition2) {
|
||||
// 第一种情况不成立,第二种情况成立
|
||||
} else {
|
||||
// 以上条件都不成立
|
||||
}
|
||||
```
|
||||
|
||||
多分支按照从上到下的顺序判断。一旦某个条件为真,程序执行对应代码块,然后跳过整条分支链。因此,条件书写顺序会影响结果。
|
||||
|
||||
例如对于成绩分级的实例,应首先判断更高的分数:
|
||||
|
||||
```cpp
|
||||
if (score >= 90) {
|
||||
cout << "A";
|
||||
} else if (score >= 80) {
|
||||
cout << "B";
|
||||
} else if (score >= 60) {
|
||||
cout << "C";
|
||||
} else {
|
||||
cout << "D";
|
||||
}
|
||||
```
|
||||
|
||||
而且对于多分支而言,后面的条件隐含了前面条件不成立。例如在执行 `score >= 80` 的分支时,已经知道 `score < 90`。
|
||||
|
||||
### 独立 `if` 与 `else if`
|
||||
|
||||
多个独立 `if` 属于“多选多”:
|
||||
|
||||
```cpp
|
||||
if (x % 2 == 0) {
|
||||
cout << "even ";
|
||||
}
|
||||
if (x % 3 == 0) {
|
||||
cout << "multiple of 3";
|
||||
}
|
||||
```
|
||||
|
||||
当 `x` 为 6 时,两个条件都成立,两段代码都会执行。
|
||||
|
||||
`if-else if` 属于“多选一”:前面的分支一旦命中,后面的分支不再判断。
|
||||
|
||||
### `switch` 多路选择
|
||||
|
||||
`switch` 适合对同一个表达式进行多个等值匹配:
|
||||
|
||||
```cpp
|
||||
switch (expression) {
|
||||
case constant1:
|
||||
// expression 等于 constant1
|
||||
break;
|
||||
case constant2:
|
||||
// expression 等于 constant2
|
||||
break;
|
||||
default:
|
||||
// 没有任何 case 匹配
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `switch` 后的 `expression` 表达式应为整型、字符型的值。
|
||||
- `case` 后的 `constant` 所匹配的值必须是常量,不能写范围条件,不能写变量。
|
||||
- `break` 会立即跳出 `switch`。
|
||||
- `default` 是可选的保底分支,作用类似多分支最后的 `else`。
|
||||
- 缺少 `break` 时,程序会从匹配位置继续向下执行直到遇到 `break` 为止,这种情况称为** `case` 穿透**。
|
||||
|
||||
在一些特殊情况,多个 `case` 可以有意使用穿透来共用一段逻辑:
|
||||
|
||||
```cpp
|
||||
switch (letter) {
|
||||
case 'a':
|
||||
case 'e':
|
||||
case 'i':
|
||||
case 'o':
|
||||
case 'u':
|
||||
cout << "元音";
|
||||
break;
|
||||
default:
|
||||
cout << "非元音";
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 在 `if` 条件后误写分号:
|
||||
|
||||
```cpp
|
||||
if (x > 0); { // 错误:分号提前结束了 if
|
||||
cout << x;
|
||||
}
|
||||
```
|
||||
|
||||
- 省略花括号后添加第二条语句,误以为两条语句都受 `if` 控制。
|
||||
- 把多选一写成多个独立 `if`,造成多个分支同时输出。
|
||||
- 把允许多个结果的情况写成 `else if`,导致后续条件被跳过。
|
||||
- 多分支先写宽松条件,导致严格条件永远没有机会命中。
|
||||
- 用 `switch` 处理 `score >= 60` 等范围条件。
|
||||
- `case` 后写变量、范围或重复的常量值。
|
||||
- 漏写 `break`,造成意外穿透。
|
||||
|
||||
## 3.3 分支嵌套
|
||||
|
||||
### 核心知识
|
||||
|
||||
分支嵌套,是在一个 `if`、`else if` 或 `else` 的代码块内部再放入一个完整的分支结构。
|
||||
|
||||
嵌套适合“先判断大前提,再处理细分情况”的问题:
|
||||
|
||||
- 外层分支确定主要类别或进入资格。
|
||||
- 内层分支只在外层条件成立后继续判断。
|
||||
- 如果外层条件不成立,内层条件不会执行。
|
||||
|
||||
### 语法与执行流程
|
||||
|
||||
```cpp
|
||||
if (外层条件) {
|
||||
if (内层条件) {
|
||||
// 外层条件和内层条件都成立
|
||||
} else {
|
||||
// 外层条件成立,但内层条件不成立
|
||||
}
|
||||
} else {
|
||||
// 外层条件不成立
|
||||
}
|
||||
```
|
||||
|
||||
执行过程:
|
||||
|
||||
1. 先判断外层条件。
|
||||
2. 外层条件为假时,跳过整个外层代码块。
|
||||
3. 外层条件为真时,进入代码块,再判断内层条件。
|
||||
4. 内层分支执行完成后,程序继续执行嵌套结构后面的代码。
|
||||
|
||||
### 典型例题:会员折扣
|
||||
|
||||
商场规则如下:
|
||||
|
||||
- 非会员按原价结算。
|
||||
- 会员消费满 500 元打 8 折。
|
||||
- 会员消费不足 500 元打 9 折。
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
double amount;
|
||||
bool isMember;
|
||||
cin >> amount >> isMember;
|
||||
|
||||
double payment = amount;
|
||||
|
||||
if (isMember) {
|
||||
if (amount >= 500) {
|
||||
payment = amount * 0.8;
|
||||
} else {
|
||||
payment = amount * 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
cout << payment;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**说明:** 外层先判断会员资格,只有会员才进入内层判断消费金额。非会员不进入内层,`payment` 保持原价。
|
||||
|
||||
### 常见错误
|
||||
|
||||
- 缩进混乱,无法看清 `else` 与哪个 `if` 配对。
|
||||
- 外层条件不成立时仍然期待内层判断执行。
|
||||
- 把所有条件都塞进多层嵌套,使代码难以阅读。能用清晰逻辑表达式解决时,不必强行嵌套。
|
||||
- 忘记为各层代码块补齐花括号。
|
||||
- 在不同层级重复或遗漏条件,导致某些情况没有处理。
|
||||
|
||||
## 3.4 分支程序设计案例
|
||||
|
||||
解决分支题时,可以采用四步流程:
|
||||
|
||||
1. **找到分岔点**:哪些条件会改变程序执行路径?
|
||||
2. **写出条件表达式**:把自然语言准确翻译为关系或逻辑表达式。
|
||||
3. **选择分支结构**:单选、二选一、多选一、多选多、嵌套或 `switch`。
|
||||
4. **实现各分支代码**:在每个分支写入对应处理代码。
|
||||
|
||||
### 案例一:BMI 分类
|
||||
|
||||
输入体重 `mass`(kg)和身高 `height`(m),计算 BMI。小于 18.5 输出 `Underweight`;小于 24 输出 `Normal`;否则输出 `Overweight`。
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
double mass, height;
|
||||
cin >> mass >> height;
|
||||
|
||||
double bmi = mass / (height * height);
|
||||
|
||||
if (bmi < 18.5) {
|
||||
cout << "Underweight";
|
||||
} else if (bmi < 24) {
|
||||
cout << "Normal";
|
||||
} else {
|
||||
cout << "Overweight";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
第二个分支隐含了第一个条件不成立,即 `bmi >= 18.5`,所以只需继续判断 `bmi < 24`。
|
||||
|
||||
### 案例二:闰年判断
|
||||
|
||||
输入年份。如果是闰年,输出该年份;否则输出 `No`。
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
int year;
|
||||
cin >> year;
|
||||
|
||||
bool leap = ((year % 4 == 0) && (year % 100 != 0))
|
||||
|| (year % 400 == 0);
|
||||
|
||||
if (leap) {
|
||||
cout << year;
|
||||
} else {
|
||||
cout << "No";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
本题先用关系和逻辑运算构建一个完整条件,再用 `if-else` 根据真假输出。
|
||||
|
||||
### 案例三:简易计算器
|
||||
|
||||
输入形如 `7 * 2` 的表达式,根据运算符完成计算。
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
double a, b;
|
||||
char op;
|
||||
cin >> a >> op >> b;
|
||||
|
||||
switch (op) {
|
||||
case '+':
|
||||
cout << a + b;
|
||||
break;
|
||||
case '-':
|
||||
cout << a - b;
|
||||
break;
|
||||
case '*':
|
||||
cout << a * b;
|
||||
break;
|
||||
case '/':
|
||||
if (b == 0) {
|
||||
cout << "Error";
|
||||
} else {
|
||||
cout << a / b;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
cout << "Unknown operator";
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
`switch` 负责对字符 `op` 做等值匹配;除法分支中再用嵌套 `if` 检查除数是否为 0。
|
||||
|
||||
## 本章练习
|
||||
|
||||
### 一、表达式练习
|
||||
|
||||
用 C++ 表达式表示下列条件:
|
||||
|
||||
1. `x` 大于等于 10 且小于等于 20。
|
||||
|
||||
<br>
|
||||
|
||||
2. `x` 小于 0 或大于 100。
|
||||
|
||||
<br>
|
||||
|
||||
3. `n` 是 5 的倍数。
|
||||
|
||||
<br>
|
||||
|
||||
4. `n` 能被 3 整除,但不能被 2 整除。
|
||||
|
||||
<br>
|
||||
|
||||
5. 字符 `ch` 是大写英文字母。
|
||||
|
||||
<br>
|
||||
|
||||
6. 年龄 `age` 不在 7~15 岁的范围内。
|
||||
|
||||
<br>
|
||||
|
||||
7. 数学或英语至少有一门达到 90 分,并且作业已经完成。变量分别为 `math`、`english`、`homeworkFinished`。
|
||||
|
||||
<br>
|
||||
|
||||
9. `a`、`b`、`c` 三个条件中至少有两个为真。
|
||||
|
||||
<br>
|
||||
|
||||
### 二、代码阅读
|
||||
|
||||
1. 写出程序的输出结果:
|
||||
|
||||
```cpp
|
||||
int x = 8;
|
||||
if (x > 5) {
|
||||
cout << "A";
|
||||
}
|
||||
if (x % 2 == 0) {
|
||||
cout << "B";
|
||||
}
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
2. 写出程序的输出结果,并说明原因:
|
||||
|
||||
```cpp
|
||||
int score = 85;
|
||||
if (score >= 60) {
|
||||
cout << "及格";
|
||||
} else if (score >= 80) {
|
||||
cout << "良好";
|
||||
} else if (score >= 90) {
|
||||
cout << "优秀";
|
||||
}
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
3. 写出 `y` 的最终值:
|
||||
|
||||
```cpp
|
||||
int x = 5;
|
||||
int y = 10;
|
||||
bool result = (x > 1) || (++y > 20);
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
4. 写出程序的输出结果:
|
||||
|
||||
```cpp
|
||||
int n = 2;
|
||||
switch (n) {
|
||||
case 1:
|
||||
cout << "A";
|
||||
case 2:
|
||||
cout << "B";
|
||||
case 3:
|
||||
cout << "C";
|
||||
break;
|
||||
default:
|
||||
cout << "D";
|
||||
}
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
5. 写出程序的输出结果:
|
||||
|
||||
```cpp
|
||||
bool loggedIn = true;
|
||||
bool isAdmin = false;
|
||||
|
||||
if (loggedIn) {
|
||||
if (isAdmin) {
|
||||
cout << "管理员";
|
||||
} else {
|
||||
cout << "普通用户";
|
||||
}
|
||||
} else {
|
||||
cout << "未登录";
|
||||
}
|
||||
```
|
||||
|
||||
<br><br>
|
||||
|
||||
6. 判断表达式 `5 < x < 10` 能否正确表示 `x` 在 5 和 10 之间,并说明计算过程。
|
||||
|
||||
<br><br>
|
||||
|
||||
### 三、程序改错
|
||||
|
||||
1. 改正下面的判断语句:
|
||||
|
||||
```cpp
|
||||
if (score = 100); {
|
||||
cout << "Full marks";
|
||||
}
|
||||
```
|
||||
|
||||
2. 下面的程序想判断 `x` 是否在闭区间 [10, 20] 内,请改正:
|
||||
|
||||
```cpp
|
||||
if (10 <= x <= 20) {
|
||||
cout << "Yes";
|
||||
}
|
||||
```
|
||||
|
||||
3. 下面的成绩分级程序可以正确的区分等级吗?
|
||||
|
||||
```cpp
|
||||
if (score >= 60) {
|
||||
cout << "C";
|
||||
} else if (score >= 80) {
|
||||
cout << "B";
|
||||
} else if (score >= 90) {
|
||||
cout << "A";
|
||||
} else {
|
||||
cout << "D";
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 四、程序设计
|
||||
|
||||
1. **奇偶判断**
|
||||
|
||||
输入一个整数,偶数输出 `Even`,奇数输出 `Odd`。
|
||||
|
||||
2. **景点门票**
|
||||
|
||||
某景点全价票 50 元。年龄不超过 6 岁或不低于 60 岁时票价为 25 元,其余情况为 50 元。输入年龄,输出票价。
|
||||
|
||||
- 样例输入:`65`
|
||||
- 样例输出:`25`
|
||||
|
||||
3. **三个数的最大值**
|
||||
|
||||
输入三个整数,使用分支语句输出其中的最大值。
|
||||
|
||||
- 样例输入:`6 5 2`
|
||||
- 样例输出:`6`
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
while 循环
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
while 循环的余下部分,把while循环的一尺之锤的例题讲解并让学生课堂完成,讲解ppt while循环的余下部分
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
while 循环的余下部分,讲解循环n次,讲解ppt while循环的累加与遍历和筛选模式
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
复习讲解循环n次,while循环的累加,讲解遍历和筛选模式
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
OJ 题目 P0058 翻倍达标、P0059 阶梯存钱、P0060 筛选编号
|
||||
|
||||
作业: B0055 小玉在游泳
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
数学运算初步
|
||||
@@ -0,0 +1 @@
|
||||
继续讲解数学运算初步,整除除法和小数除法,取余数,完成例题1-3,1-4,学会OJ系统的使用
|
||||
@@ -0,0 +1 @@
|
||||
完成例题1-4,讲解变量初步
|
||||
@@ -0,0 +1 @@
|
||||
完成例题1-4,讲解变量初步
|
||||
@@ -0,0 +1 @@
|
||||
讲解并完成书本例题 例1-10 猴子吃桃
|
||||
Reference in new issue
Block a user