# 第8课:枚举与模拟(三级工具降维) > 等级说明:枚举与模拟属于 GESP 三级知识。本课把它们作为工具,用来简化一级、二级的循环、分支与简单数学问题。 ## 教学目标 - 理解枚举算法的核心思想:列举所有可能,逐一验证 - 掌握枚举优化的两个技巧:缩小范围、提前终止 - 理解模拟算法的核心思想:按题目描述一步步翻译成代码 ## 核心考点:枚举范围与模拟执行顺序 ### 考什么 枚举题要求列出所有可能答案并逐一验证;模拟题要求按照题目描述复现一个变化过程。两者都依赖清晰的范围、条件和执行顺序,常考输出满足条件的方案、计数或最终状态。 ### 必须理解 枚举包含三步:确定候选变量、确定合法范围、写出验证条件。范围必须覆盖所有可能答案,但可以利用题目约束缩小。找到答案后是否 `break`,取决于题目要第一个答案还是全部答案。模拟则要把文字规则拆成按顺序执行的动作,明确每一步读取旧状态还是更新后的状态;若多个变量应同时变化,不能让前一个更新意外影响后一个计算。 ### 核心写法 ```cpp for (int candidate = lower; candidate <= upper; candidate++) { if (满足全部条件) { // 记录、计数或输出 candidate } } for (int step = 1; step <= totalSteps; step++) { // 按题意顺序更新当前状态 } ``` 枚举是在“答案空间”中找;模拟是在“时间步骤”中走。先判断题目属于哪一种,再确定循环变量的含义。 ### 常见错误 - 枚举范围少一个端点,漏掉合法答案。 - 未判断题目要全部解,找到第一个就错误 `break`。 - 把模拟步骤顺序颠倒,得到不同状态。 - 明明能由约束缩小范围,却枚举过多无效候选,造成超时。 ### 判断是否掌握 能指出循环变量代表候选答案还是执行步数,能给出完整上下界和验证条件,并能用手算前几步检查模拟更新顺序。 --- ## 题目一:鸡兔同笼(枚举入门) ### 核心代码(一句话) > 枚举的核心:**`for` 循环遍历所有可能的答案**,用 `if` 判断是否满足条件,满足就输出。 ### 自然语言思路 笼子里有鸡和兔,一共 n 个头、m 只脚。鸡有 2 只脚,兔有 4 只脚。问鸡和兔各有几只? 第一步,鸡的数量可能是 0 到 n(因为总共 n 个头),让 `chicken` 从 0 循环到 n。 第二步,兔的数量 = 总数 - 鸡数:`rabbit = n - chicken`。 第三步,验证脚数:`chicken * 2 + rabbit * 4 == m`,如果成立,这就是答案。 第四步,输出鸡和兔的数量。 > 枚举就像"地毯式搜索"——把所有可能性都试一遍,看哪个符合条件。 ### 伪代码 ``` 1. 输入 n(头数), m(脚数) 2. for chicken = 0 到 n: 3. rabbit = n - chicken 4. if chicken * 2 + rabbit * 4 == m: 5. 输出 chicken 和 rabbit ``` ### 真实代码 ```cpp #include using namespace std; int main() { int n, m; cin >> n >> m; for (int chicken = 0; chicken <= n; chicken++) { int rabbit = n - chicken; if (chicken * 2 + rabbit * 4 == m) { cout << "鸡:" << chicken << ",兔:" << rabbit << endl; } } return 0; } ``` ### 注意点 1. **枚举范围 0 到 n**:不要漏掉 0(可能全是兔子)和 n(可能全是鸡)。 2. **兔的数量直接算**:因为鸡兔总数固定,所以兔 = n - 鸡,不需要再套一层循环枚举兔。这是"间接枚举"——少一个变量就少一层循环。 3. **可能无解**:如果循环结束都没输出,说明数据有问题(比如输入 2 个头 100 只脚)。 4. **脚数奇偶性判断**:实际上可以先用 `if (m % 2 != 0)` 快速判断无解(因为每只动物脚数都是偶数),但这是优化技巧,初学者先掌握枚举思路。 --- ## 题目二:找完数(枚举进阶) ### 核心代码(一句话) > 完数判断的核心:**枚举所有小于 n 的正整数 i,如果 `n % i == 0`,i 就是 n 的因子,把因子累加起来**,最后比较和是否等于 n。 ### 自然语言思路 完数是指一个数等于它所有真因子(不包括自身)之和。比如 6 = 1 + 2 + 3,28 = 1 + 2 + 4 + 7 + 14。请找出 1000 以内的所有完数。 第一步,外层枚举:`for (n = 2; n <= 1000; n++)`,对每个 n 判断它是不是完数。 第二步,对每个 n,用内层循环从 1 到 n-1 找它的因子:`if (n % i == 0)` 则 `sum += i`。 第三步,遍历完所有因子后,`if (sum == n)` 则 n 是完数,输出。 第四步,优化:找因子只需要搜到 `n/2`(因为大于 n/2 的数不可能是 n 的因子,除了 n 自身)。不过用 `sqrt(n)` 也行但要处理成对出现的情况。 ### 伪代码 ``` 1. for n = 2 到 1000: 2. sum = 0 3. for i = 1 到 n-1: 4. if n % i == 0: 5. sum += i 6. if sum == n: 7. 输出 n ``` ### 真实代码 ```cpp #include using namespace std; int main() { for (int n = 2; n <= 1000; n++) { int sum = 0; // 找因子并累加(优化:只需搜到 n/2) for (int i = 1; i <= n / 2; i++) { if (n % i == 0) { sum += i; } } if (sum == n) { cout << n << " "; // 输出:6 28 496 } } cout << endl; return 0; } ``` ### 注意点 1. **sum 必须在内层循环前清零**:每次换一个 n,sum 要重新从 0 开始累加,忘了清零会累加前面所有数的结果。 2. **优化枚举范围**:因子搜索范围从 `n-1` 优化到 `n/2`,因为大于 n/2 且小于 n 的数不可能是 n 的因子。 3. **1 不是完数**:1 的真因子之和是 0(1 没有真因子),不等于 1,所以从 2 开始枚举。 4. **1000 以内的完数只有 3 个**:6、28、496。如果输出不止这些,说明逻辑有误。 --- ## 题目三:斐波那契数列(模拟算法) ### 核心代码(一句话) > 模拟的核心:**按照规则,用变量记录当前状态,一步一步更新**。斐波那契的规则是:**第三项 = 前两项之和**,用 `a, b` 两个变量滚动更新即可。 ### 自然语言思路 斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55……每一项等于前两项之和。输入 n,输出前 n 项。 第一步,用两个变量 `a` 和 `b` 分别记录"当前项"和"下一项",初始 `a = 1, b = 1`。 第二步,循环 n 次,每次输出 `a`(当前项),然后计算新的两项:`temp = a + b; a = b; b = temp;`。 第三步,模拟过程:a=1,b=1 → 输出1 → a=1,b=2 → 输出1 → a=2,b=3 → 输出2 → a=3,b=5 → 输出3 …… 就像"滚动更新"。 > 模拟算法 = 读懂题目规则 → 翻译成代码。不需要聪明,只需要认真。 ### 伪代码 ``` 1. 输入 n 2. a = 1, b = 1 3. for i = 1 到 n: 4. 输出 a 5. temp = a + b 6. a = b 7. b = temp ``` ### 真实代码 ```cpp #include using namespace std; int main() { int n; cin >> n; int a = 1, b = 1; for (int i = 1; i <= n; i++) { cout << a << " "; int temp = a + b; // 计算下一项 a = b; // a 前进 b = temp; // b 前进 } cout << endl; return 0; } ``` ### 注意点 1. **更新顺序不能错**:必须先算 `temp = a + b`(基于旧值),再更新 a 和 b。如果先改 a 再算 `a + b`,用的就是新的 a 了,结果全乱。 2. **n=1 和 n=2 的情况**:循环 1 次输出 1,循环 2 次输出 "1 1"。代码不用特殊处理,天然正确。 3. **数据范围**:斐波那契数列增长很快,第 46 项就超过 20 亿了。如果 n 大到 50+,要用 `long long` 替代 `int`(因为 `int` 最大约 21 亿)。 4. **另一种写法 — 数组存储**:`int fib[100]; fib[1]=1; fib[2]=1; fib[i]=fib[i-1]+fib[i-2];` 更直观但占内存。 --- ## 枚举 vs 模拟 对比 | 对比维度 | 枚举 | 模拟 | |----------|------|------| | 核心思路 | 列举所有可能 → 逐一验证 | 按照规则 → 逐步执行 | | 适用场景 | 答案范围有限、可以逐一验证 | 题目有明确的步骤描述 | | 典型题目 | 鸡兔同笼、百钱百鸡、完数 | 斐波那契、杨辉三角、日期计算 | | 优化方向 | 缩小范围、提前终止、剪枝 | 找规律、用公式替代逐步模拟 | --- ## 本课打油诗 > 枚举就是全试过,循环里面 if 判断。 > 范围要想清边界,找到答案就停站。 > 模拟照着步骤走,题目说啥咱干啥。 > 变量更新看顺序,一步一步不出错。 --- ## 课后作业 1. 找出 1000 以内所有的"亲密数对"(a 的因子之和等于 b,b 的因子之和等于 a,且 a ≠ b) 2. 百钱百鸡:公鸡 5 元/只,母鸡 3 元/只,小鸡 1 元 3 只。用 100 元买 100 只鸡,输出所有方案 3. 输出杨辉三角的前 n 行 4. 输出所有三位数的水仙花数(如 153 = 1³ + 5³ + 3³)