第一章 · 总览与决策流程
第二单元:暴力枚举——让每一种可能都接受检查!
同学们,欢迎回到算法王国!
上一单元,我们绘制了算法王国的藏宝地图,学会了拿到一道题时,先分析题目要求、数据规模和问题特征。
今天,我们要认识算法王国里一支非常重要的探险队——暴力枚举队!
这支探险队有一个特别鲜明的特点:
不怕麻烦,不怕重复检查,只要可能的情况不多,就把它们有条理地检查一遍!
你可能会想:“这也算算法吗?这不就是一个一个地试吗?”
没错!但千万不要小看它。
暴力枚举是许多复杂算法的起点。学会枚举,我们不仅能解决一大批基础问题,还能为以后学习 DFS、剪枝、动态规划等算法打下重要基础。
今天,我们将跟随小探险家进入魔法宝藏城,完成以下任务:
理解什么是暴力枚举,以及什么时候适合使用它。
学会设计枚举范围,避免漏掉答案。
掌握单层循环、双层循环和多层循环枚举。
通过典型例题学习完整的解题过程。
练习枚举数字、数字对、三位数和组合方案。
学会分析时间复杂度,判断程序会不会太慢。
完成一组自测题,检查自己是否真正掌握了枚举。
准备好了吗?出发!
一、第一站:什么是暴力枚举?
1. 国王的寻宝任务
魔法国王有 5 个宝箱,编号分别是:
1、2、3、4、5
其中只有一个宝箱藏着传说中的魔法钥匙。
国王告诉你:
钥匙一定在这 5 个宝箱中的某一个。
每次只能打开一个宝箱。
打开后,就能知道里面有没有钥匙。
请问:怎样才能确保找到钥匙?
最简单、最可靠的办法就是:
打开 1 号宝箱。
如果没有,就打开 2 号宝箱。
如果没有,就打开 3 号宝箱。
继续检查,直到找到钥匙。
因为钥匙一定在这 5 个宝箱中,所以只要把所有宝箱检查完,就一定不会漏掉答案。
这就是暴力枚举的核心思想。
2. 用一句话理解暴力枚举
暴力枚举,就是按照一定的规则,把所有可能的候选情况逐一检查,找出符合条件的答案。
这里有三个重要词语:
所有可能: 不能随意漏掉一种情况。
逐一检查: 每次检查一种候选方案。
符合条件: 不符合要求的方案要排除。
注意,暴力枚举并不是毫无头绪地乱试。
优秀的枚举程序,一定有明确的枚举范围、清楚的枚举顺序,以及准确的判断条件。
3. 暴力枚举的三件套
第一件:枚举范围
我们要检查哪些候选答案?
例如:从 1 到 100 的整数。
第二件:逐一尝试
使用循环,把每种候选情况依次拿出来。
例如:使用for循环检查每一个整数。
第三件:判断条件
每次检查候选答案是否符合题目要求。
例如:如果一个数能被 3 整除,就把它记录下来。
我们可以把整个过程记成:
确定范围 → 逐个尝试 → 检查条件 → 记录答案。
以后看到一道题,如果你发现候选情况不多,而且每种情况都容易检查,就可以先问问自己:
“我能不能直接把所有可能都枚举一遍?”
这往往是非常好的解题起点。
二、第二站:最简单的枚举——检查每一个数字
典型例题 1:寻找魔法数字
题目故事
魔法学院的老师想找出 1 到 20 之间所有能被 3 整除的数字。
请你编写程序,输出这些数字。
第一步:分析题目
我们要做什么?
候选数字:1 到 20。
判断条件:能否被 3 整除。
输出要求:把所有符合条件的数字输出。
怎样判断一个整数 i 能否被 3 整除?
使用取模运算符%:
i % 3 == 0这里的%表示取余数。
例如:
6 % 3 == 0,所以 6 能被 3 整除。7 % 3 == 1,所以 7 不能被 3 整除。12 % 3 == 0,所以 12 能被 3 整除。
第二步:设计算法
我们不需要一个一个手动判断,只需要让计算机替我们检查。
第三步:编写 C++ 程序
#include <iostream> using namespace std; int main() { for (int i = 1; i <= 20; i++) { if (i % 3 == 0) { cout << i << " "; } } cout << endl; return 0; }运行结果:
3 6 9 12 15 18第四步:逐步模拟
当前数字 |
| 是否输出 |
|---|---|---|
1 | 1 | 否 |
2 | 2 | 否 |
3 | 0 | 是 |
4 | 1 | 否 |
5 | 2 | 否 |
6 | 0 | 是 |
… | … | … |
18 | 0 | 是 |
19 | 1 | 否 |
20 | 2 | 否 |
程序没有提前知道答案,而是从 1 检查到 20。
第五步:本题的核心知识
这道题的枚举范围只有一个数字变量,因此只需要一层循环。
时间复杂度为 O(n) ,这里 n = 20。如果把上限从 20 改为 n ,循环就大约执行 n 次。
小探险家记忆卡
枚举对象:一个整数。
枚举范围:1 到 20。
判断条件:
i % 3 == 0。循环层数:一层。
时间复杂度:O(n) 。
三、第三站:双层循环——让两位数字骑士组队
一层循环可以检查一个一个的数字。
可是,如果题目要求检查两个数字的组合,怎么办?
例如:
从一组数字中,找出和为 10 的两个数;
找出满足条件的两个不同编号;
统计有多少对数字符合要求。
这时,我们往往需要两层循环。
典型例题 2:魔法数字搭档
题目故事
魔法学校有 4 张数字卡片:
1、2、3、4
老师想知道:从中选出两张不同的卡片,它们的数字之和恰好等于 5,一共有多少种选法?
每张卡片只能选一次。
第一步:先用小学生的办法思考
我们把所有可能的搭档列出来:
1 和 2:和为 3。
1 和 3:和为 4。
1 和 4:和为 5,符合!
2 和 3:和为 5,符合!
2 和 4:和为 6。
3 和 4:和为 7。
所以,一共有两种选法:
( 1,4 ), ( 2,3 )
答案是 2。
但是,如果有 100 张卡片呢?有 1,000 张卡片呢?
我们总不能手动列出所有搭档吧?
让计算机来枚举!
第二步:为什么需要两层循环?
假设外层循环负责选择第一张卡片,内层循环负责选择第二张卡片。
这就是两层循环的基本结构:
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { // 检查 i 和 j 组成的情况 } }外层循环每走一步,内层循环就完整执行一轮。
不过,这段代码会检查(1,2)和(2,1),也会检查(1,1)。如果题目要求选择两张不同的卡片,而且不区分选择顺序,就应该避免这些重复。
第三步:改进枚举范围
我们规定:
1 < = i < j < = n
什么意思?
i < j:两张卡片不能是同一张。只考虑
i < j:不再重复检查交换顺序后的同一对。
例如,检查(1,4)后,就不需要再检查(4,1)。
第四步:完整 C++ 程序
#include <iostream> using namespace std; int main() { int a[4] = {1, 2, 3, 4}; int n = 4; int target = 5; int cnt = 0; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (a[i] + a[j] == target) { cout << a[i] << " " << a[j] << endl; cnt++; } } } cout << "总共有 " << cnt << " 种选法" << endl; return 0; }运行结果:
1 4 2 3 总共有 2 种选法第五步:看懂双层循环的执行过程
外层 | 内层 | 符合条件的组合 |
|---|---|---|
0 | 1、2、3 |
|
1 | 2、3 |
|
2 | 3 | 无 |
3 | 无 | 无 |
这里的数组下标从 0 开始,所以a[0]是数字 1,a[1]是数字 2,以此类推。
注意,内层循环并不是每次都执行 4 次:因为我们只允许j > i,所以随着i增大,内层循环的次数会逐渐减少。
第六步:复杂度分析
如果每个数字都和其他数字配对,双层循环通常需要 O(n^2) 级别的时间。
这段优化后的代码实际检查的组合数量为:
例如,n=4 时,一共检查:
种组合。
当 n 很大时,仍然属于 O(n^2) 。
小探险家记忆卡
一个循环:通常检查一组候选对象。
两个循环:可以检查两组对象组成的组合。
如果组合不区分顺序,可以尝试使用
j = i + 1,避免重复。双层循环通常会带来平方级的时间复杂度。
四、第四站:枚举中的大陷阱——漏掉、重复和越界
暴力枚举看起来简单,但有三个特别容易踩中的陷阱。
陷阱一:漏掉边界
如果题目要求检查 1 到 100 的整数,下面两种写法有什么区别?
for (int i = 1; i <= 100; i++)for (int i = 1; i < 100; i++)第一段会检查 1 到 100,第二段只检查 1 到 99。
如果 100 恰好是答案,第二段就会漏掉它。
所以,枚举范围必须严格对应题目要求。
陷阱二:重复检查
从 1、2、3、4 中选择两个数字:
(1, 4)和(4, 1)是不是同一种选法?
如果题目不区分顺序,它们就是同一种选法。
这时,可以让j从i + 1开始,避免重复。
但如果题目研究的是有方向的关系,例如“从城市 A 到城市 B”和“从城市 B 到城市 A”,两者可能并不相同。
因此,是否可以去重,要先看题目的规则。
陷阱三:数组越界
如果数组中有 5 个元素:
int a[5] = {10, 20, 30, 40, 50};合法下标是:
0、1、2、3、4
下面的写法就有问题:
for (int i = 0; i <= 5; i++) { cout << a[i] << endl; }因为i == 5时,程序访问了不存在的a[5]。
正确写法是:
for (int i = 0; i < 5; i++) { cout << a[i] << endl; }如果有n个元素,下标通常从 0 到n - 1,所以条件应写成:
i < n记住这句口诀:
范围先确认,边界要小心;不漏也不重,下标不越界。
五、第五站:三层循环——寻找三位数密码
魔法城堡的大门上有三个数字转盘。
第一个转盘表示百位,第二个表示十位,第三个表示个位。
国王留下了一条线索:
密码是一个三位数,三个数字互不相同,而且三个数字相加等于 6。请找出所有可能的密码!
这次,我们需要同时枚举百位、十位和个位。
典型例题 3:魔法城堡的三位数密码
题目要求
找出所有满足下列条件的三位数:
是三位数。
百位、十位、个位上的数字互不相同。
三个数字之和等于 6。
第一步:怎样表示一个三位数?
假设三位数是abc,其中:
a是百位数字。b是十位数字。c是个位数字。
那么这个三位数的数值就是:
100a + 10b + c
例如,数字 123:
由于它必须是三位数,所以:
1 < = a < = 9
而十位、个位可以是 0 到 9:
0 < = a ,b < = 9
第二步:设计三层循环
我们可以这样写:
for (int a = 1; a <= 9; a++) { for (int b = 0; b <= 9; b++) { for (int c = 0; c <= 9; c++) { // 判断三个数字是否符合要求 } } }三层循环就像三个数字转盘:
外层决定百位。
中层决定十位。
内层决定个位。
每确定前两位,就尝试所有可能的个位。
第三步:添加判断条件
三个数字互不相同,需要满足:
a != b && a != c && b != c三个数字相加等于 6,需要满足:
a + b + c == 6把条件合起来,就是:
if (a != b && a != c && b != c && a + b + c == 6)第四步:完整 C++ 程序
#include <iostream> using namespace std; int main() { int cnt = 0; for (int a = 1; a <= 9; a++) { for (int b = 0; b <= 9; b++) { for (int c = 0; c <= 9; c++) { if (a != b && a != c && b != c && a + b + c == 6) { cout << 100 * a + 10 * b + c << endl; cnt++; } } } } cout << "密码数量:" << cnt << endl; return 0; }第五步:程序为什么一定不会漏掉答案?
因为:
百位从 1 到 9,每一种都检查。
十位从 0 到 9,每一种都检查。
个位从 0 到 9,每一种都检查。
对每个三位数,都判断了题目要求的两个条件。
所以,所有符合条件的三位数都会被找到。
这正是枚举算法最重要的正确性思路:
只要候选范围覆盖全部可能,并且判断条件准确,就能找出所有符合要求的答案。
第六步:怎样减少无用检查?
刚才的程序最多检查:
9 * 10 * 10 = 900
种三位数。
其实,当 a+ b > = 6 时,个位数字 c 已经不可能是一个非负整数并使三数之和等于 6。我们可以进一步缩小搜索范围。
例如:
for (int a = 1; a <= 9; a++) { for (int b = 0; b <= 9; b++) { for (int c = 0; c <= 9; c++) { if (a + b + c == 6 && a != b && a != c && b != c) { cout << 100 * a + 10 * b + c << endl; } } } }这段代码虽然仍然枚举 900 种候选,但判断条件已经非常清楚。
如果要进一步优化,可以根据前面的数字缩小后面的枚举范围。不过,初学阶段先写出正确、清晰、不漏答案的程序,比一开始就追求复杂的优化更重要。
第七步:复杂度分析
三层循环分别大约执行 n 次时,总操作量约为:
n * n * n = n^3
因此,三层循环通常对应 O(n^3) 的时间复杂度。
本题的百位只有 9 种可能,十位和个位各有 10 种,所以候选数量最多只有 900 种,运行起来非常轻松。
但是,如果三个循环的范围都扩大到 100,000,就不能再轻易使用这种方法了。
记住:循环层数是观察复杂度的重要线索,但还要结合每层循环的实际范围。
六、第六站:用枚举找因数——数字城堡的守门员
魔法数字城堡有一个守门员。
它只允许某些数字通过:能够整除城堡编号的数字。
例如,城堡编号是 12,那么:
1 可以整除 12。
2 可以整除 12。
3 可以整除 12。
4 可以整除 12。
6 可以整除 12。
12 可以整除 12。
这些数字就是 12 的正因数。
典型例题 4:寻找城堡编号的所有因数
题目
输入一个正整数 n ,输出它的所有正因数,并统计因数的个数。
第一步:分析题目
我们不知道答案是什么,但知道正因数一定是 1 到 n 之间的整数。
因此,可以让i从 1 枚举到n。
每次判断:
n % i == 0如果余数是 0,说明i可以整除n,它就是一个因数。
第二步:完整 C++ 程序
#include <iostream> using namespace std; int main() { int n; cin >> n; int cnt = 0; for (int i = 1; i <= n; i++) { if (n % i == 0) { cout << i << " "; cnt++; } } cout << endl; cout << "因数个数:" << cnt << endl; return 0; }输入:
12输出:
1 2 3 4 6 12 因数个数:6第三步:模拟检查
候选数 |
| 是因数吗? |
|---|---|---|
1 | 0 | 是 |
2 | 0 | 是 |
3 | 0 | 是 |
4 | 0 | 是 |
5 | 2 | 否 |
6 | 0 | 是 |
7 | 5 | 否 |
… | … | … |
12 | 0 | 是 |
第四步:复杂度分析
循环从 1 运行到 n ,因此时间复杂度为:
O(n)
如果 n=100 ,只需要检查 100 个候选数。
如果 n=10^9,就需要检查约 10 亿个候选数,可能非常慢。
我们可以利用一个数学性质优化它:因数通常成对出现。
例如:
12 =2 * 6 =3 * 4
如果一个因数大于,它对应的另一个因数一定小于
。
因此,我们只需要枚举到,就能找到因数对。对于本题,如果只要求统计因数个数,还可以把每一对因数贡献的数量累加起来。
不过,初学者应先掌握最直接的枚举方法,再理解为什么可以优化。
这也是算法学习的一条重要路线:先解决问题,再研究怎样让它跑得更快。
七、第七站:枚举所有子集——小精灵的礼物袋
森林里有三件礼物:
魔法球 A
水晶 B
金苹果 C
小精灵可以选择其中任意几件,也可以一件都不选。
一共有多少种选法?
我们列一列:
选法编号 | 选择的礼物 |
|---|---|
1 | 什么都不选 |
2 | A |
3 | B |
4 | C |
5 | A、B |
6 | A、C |
7 | B、C |
8 | A、B、C |
一共 8 种。
为什么是 8 种呢?
因为每件礼物都有两种选择:
选它。
不选它。
三件礼物相互独立,所以总方案数是:
2 * 2 * 2 = 2^3 = 8
如果有 n 件礼物,每件都可以选或不选,那么子集总数就是:
2^n
典型例题 5:枚举三件礼物的所有选法
我们可以用二进制表示每件礼物的选择状态。
例如:
000:什么都不选。001:只选 A。010:只选 B。111:三件全部选。
对于三件礼物,可以枚举从 0 到 7 的所有状态。
#include <iostream> using namespace std; int main() { char gift[3] = {'A', 'B', 'C'}; for (int mask = 0; mask < 8; mask++) { cout << "方案 " << mask << ":"; for (int i = 0; i < 3; i++) { if (mask & (1 << i)) { cout << gift[i] << " "; } } cout << endl; } return 0; }运行结果中的方案会依次对应:
方案 0: 方案 1:A 方案 2:B 方案 3:A B 方案 4:C 方案 5:A C 方案 6:B C 方案 7:A B C这里出现了位运算:
mask & (1 << i)它用来检查第i位是否为 1。第i位为 1,就表示选择了对应礼物。
如果你还没有学习位运算,可以先理解“每件礼物只有选或不选两种状态”,以后学到二进制与状态压缩时,再深入学习这种写法。
复杂度分析
如果有 n 件礼物,一共有 2^n 个子集。
若每种方案都需要检查 n 件礼物,那么时间复杂度通常是:
O(n2^n)
当 n=3 时,只有 8 种方案。
但当 n=30 时,子集数量已经超过 10 亿种。
所以,枚举子集只适合规模较小的问题,或者需要配合剪枝、动态规划等方法进一步优化的情形。
这也为后续的状态压缩 DP埋下了一颗小小的种子。
八、变式训练:同一把枚举钥匙,可以打开多少扇门?
同学们,现在我们已经学习了:
单层循环枚举一个数字;
双层循环枚举数字对;
三层循环枚举三位数;
枚举一个整数的因数;
枚举所有子集。
接下来,我们通过更多题目练习,看看能不能把这些知识真正用起来。
变式题 1:寻找幸运数字
题目
魔法村庄认为,能同时被 2 和 5 整除的整数是幸运数字。
输入正整数 \(n\),输出 1 到 \(n\) 中所有幸运数字,并统计数量。
思路分析
能同时被 2 和 5 整除,就等价于能被 10 整除。
当然,我们也可以直接判断:
i % 2 == 0 && i % 5 == 0完整程序:
#include <iostream> using namespace std; int main() { int n; cin >> n; int cnt = 0; for (int i = 1; i <= n; i++) { if (i % 2 == 0 && i % 5 == 0) { cout << i << " "; cnt++; } } cout << endl; cout << cnt << endl; return 0; }输入:
50输出:
10 20 30 40 50 5思考升级: 为什么不用两层循环?
因为我们只需要判断每一个数字是否满足条件,不需要把两个数字组合起来。
变式题 2:寻找和为 10 的数字搭档
题目
有一组数字:
1 2 3 4 5 6 7 8 9
请找出所有两个不同数字的组合,使它们的和等于 10。每一组只输出一次。
思路分析
我们要检查数字对,所以使用双层循环。
为了避免重复:
j = i + 1参考程序:
#include <iostream> using namespace std; int main() { int a[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; for (int i = 0; i < 9; i++) { for (int j = i + 1; j < 9; j++) { if (a[i] + a[j] == 10) { cout << a[i] << " " << a[j] << endl; } } } return 0; }输出:
1 9 2 8 3 7 4 6为什么没有5 5?
因为题目要求两个不同数字,而且我们使用的是两张不同的卡片。这里只有一张数字 5 的卡片,不能重复使用。
思考升级: 如果允许同一个数字使用两次,程序应该怎样修改?这时需要重新明确题目中的“两个数”是两个数值,还是两个不同位置的元素。
变式题 3:寻找神奇的水仙花数
题目
三位水仙花数,是指一个三位数的各位数字的立方和等于这个数本身。
例如:
153 = 1^3 + 5^3 + 3^3
请找出所有三位水仙花数。
思路分析
三位数范围是 100 到 999,一共只有 900 个候选数。
所以,可以直接枚举每个三位数。
对于三位数i:
百位:
i / 100十位:
i / 10 % 10个位:
i % 10
参考程序:
#include <iostream> using namespace std; int main() { for (int i = 100; i <= 999; i++) { int a = i / 100; int b = i / 10 % 10; int c = i % 10; if (a * a * a + b * b * b + c * c * c == i) { cout << i << endl; } } return 0; }运行结果:
153 370 371 407这道题很好地展示了枚举的威力:我们不必事先知道水仙花数在哪里,只要完整检查 100 到 999,就能找到所有符合条件的数字。
变式题 4:寻找三数之和
题目
给定数组:
1 2 3 4 5
请找出所有三个不同位置的元素,使它们的和等于 9。每组只输出一次。
思路分析
这次需要选择三个位置:
第一层循环选择第一个位置。
第二层循环选择第二个位置。
第三层循环选择第三个位置。
为了不重复、不选择同一个位置,我们规定:
i < j < k
参考程序:
#include <iostream> using namespace std; int main() { int a[5] = {1, 2, 3, 4, 5}; for (int i = 0; i < 5; i++) { for (int j = i + 1; j < 5; j++) { for (int k = j + 1; k < 5; k++) { if (a[i] + a[j] + a[k] == 9) { cout << a[i] << " " << a[j] << " " << a[k] << endl; } } } } return 0; }输出:
1 3 5 2 3 4时间复杂度属于 O(n^3)。
对于只有 5 个数字的题目,这个方法很合适;如果数组有几十万个数字,就需要考虑更高效的算法。
变式题 5:统计能整除 24 的数字
题目
魔法宝箱的编号是 24。请输出 24 的所有正因数,并计算因数的个数。
思路分析
枚举 1 到 24,检查24 % i == 0。
参考程序:
#include <iostream> using namespace std; int main() { int n = 24; int cnt = 0; for (int i = 1; i <= n; i++) { if (n % i == 0) { cout << i << " "; cnt++; } } cout << endl; cout << "个数:" << cnt << endl; return 0; }输出:
1 2 3 4 6 8 12 24 个数:8思考升级: 如果只想判断 24 是不是完全平方数,还需要把 1 到 24 的每个数都检查一遍吗?不需要。我们可以进一步利用平方数的性质。枚举不只是遍历数字,也要学会缩小合理的搜索范围。
九、复杂度分析:暴力枚举到底会有多慢?
我们已经写了很多枚举程序。现在要学会回答一个重要问题:
当数据规模变大时,程序还能跑得动吗?
1. 常见枚举方式的复杂度
枚举方式 | 典型结构 | 时间复杂度 |
|---|---|---|
枚举一个数字 | 一层循环 | O(n) |
枚举数字对 | 两层循环 | O(n^2) |
枚举三个数字 | 三层循环 | O(n^3) |
枚举所有子集 | 2^n 种子集 | 通常为 O(n2^n),若逐个检查每个子集中的元素 |
枚举所有排列 | n! 种排列 | 通常至少与 n! 级别的候选数量有关 |
最后两行要特别注意:
如果只枚举所有子集编号,复杂度和逐个检查每个子集不是一回事。
如果只计算排列数量,和真正生成、输出所有排列,也不是一回事。
分析复杂度时,要看程序究竟做了什么。
2. 为什么循环层数很重要?
假设每一层循环都执行 n 次:
一层循环
大约检查 n 次:O(n)
两层循环
大约检查 n^2 次:O(n^2)
三层循环
如果 n = 100,最简单的估算就是 100^3=1,000,000 次组合检查。
大约检查 n^3 次:O(n^3)
循环嵌套通常会让操作数量快速增加,但也有例外。例如,内层循环的范围可能随外层变量改变,或者循环中途提前结束。
所以,循环层数是一个很好的入门线索,却不是全部答案。
3. 用数据规模帮助判断
下面是非常粗略的参考,假设机器、实现方式和时间限制都比较普通。
数据规模 | 可以优先考虑的方案 |
|---|---|
n < = 20 | 枚举、部分子集枚举等可能可行 |
n < = 1000 | O(n^2) 有时可行,需具体估算 |
通常优先考虑 O(nlog n)或 O(n) | |
通常考虑O(n),还需要认真检查输入输出和内存 | |
n 很大 | 可能需要数学性质、对数级算法或其他专门方法 |
这些不是硬性规定!例如,n=1000 时,某些O(n^2) 程序很轻松;但同时,另一些程序可能因为内部操作昂贵而很慢。
同样,n=20 的子集枚举也要注意具体复杂度。如果每个子集还要执行大量操作,就不能只看 2^20 这个数字。
4. 暴力枚举的优点与局限
优点
思路直观,容易理解;适合小规模问题;容易验证正确性;经常可以作为复杂算法的基础版本。
局限
当候选方案过多时,可能超时;枚举多个对象时,复杂度增长很快;如果范围或判断条件设计错误,也可能漏答案或重复统计。
请记住:暴力枚举不是“笨办法”,而是一种有明确适用范围的算法策略。
在小数据问题中,它常常是最简单、最可靠的选择;在大数据问题中,它也经常是我们寻找优化方法的起点。
十、终极挑战:暴力枚举自测营
同学们,恭喜你们已经完成了暴力枚举的基础训练!
现在进入自测环节。建议先独立完成题目,再查看参考答案。每道题都尽量写出自己的思考过程,而不只是选择一个答案。
第一组:基础概念题
6 道基础自测题
1. 暴力枚举最核心的思想是什么?
A. 只尝试第一个答案
B. 有条理地检查所有可能的候选情况
C. 只选择看起来最好的方案
2. 检查 1 到 n 的每个整数是否能被 5 整除,通常需要几层循环?
A. 一层
B. 两层
C. 三层
3. 枚举 n 个数字的所有数字对,常见的时间复杂度是什么?
A. O(1)
B. O(n)
C. O(n²)
4. 从 n 件物品中,每件物品都可以选或不选,一共有多少种子集?
A. n²
B. 2ⁿ
C. n!
5. 数组有 n 个元素,合法下标是 0 到 n-1。下面哪个循环范围正确?
A. i = 0; i < n; i++
B. i = 0; i <= n; i++
C. i = 1; i <= n; i++
6. 如果一道题的数据规模很小,暴力枚举一定不能使用吗?
A. 一定不能
B. 一定是最优算法
C. 不能一概而论,需要看题目和复杂度
第二组:程序阅读题
题目 7:数数小精灵
下面的程序会输出多少个数字?
#include <iostream> using namespace std; int main() { int cnt = 0; for (int i = 1; i <= 20; i++) { if (i % 4 == 0) { cnt++; } } cout << cnt << endl; return 0; }题目 8:寻找没有重复的数字搭档
下面程序中的循环条件为什么使用j = i + 1,而不是j = 0?
for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { // 检查一对数字 } }题目 9:找出程序的问题
下面的程序想输出数组中的全部 5 个元素。它有什么问题?
int a[5] = {2, 4, 6, 8, 10}; for (int i = 0; i <= 5; i++) { cout << a[i] << " "; }第三组:编程挑战题
下面两题作为课后练习。请先自己完成,再对照参考程序。
挑战题 10:魔法数字收藏家
题目要求
输入一个正整数 n ,输出 1 到 n 中所有同时满足以下条件的数字:
能被 3 整除;
不能被 5 整除。
最后输出符合条件的数字个数。
输入示例
20输出示例
3 6 9 12 18 个数:5参考程序
#include <iostream> using namespace std; int main() { int n; cin >> n; int cnt = 0; for (int i = 1; i <= n; i++) { if (i % 3 == 0 && i % 5 != 0) { cout << i << " "; cnt++; } } cout << endl; cout << "个数:" << cnt << endl; return 0; }解析
这道题只需要枚举一个数字,因此使用一层循环。
判断条件有两个:
i % 3 == 0:能被 3 整除。i % 5 != 0:不能被 5 整除。
两个条件必须同时成立,所以使用&&。
时间复杂度为 O(n) ,额外空间复杂度为 O(1) 。
挑战题 11:宝箱搭档大搜索
题目要求
有 6 个数字:
2 4 6 8 10 12
请找出所有不同位置的数字对,使它们的和等于 14。每组只输出一次,并统计共有多少组。
参考程序
#include <iostream> using namespace std; int main() { int a[6] = {2, 4, 6, 8, 10, 12}; int cnt = 0; for (int i = 0; i < 6; i++) { for (int j = i + 1; j < 6; j++) { if (a[i] + a[j] == 14) { cout << a[i] << " " << a[j] << endl; cnt++; } } } cout << "总共有 " << cnt << " 组" << endl; return 0; }参考输出
2 12 4 10 6 8 总共有 3 组解析
因为要选择两个不同位置,所以使用双层循环。
j = i + 1保证每一组只检查一次,不会把(2,12)和(12,2)当成两组。
时间复杂度为 O(n^2),因为程序需要检查所有可能的数字对。
十一、本单元总结:你真正学会了什么?
今天,我们从魔法宝箱出发,认识了暴力枚举,并把它运用到了数字筛选、数字配对、三位数密码、因数统计和子集枚举等问题中。
请记住下面这张知识地图:
最后,请把暴力枚举的核心口诀记下来:
范围要完整,循环要清楚;
条件要准确,边界不马虎;
小数据敢枚举,大数据算复杂度。
下一单元,我们将进入算法王国的另一支探险队——贪心算法:每一步都做出选择,为什么有时能得到最优答案,又为什么有时会掉进陷阱?