- juxq 的博客
基础算法之枚举算法教学讲义
- @ 2026-4-4 12:59:43
基础算法之枚举算法教学讲义
一、概念定义、价值意义
1.1 什么是枚举算法
枚举算法(Enumeration / Brute Force):将问题的所有可能解一一列举出来,逐一检验每个候选者是否满足题目条件,从而找到正确答案的算法策略。
枚举法又称穷举法,是计算机算法中最基础、最直观的方法。它不依赖巧妙的数学推导,而是利用计算机高速运算的能力,通过“全面撒网”来解决问题。
1.2 价值意义
- 思维简单:不需要复杂的算法设计,直接按照问题描述翻译成代码。
- 保证正确性:只要解空间有限且枚举完整,就不会遗漏任何解。
- 通用性强:几乎任何问题都可以用枚举法得到一个可行解(虽然可能超时)。
- 验证基准:常用于验证其他算法(如贪心、DP)的正确性,作为对拍程序。
- 竞赛实用:当数据范围很小(如 n ≤ 20,或取值范围 ≤ 10⁶)时,枚举往往是满分或部分分解法。
二、算法原理、算法框架、图示解析
2.1 核心思想
枚举算法的核心可以概括为四个字:列举、检验。
- 列举:确定解空间的范围,用循环或递归生成所有候选解。
- 检验:对每个候选解,判断其是否满足题目条件。
2.2 算法框架(伪代码)
Enumeration():
for each candidate in solution_space:
if check(candidate) == true:
record or output candidate
2.3 图示解析(图框内文字用简单英文)
Start
│
▼
┌───────────────┐
│ Define │
│ solution space│
└───────┬───────┘
│
▼
┌───────────────┐
│ Generate next │◄───┐
│ candidate │ │
└───────┬───────┘ │
│ │
▼ │
┌───────────────┐ │
│ Check if │ │
│ valid? │ │
└───────┬───────┘ │
│ │
┌────┴────┐ │
Yes No │
│ │ │
▼ │ │
┌───────┐ │ │
│Record │ │ │
└───┬───┘ │ │
│ │ │
└────────┼───────┘
│
more candidates?
│ │
Yes No
│ │
└─────┐ ▼
│ ┌───────┐
│ │Output │
│ │result │
│ └───────┘
│
(loop back)
三、代码模型、代码模板
3.1 基础模板:单层循环枚举
#include <bits/stdc++.h>
using namespace std;
int main() {
// 定义解空间范围 [L, R]
for (int i = L; i <= R; i++) {
if (check(i)) {
// 找到解
cout << i << endl;
}
}
return 0;
}
3.2 多重循环枚举(组合枚举)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n = 10; // 假设范围 1~10
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
for (int k = j + 1; k <= n; k++) {
if (check(i, j, k)) {
cout << i << " " << j << " " << k << endl;
}
}
}
}
return 0;
}
3.3 递归枚举(全排列、子集)
#include <bits/stdc++.h>
using namespace std;
vector<int> path;
bool used[10];
void dfs(int depth, int n) {
if (depth == n) {
// 生成一个完整候选解
if (check(path)) {
// 输出或记录
for (int x : path) cout << x << " ";
cout << endl;
}
return;
}
for (int i = 1; i <= n; i++) {
if (!used[i]) {
used[i] = true;
path.push_back(i);
dfs(depth + 1, n);
path.pop_back();
used[i] = false;
}
}
}
3.4 二进制枚举(子集)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n = 5;
vector<int> arr = {1, 2, 3, 4, 5};
// 枚举所有非空子集
for (int mask = 1; mask < (1 << n); mask++) {
vector<int> subset;
for (int i = 0; i < n; i++) {
if (mask >> i & 1) subset.push_back(arr[i]);
}
// 对 subset 进行检验
}
return 0;
}
四、基本要素、基本方法、基本步骤
4.1 基本要素
| 要素 | 说明 |
|---|---|
| 解空间 | 所有可能解的集合,必须能完整无遗漏地枚举 |
| 枚举方式 | 循环、递归、位运算等生成候选解的方法 |
| 检验条件 | 判断候选解是否为有效解的逻辑表达式 |
| 记录/输出 | 保存满足条件的解,或统计个数、求最值等 |
4.2 基本方法
- 直接循环枚举:适用于变量个数少、范围小的问题(如百钱买百鸡)。
- 多重循环枚举:适用于组合问题(如从 n 个数中选 m 个)。
- 递归回溯枚举:适用于排列、子集、N 皇后等问题。
- 二进制枚举:适用于子集枚举(用 0/1 表示选或不选)。
- STL 枚举:
next_permutation枚举全排列。
4.3 基本步骤
- 分析解空间:确定哪些变量需要枚举,它们的取值范围。
- 设计枚举方式:选择合适的循环结构或递归。
- 编写检验函数:将题目条件翻译成 C++ 表达式。
- 优化剪枝(可选):提前排除明显不可能的候选,减少枚举量。
- 输出结果:按题目要求输出(计数、打印、求最值等)。
五、经典例题(3题,含思路解析和详细注释)
例题1:百钱买百鸡(经典枚举优化)
题目描述
公鸡 5 元一只,母鸡 3 元一只,小鸡 1 元三只。用 100 元买 100 只鸡,问公鸡、母鸡、小鸡各多少只?输出所有可能的组合,每行一组。
思路解析
- 设公鸡 x 只,母鸡 y 只,小鸡 z 只。
- 约束:x + y + z = 100,5x + 3y + z/3 = 100,且 z 能被 3 整除。
- 直接三重循环:x:0~100,y:0~100,z:0~100,复杂度 101³ ≈ 10⁶,可接受。
- 优化:利用 x+y+z=100 消去 z,只需枚举 x 和 y,z = 100-x-y,再判断整除和价格。x 最多 20(520=100),y 最多 33(333=99),复杂度约 20*33=660。
代码实现(优化版)
#include <bits/stdc++.h>
using namespace std;
int main() {
// 公鸡最多 20 只,母鸡最多 33 只
for (int x = 0; x <= 20; x++) {
for (int y = 0; y <= 33; y++) {
int z = 100 - x - y;
if (z % 3 == 0 && 5 * x + 3 * y + z / 3 == 100) {
cout << x << " " << y << " " << z << endl;
}
}
}
return 0;
}
例题2:火柴棒等式
题目描述
给定 n 根火柴棒(n ≤ 24),拼出形如 A + B = C 的等式。数字 0~9 的火柴棒根数分别为 {6,2,5,5,4,5,6,3,7,6}。加号和等号各用 2 根。A、B、C 均为非负整数,且若数字不为 0 则最高位不能是 0。求能拼出的等式总数(A≠B 时交换算两个)。
思路解析
- 先确定枚举范围:n 最大 24,去掉加号和等号 4 根,剩余最多 20 根。数字 1 只用 2 根,所以最大数字不会太大(1111 约需 8 根)。经验范围 0~1111 足够。
- 预处理函数
cnt(x)计算数字 x 所需火柴根数。 - 枚举 A 和 B,计算 C = A + B,检查
cnt(A)+cnt(B)+cnt(C) == n-4。 - 注意:当 AB 且 A!=0 时,同一组等式只算一次?题目说如果 A!=B,则交换算两个;AB 时只算一个。实际上在枚举中,当 A==B 时,只会被枚举一次,所以不需要额外处理。但需注意 0+0=0 这种情况,火柴数 6+6+6+4=22,若 n=22 会被计入一次。
代码实现
#include <bits/stdc++.h>
using namespace std;
int cost[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
// 计算数字 x 需要多少根火柴
int getCost(int x) {
if (x == 0) return cost[0];
int res = 0;
while (x > 0) {
res += cost[x % 10];
x /= 10;
}
return res;
}
int main() {
int n;
cin >> n;
int total = 0;
int limit = 1111; // 经验上限
for (int a = 0; a <= limit; a++) {
for (int b = 0; b <= limit; b++) {
int c = a + b;
if (getCost(a) + getCost(b) + getCost(c) == n - 4) {
total++;
}
}
}
cout << total << endl;
return 0;
}
例题3:有效三角形个数(枚举所有三元组)
题目描述
给定一个包含非负整数的数组
nums,统计其中可以组成三角形三条边的三元组个数。例如,输入nums = [2,2,3,4],输出应为 3([2,3,4] 使用第一个2、[2,3,4] 使用第二个2、[2,2,3])。
思路解析
- 三重循环枚举所有三元组 (i, j, k) 且 i < j < k。
- 检验条件:任意两边之和大于第三边。由于三个数排序后只需检查最小两边之和大于最大边,但此处直接检验三个条件即可。
- 时间复杂度 O(n³),当 n ≤ 100 时可接受。
代码实现
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> nums(n);
for (int i = 0; i < n; i++) cin >> nums[i];
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
int a = nums[i], b = nums[j], c = nums[k];
if (a + b > c && a + c > b && b + c > a) {
count++;
}
}
}
}
cout << count << endl;
return 0;
}
六、课后练习(20题,含可直接点击的链接)
以下题目涵盖各种枚举技巧,难度递进,建议按顺序完成。
力扣(LeetCode)
| 序号 | 题目名称 | 题号 | 链接 |
|---|---|---|---|
| 1 | 两数之和 | 1 | https://leetcode.cn/problems/two-sum/ |
| 2 | 三数之和 | 15 | https://leetcode.cn/problems/3sum/ |
| 3 | 四数之和 | 18 | https://leetcode.cn/problems/4sum/ |
| 4 | 子集 | 78 | https://leetcode.cn/problems/subsets/ |
| 5 | 全排列 | 46 | https://leetcode.cn/problems/permutations/ |
信息学奥赛一本通(ybt.ssoier.cn)
| 序号 | 题目名称 | 题号 | 链接 |
|---|---|---|---|
| 6 | 百钱买百鸡 | 1022 | https://ybt.ssoier.cn/problem_show.php?pid=1022 |
| 7 | 统计平方和 | 1023 | https://ybt.ssoier.cn/problem_show.php?pid=1023 |
| 8 | 因子分解 | 1200 | https://ybt.ssoier.cn/problem_show.php?pid=1200 |
| 9 | 子集和问题 | 1291 | https://ybt.ssoier.cn/problem_show.php?pid=1291 |
| 10 | 排列 | 1199 | https://ybt.ssoier.cn/problem_show.php?pid=1199 |
洛谷(Luogu)
| 序号 | 题目名称 | 题号 | 链接 |
|---|---|---|---|
| 11 | 三连击 | P1008 | https://www.luogu.com.cn/problem/P1008 |
| 12 | 全排列问题 | P1706 | https://www.luogu.com.cn/problem/P1706 |
| 13 | 组合的输出 | P1157 | https://www.luogu.com.cn/problem/P1157 |
| 14 | 火柴棒等式 | P1149 | https://www.luogu.com.cn/problem/P1149 |
| 15 | 回文质数 | P1217 | https://www.luogu.com.cn/problem/P1217 |
Codeforces
| 序号 | 题目名称 | 题号 | 链接 |
|---|---|---|---|
| 16 | 1360A - Minimal Square | 1360A | https://codeforces.com/problemset/problem/1360/A |
| 17 | 1579B - Shifting Sort | 1579B | https://codeforces.com/problemset/problem/1579/B |
AtCoder
| 序号 | 题目名称 | 题号 | 链接 |
|---|---|---|---|
| 18 | ABC 085 B - Kagami Mochi | ABC085B | https://atcoder.jp/contests/abc085/tasks/abc085_b |
| 19 | ABC 122 B - ATCoder | ABC122B | https://atcoder.jp/contests/abc122/tasks/abc122_b |
AcWing
| 序号 | 题目名称 | 题号 | 链接 |
|---|---|---|---|
| 20 | 数的范围 | 789 | https://www.acwing.com/problem/content/791/ |
七、注意事项
- 解空间大小:枚举前务必估算时间复杂度,确保在题目时限内可运行。通常 1 秒内可执行约 10⁷~10⁸ 次简单操作。
- 循环变量范围:尽量通过数学分析缩小枚举范围(如百鸡问题中公鸡≤20)。
- 避免重复枚举:组合问题中,用
j=i+1等方式避免 (i,j) 和 (j,i) 重复。 - 浮点数精度:避免直接比较浮点数相等,可用整数运算代替(如小鸡价格用
z%3==0和整数运算)。 - 剪枝优化:在循环内提前判断不可能的情况,用
continue或break跳过。 - 递归枚举注意栈溢出:深度较大时改用非递归或 BFS。
- 使用 STL 加速:
next_permutation枚举全排列非常方便。 - 考虑边界条件:如空数组、单个元素、最大值最小值等。
八、课堂总结
| 核心要点 | 说明 |
|---|---|
| 枚举算法定义 | 列举所有可能解,逐一检验。 |
| 适用场景 | 解空间较小(如 n≤20,取值范围可枚举)的问题。 |
| 时间复杂度 | 通常 O(N^k),N 为单个变量取值范围,k 为变量个数。 |
| 优点 | 简单、可靠、不易出错,可作为验证基准。 |
| 缺点 | 效率低,不适用于大规模数据。 |
| 优化方向 | 缩小枚举范围、剪枝、降维(减少循环层数)。 |
| 与搜索算法关系 | 深度/广度优先搜索本质也是枚举,但利用了状态空间剪枝。 |
教练寄语:枚举算法是信息学竞赛的“基本功”,看似笨拙,却是通向更高级算法的必经之路。当你遇到一道题毫无思路时,不要忘记枚举——也许它就能为你赢得部分分甚至满分。更重要的是,通过枚举你能深入理解问题的解空间结构,为后续学习搜索、动态规划打下坚实基础。
参考资料
- 【书籍】《啊哈!算法》第1章:排序与枚举
- 【书籍】《信息学奥赛一本通》第5章:搜索与回溯
- 【在线资源】OI Wiki - 枚举
- 【在线资源】洛谷 - 枚举题单