更新各班级出勤登记与课评汇总,新增教学日程查询技能与CSP03枚举算法教案
This commit is contained in:
580
.claude/lesson/CSP03/CSP03-08_枚举算法.md
Normal file
580
.claude/lesson/CSP03/CSP03-08_枚举算法.md
Normal file
@@ -0,0 +1,580 @@
|
||||
# CSP03-08 枚举算法
|
||||
|
||||
## 一、课程简介(5分钟)
|
||||
|
||||
### 🎯 课程目标
|
||||
|
||||
1. 理解算法的三种描述方式:自然语言、流程图、伪代码
|
||||
2. 掌握枚举法(穷举法)的核心思想
|
||||
3. 理解枚举法的边界设计原则
|
||||
4. 能够用枚举法解决常见的穷举搜索类问题
|
||||
|
||||
### 📚 核心知识点
|
||||
|
||||
- 算法的描述方式:自然语言(文字说明)、流程图(图形表示)、伪代码(接近代码的文字)
|
||||
- 枚举法的定义:逐一列举所有可能的解,逐个验证
|
||||
- 枚举的范围(边界):如何确定起点和终点
|
||||
- 枚举的验证条件:什么样的值满足要求
|
||||
- 枚举的优化:剪枝、缩小枚举范围
|
||||
- 枚举法的时间复杂度分析
|
||||
|
||||
---
|
||||
|
||||
## 二、知识回顾(10分钟)
|
||||
|
||||
### 👩🏫 教师引导
|
||||
|
||||
> 同学们,我们在 CSP03 前几节学了数组和字符串。从这节课开始,我们进入**算法**的世界!
|
||||
>
|
||||
> 什么是算法?就是解决问题的**方法和步骤**。
|
||||
>
|
||||
> 今天先从最简单的算法思想开始——**枚举法**,也叫穷举法:用最"笨"的方法,把所有可能的答案都试一遍!
|
||||
>
|
||||
> 虽然"笨",但是有效!很多竞赛题用枚举就能解决!
|
||||
|
||||
**互动复习:**
|
||||
|
||||
> - for 循环可以控制变量从 a 到 b 遍历,这不就是在"枚举" a 到 b 的所有值吗?
|
||||
> - 学了枚举法,就相当于给 for 循环赋予了"算法内涵"!
|
||||
|
||||
---
|
||||
|
||||
## 三、新知讲解(45分钟)
|
||||
|
||||
### 1. 新知导入 🎬
|
||||
|
||||
> **猜数游戏:** 我脑子里想了一个 1~100 之间的整数,你能猜出来吗?
|
||||
>
|
||||
> 最"笨"的方法:从 1 猜到 100,总能猜到!——这就是枚举法!
|
||||
>
|
||||
> 实际上,枚举法是很多算法的基础。很多看似复杂的问题,暴力枚举都能解决——只要数据规模不太大。
|
||||
>
|
||||
> 信奥比赛中,枚举法写得好,可以得到部分分甚至满分!
|
||||
|
||||
---
|
||||
|
||||
### 2. 知识点讲解
|
||||
|
||||
#### 2.1 算法的三种描述方式
|
||||
|
||||
**① 自然语言**(文字描述):
|
||||
|
||||
> 找出 1~100 中所有能被 3 整除的数:
|
||||
> 从 1 遍历到 100,对每个数判断能否被 3 整除,若能则输出。
|
||||
|
||||
**② 流程图**(图形表示关键节点和流向):
|
||||
|
||||
```
|
||||
开始
|
||||
↓
|
||||
i = 1
|
||||
↓
|
||||
i <= 100? → 否 → 结束
|
||||
↓ 是
|
||||
i % 3 == 0? → 否 → i++
|
||||
↓ 是
|
||||
输出 i
|
||||
↓
|
||||
i++
|
||||
↓
|
||||
(返回判断)
|
||||
```
|
||||
|
||||
**③ 伪代码**(介于自然语言和代码之间):
|
||||
|
||||
```
|
||||
FOR i FROM 1 TO 100:
|
||||
IF i MOD 3 == 0:
|
||||
OUTPUT i
|
||||
```
|
||||
|
||||
**代码实现:**
|
||||
|
||||
```cpp
|
||||
for (int i = 1; i <= 100; i++) {
|
||||
if (i % 3 == 0) cout << i << " ";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.2 枚举法的核心思想
|
||||
|
||||
**枚举法 = 确定范围 + 逐一验证**
|
||||
|
||||
**三要素:**
|
||||
|
||||
1. **枚举的对象**:什么是我们要找的"候选答案"?
|
||||
2. **枚举的范围(边界)**:候选答案的取值范围?
|
||||
3. **验证的条件**:候选答案满足什么条件才算正确答案?
|
||||
|
||||
---
|
||||
|
||||
#### 2.3 枚举的边界设计
|
||||
|
||||
**原则:宁可多枚举,不要漏掉正确答案!**
|
||||
|
||||
但也要避免枚举太多导致超时。
|
||||
|
||||
**示例:百钱百鸡问题**
|
||||
|
||||
100 文钱买 100 只鸡,鸡翁 5 文/只,鸡母 3 文/只,鸡雏 1 文/3只,三种都要买,各买几只?
|
||||
|
||||
**枚举分析:**
|
||||
|
||||
- 设公鸡 x 只(范围:0 到 20,因为 5×20=100)
|
||||
- 设母鸡 y 只(范围:0 到 33,因为 3×33=99<100)
|
||||
- 小鸡 z = 100 - x - y
|
||||
|
||||
**验证条件:**
|
||||
|
||||
- `z >= 0`(鸡的数量不为负)
|
||||
- `z % 3 == 0`(小鸡必须是 3 的倍数)
|
||||
- `5*x + 3*y + z/3 == 100`(总价格等于 100)
|
||||
|
||||
```cpp
|
||||
for (int x = 0; x <= 20; x++) {
|
||||
for (int y = 0; y <= 33; y++) {
|
||||
int z = 100 - x - y;
|
||||
if (z >= 0 && z % 3 == 0 && 5*x + 3*y + z/3 == 100) {
|
||||
cout << "公鸡:" << x << " 母鸡:" << y << " 小鸡:" << z << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 📌 黑板推演:两层循环,每次组合验证一种可能。虽然最多循环 21×34=714 次,但逻辑清晰,代码简洁。
|
||||
|
||||
---
|
||||
|
||||
#### 2.4 枚举法的时间复杂度
|
||||
|
||||
| 枚举层数 | 时间复杂度 | 适用规模(n) |
|
||||
|---------|-----------|------------|
|
||||
| 单层枚举 | O(n) | n ≤ 10⁸ |
|
||||
| 双层枚举 | O(n²) | n ≤ 10⁴ |
|
||||
| 三层枚举 | O(n³) | n ≤ 500 |
|
||||
|
||||
> ⚠️ 竞赛中,一般 10⁸ 次操作是 1 秒的上限,超过则可能超时!
|
||||
|
||||
---
|
||||
|
||||
#### 2.5 枚举法的优化(剪枝)
|
||||
|
||||
**剪枝**:在枚举过程中,提前跳过不可能是答案的情况。
|
||||
|
||||
**示例:** 找满足 a + b + c = 100 的正整数组合(a≤b≤c)
|
||||
|
||||
```cpp
|
||||
// 优化前(三层循环):O(n³)
|
||||
for (int a = 1; a <= 100; a++)
|
||||
for (int b = 1; b <= 100; b++)
|
||||
for (int c = 1; c <= 100; c++)
|
||||
if (a + b + c == 100) ...
|
||||
|
||||
// 优化后(剪枝):c = 100 - a - b,直接计算
|
||||
for (int a = 1; a <= 100; a++)
|
||||
for (int b = a; b <= 100; b++) { // b >= a
|
||||
int c = 100 - a - b;
|
||||
if (c >= b) ... // 满足 c >= b 才输出
|
||||
}
|
||||
```
|
||||
|
||||
**效率对比:** 三层循环 100³ = 10⁶ 次;优化后约 10⁴/2 次!
|
||||
|
||||
---
|
||||
|
||||
### 3. GESP 真题演练 ⚡
|
||||
|
||||
**抢答题 1(选择题):**
|
||||
|
||||
> 使用枚举法求 1 到 1000 中所有完全平方数(即某个整数的平方),最合理的枚举方式是( )
|
||||
>
|
||||
> A. 枚举 1~1000 的所有数,判断其是否是某整数的平方
|
||||
> B. 枚举 1~31 的所有整数 i,计算 i² 加入结果
|
||||
> C. 枚举 1~1000 的所有数对 (i, j),判断 i×j 是否满足
|
||||
> D. 随机猜测
|
||||
>
|
||||
> **答案:B**
|
||||
> 解析:√1000 ≈ 31.6,所以枚举 i 从 1 到 31,计算 i² 即可找到 1~1000 内所有完全平方数,效率最高。
|
||||
|
||||
**判断题 2:**
|
||||
|
||||
> 枚举法的核心思想是:逐一检验所有候选答案,找出满足条件的解。( )
|
||||
>
|
||||
> **答案:✓(正确)**
|
||||
|
||||
**抢答题 3:**
|
||||
|
||||
> 以下哪种问题**不适合**用枚举法解决?
|
||||
>
|
||||
> A. 在 1~100 中找满足某条件的整数
|
||||
> B. 在 n=10⁹ 的数组中找出所有满足条件的下标对
|
||||
> C. 在一个 4×4 的棋盘上找所有放置方式
|
||||
> D. 在 1~1000 中找所有质数
|
||||
>
|
||||
> **答案:B**
|
||||
> 解析:n=10⁹ 时,二层枚举需要 10¹⁸ 次,严重超时,需要更高效的算法。
|
||||
|
||||
---
|
||||
|
||||
### 4. 进阶扩展
|
||||
|
||||
**枚举法的经典模板:**
|
||||
|
||||
```cpp
|
||||
// 模板1:单变量枚举
|
||||
for (int x = 起点; x <= 终点; x++) {
|
||||
if (满足条件(x)) {
|
||||
处理答案;
|
||||
}
|
||||
}
|
||||
|
||||
// 模板2:双变量枚举
|
||||
for (int x = 起点1; x <= 终点1; x++) {
|
||||
for (int y = 起点2; y <= 终点2; y++) {
|
||||
if (满足条件(x, y)) {
|
||||
处理答案;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**枚举法的应用领域:**
|
||||
|
||||
- 数字问题(整数分解、质因数)
|
||||
- 组合问题(选 k 个数的方案)
|
||||
- 棋盘问题(八皇后、数独)
|
||||
- 密码暴力破解(有限字符集)
|
||||
|
||||
---
|
||||
|
||||
## 四、课堂练习(45分钟)🎈
|
||||
|
||||
### 练习 1(基础):找因数
|
||||
|
||||
**题目描述:**
|
||||
|
||||
输入一个正整数 n,输出 n 的所有因数(包括 1 和 n 本身),从小到大排列。
|
||||
|
||||
**输入格式:**
|
||||
- 一个正整数 n(1 ≤ n ≤ 10000)
|
||||
|
||||
**输出格式:**
|
||||
- 所有因数,空格分隔
|
||||
|
||||
**样例输入:**
|
||||
```
|
||||
12
|
||||
```
|
||||
|
||||
**样例输出:**
|
||||
```
|
||||
1 2 3 4 6 12
|
||||
```
|
||||
|
||||
**题解代码:**
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int main() {
|
||||
int n; cin >> n;
|
||||
for (int i = 1; i <= n; i++) {
|
||||
if (n % i == 0) cout << i << " ";
|
||||
}
|
||||
cout << endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 练习 2(基础):判断质数
|
||||
|
||||
**题目描述:**
|
||||
|
||||
输入一个正整数 n,判断它是否是质数(素数)。
|
||||
|
||||
**输入格式:**
|
||||
- 一个正整数 n(2 ≤ n ≤ 10000)
|
||||
|
||||
**输出格式:**
|
||||
- `YES` 或 `NO`
|
||||
|
||||
**样例输入:**
|
||||
```
|
||||
17
|
||||
```
|
||||
|
||||
**样例输出:**
|
||||
```
|
||||
YES
|
||||
```
|
||||
|
||||
**题解代码:**
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
using namespace std;
|
||||
int main() {
|
||||
int n; cin >> n;
|
||||
bool isPrime = true;
|
||||
for (int i = 2; i <= sqrt(n); i++) {
|
||||
if (n % i == 0) { isPrime = false; break; }
|
||||
}
|
||||
cout << (isPrime ? "YES" : "NO") << endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 练习 3(综合):百钱百鸡
|
||||
|
||||
**题目描述:**
|
||||
|
||||
100 文钱买 100 只鸡:公鸡 5 文/只,母鸡 3 文/只,小鸡 1 文/3只,每种至少一只,找出所有购买方案。
|
||||
|
||||
**输入格式:**(无输入)
|
||||
|
||||
**输出格式:**
|
||||
- 每行输出一种方案:`公鸡x只 母鸡y只 小鸡z只`
|
||||
|
||||
**样例输出:**
|
||||
```
|
||||
公鸡4只 母鸡18只 小鸡78只
|
||||
公鸡8只 母鸡11只 小鸡81只
|
||||
公鸡12只 母鸡4只 小鸡84只
|
||||
```
|
||||
|
||||
**题解代码:**
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int main() {
|
||||
for (int x = 1; x <= 19; x++) {
|
||||
for (int y = 1; y <= 32; y++) {
|
||||
int z = 100 - x - y;
|
||||
if (z > 0 && z % 3 == 0 && 5*x + 3*y + z/3 == 100) {
|
||||
cout << "公鸡" << x << "只 母鸡" << y << "只 小鸡" << z << "只" << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 练习 4(综合):找出所有三位水仙花数
|
||||
|
||||
**题目描述:**
|
||||
|
||||
水仙花数是这样的三位数:每一位数字的立方之和等于本身(如 153 = 1³+5³+3³)。输出所有水仙花数。
|
||||
|
||||
**输入格式:**(无)
|
||||
|
||||
**输出格式:**
|
||||
- 每行一个水仙花数
|
||||
|
||||
**样例输出:**(部分)
|
||||
```
|
||||
153
|
||||
370
|
||||
371
|
||||
407
|
||||
```
|
||||
|
||||
**题解代码:**
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int main() {
|
||||
for (int n = 100; n <= 999; n++) {
|
||||
int a = n / 100; // 百位
|
||||
int b = n / 10 % 10; // 十位
|
||||
int c = n % 10; // 个位
|
||||
if (a*a*a + b*b*b + c*c*c == n) {
|
||||
cout << n << endl;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 练习 5(进阶):两数之和
|
||||
|
||||
**题目描述:**
|
||||
|
||||
输入 n 个不同整数和目标值 target,从数组中找出两个数使它们的和等于 target,输出这两个数(先小后大)。保证有且只有一组解。
|
||||
|
||||
**输入格式:**
|
||||
- 第一行 n 和 target
|
||||
- 第二行 n 个整数
|
||||
|
||||
**输出格式:**
|
||||
- 两个整数,空格分隔
|
||||
|
||||
**样例输入:**
|
||||
```
|
||||
5 9
|
||||
2 7 11 15 4
|
||||
```
|
||||
|
||||
**样例输出:**
|
||||
```
|
||||
2 7
|
||||
```
|
||||
|
||||
**题解代码:**
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int a[1005];
|
||||
int main() {
|
||||
int n, target;
|
||||
cin >> n >> target;
|
||||
for (int i = 1; i <= n; i++) cin >> a[i];
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = i + 1; j <= n; j++) {
|
||||
if (a[i] + a[j] == target) {
|
||||
int x = min(a[i], a[j]);
|
||||
int y = max(a[i], a[j]);
|
||||
cout << x << " " << y << endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、课堂总结(5分钟)🌟
|
||||
|
||||
> 今天我们学习了算法世界的入门——**枚举法**!
|
||||
>
|
||||
> 枚举法三要素:
|
||||
> 1. **枚举对象**:谁是候选答案?
|
||||
> 2. **枚举范围(边界)**:从哪里到哪里?
|
||||
> 3. **验证条件**:判断当前候选是否正确
|
||||
>
|
||||
> 算法描述方式:自然语言、流程图、伪代码——三种说同一件事,理解即可。
|
||||
>
|
||||
> 下节课我们学"模拟算法"——让计算机按照现实逻辑一步步执行,解决更复杂的问题!
|
||||
|
||||
---
|
||||
|
||||
## 六、课后作业与拓展(10分钟)
|
||||
|
||||
### 📝 课后作业(3道)
|
||||
|
||||
#### 作业 1:输出 n 以内所有质数
|
||||
|
||||
输入正整数 n,输出 2 到 n 之间所有质数,每行一个。
|
||||
|
||||
**样例:** n=10 → 2 3 5 7
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
using namespace std;
|
||||
bool isPrime(int x) {
|
||||
if (x < 2) return false;
|
||||
for (int i = 2; i <= sqrt(x); i++) if (x%i==0) return false;
|
||||
return true;
|
||||
}
|
||||
int main() {
|
||||
int n; cin>>n;
|
||||
for (int i=2;i<=n;i++) if(isPrime(i)) cout<<i<<endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 作业 2:找完全数
|
||||
|
||||
正整数中,"完全数"是所有真因子(不包含本身)之和等于自身的数(如 6 = 1+2+3)。输出 1~1000 内的所有完全数。
|
||||
|
||||
**样例输出:** 6 28 496
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int main() {
|
||||
for (int n=1;n<=1000;n++) {
|
||||
int sum=0;
|
||||
for (int i=1;i<n;i++) if(n%i==0) sum+=i;
|
||||
if (sum==n) cout<<n<<endl;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 作业 3:找满足条件的三位数
|
||||
|
||||
找出所有三位整数 n,满足:n 是 3 的倍数,且 n 的各位数字之积也是 3 的倍数。
|
||||
|
||||
**样例输出(部分):** 111 123 ...
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int main() {
|
||||
for (int n=100;n<=999;n++) {
|
||||
int a=n/100, b=n/10%10, c=n%10;
|
||||
if (n%3==0 && (a*b*c)%3==0) cout<<n<<endl;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔥 拓展习题(尖子生挑战,7道)
|
||||
|
||||
#### 挑战 1:四平方和定理
|
||||
任意正整数都可以表示为最多4个完全平方数之和。对于给定的 n,找出使用最少个完全平方数表示 n 的方案(最少2个,输出具体方案中数量最少的一种)。
|
||||
|
||||
**提示:** 枚举所有 ≤ √n 的完全平方数组合。
|
||||
|
||||
#### 挑战 2:火柴棍数字
|
||||
用 n 根火柴棍可以摆出多少种不同的数字(0-9)?1种数字可以由多根火柴棍组成,请枚举找出可以用恰好 n 根火柴摆出的最大数。(火柴数:0→6,1→2,2→5,3→5,4→4,5→5,6→6,7→3,8→7,9→6)
|
||||
|
||||
**提示:** 动态规划或枚举,但对于简单情况可用枚举。
|
||||
|
||||
#### 挑战 3:扑克牌 24 点
|
||||
给定 4 张牌(1~13),判断能否通过加减乘除(允许括号)得到 24。
|
||||
|
||||
**提示:** 枚举 4 个数的所有排列(4! = 24 种)和运算符的所有组合(4³ = 64 种),以及括号方式(5种),共约 7680 种情况。
|
||||
|
||||
#### 挑战 4:最小覆盖矩形
|
||||
给定平面上 n 个点,找出面积最小的轴对齐矩形,能包含所有点(枚举法:找 x 和 y 方向上的最小最大值)。
|
||||
|
||||
**提示:** 找 min_x, max_x, min_y, max_y,面积即 (max_x-min_x)*(max_y-min_y)。
|
||||
|
||||
#### 挑战 5:质因数分解
|
||||
输入正整数 n,输出其质因数分解结果(如 12 = 2² × 3)。
|
||||
|
||||
**提示:** 从 2 开始枚举,能整除就统计个数并除去,直到 n == 1 或当前枚举数的平方大于 n。
|
||||
|
||||
#### 挑战 6:最小公倍数与最大公因数
|
||||
枚举法求两个整数 a、b 的最大公因数(GCD):枚举 1~min(a,b),找最大的能同时整除两者的数。
|
||||
|
||||
**提示:** 理解辗转相除法(Euclid算法)其实更优,但枚举在数值小时完全可行。
|
||||
|
||||
#### 挑战 7:找所有满足条件的数组
|
||||
给定 n(1≤n≤10),找出所有满足:长度为 n、元素均为 1 到 9、所有元素之积等于给定值 M 的严格递增序列。
|
||||
|
||||
**提示:** 递归枚举,每次选一个比上一个大的数,在乘积不超过 M 的情况下继续。
|
||||
Reference in New Issue
Block a user