Files
ClassFeedback/.claude/lesson/CSP03/CSP03-08_枚举算法.md

581 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 本身),从小到大排列。
**输入格式:**
- 一个正整数 n1 ≤ 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判断它是否是质数素数
**输入格式:**
- 一个正整数 n2 ≤ 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-91种数字可以由多根火柴棍组成请枚举找出可以用恰好 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找所有满足条件的数组
给定 n1≤n≤10找出所有满足长度为 n、元素均为 1 到 9、所有元素之积等于给定值 M 的严格递增序列。
**提示:** 递归枚举,每次选一个比上一个大的数,在乘积不超过 M 的情况下继续。