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

14 KiB
Raw Blame History

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

代码实现:

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
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

// 优化前三层循环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. 枚举 11000 的所有数,判断其是否是某整数的平方
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. 在 1100 中找满足某条件的整数
B. 在 n=10⁹ 的数组中找出所有满足条件的下标对
C. 在一个 4×4 的棋盘上找所有放置方式
D. 在 1
1000 中找所有质数

答案B
解析n=10⁹ 时,二层枚举需要 10¹⁸ 次,严重超时,需要更高效的算法。


4. 进阶扩展

枚举法的经典模板:

// 模板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

题解代码:

#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

输出格式:

  • YESNO

样例输入:

17

样例输出:

YES

题解代码:

#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只

题解代码:

#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

题解代码:

#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

题解代码:

#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

#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

#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 ...

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