# 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 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 #include 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 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 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 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 #include 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< using namespace std; int main() { for (int n=1;n<=1000;n++) { int sum=0; for (int i=1;i 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<