# 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 的情况下继续。
