☰
小学生C++信息学竞赛课程----算法选择与思维训练(第一章 · 第二单元:暴力枚举——让每一种可能都接受检查!)
2026/10/13 11:01:20 网站建设 项目流程


第一章 · 总览与决策流程

第二单元:暴力枚举——让每一种可能都接受检查!

同学们,欢迎回到算法王国!

上一单元,我们绘制了算法王国的藏宝地图,学会了拿到一道题时,先分析题目要求、数据规模和问题特征。

今天,我们要认识算法王国里一支非常重要的探险队——暴力枚举队!

这支探险队有一个特别鲜明的特点:

不怕麻烦,不怕重复检查,只要可能的情况不多,就把它们有条理地检查一遍!


你可能会想:“这也算算法吗?这不就是一个一个地试吗?”

没错!但千万不要小看它。

暴力枚举是许多复杂算法的起点。学会枚举,我们不仅能解决一大批基础问题,还能为以后学习 DFS、剪枝、动态规划等算法打下重要基础。


今天,我们将跟随小探险家进入魔法宝藏城,完成以下任务:

  • 理解什么是暴力枚举,以及什么时候适合使用它。

  • 学会设计枚举范围,避免漏掉答案。

  • 掌握单层循环、双层循环和多层循环枚举。

  • 通过典型例题学习完整的解题过程。

  • 练习枚举数字、数字对、三位数和组合方案。

  • 学会分析时间复杂度,判断程序会不会太慢。

  • 完成一组自测题,检查自己是否真正掌握了枚举。

准备好了吗?出发!


一、第一站:什么是暴力枚举?

1. 国王的寻宝任务

魔法国王有 5 个宝箱,编号分别是:

1、2、3、4、5

其中只有一个宝箱藏着传说中的魔法钥匙。

国王告诉你:

  • 钥匙一定在这 5 个宝箱中的某一个。

  • 每次只能打开一个宝箱。

  • 打开后,就能知道里面有没有钥匙。

请问:怎样才能确保找到钥匙?


最简单、最可靠的办法就是:

  1. 打开 1 号宝箱。

  2. 如果没有,就打开 2 号宝箱。

  3. 如果没有,就打开 3 号宝箱。

  4. 继续检查,直到找到钥匙。

因为钥匙一定在这 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

第四步:逐步模拟

当前数字i

i % 3

是否输出

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 种选法

第五步:看懂双层循环的执行过程

外层i

内层j检查

符合条件的组合

0

1、2、3

(1,4)

1

2、3

(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:魔法城堡的三位数密码

题目要求

找出所有满足下列条件的三位数:

  1. 是三位数。

  2. 百位、十位、个位上的数字互不相同。

  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

第三步:模拟检查

候选数i

12 % i

是因数吗?

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),因为程序需要检查所有可能的数字对。


十一、本单元总结:你真正学会了什么?

今天,我们从魔法宝箱出发,认识了暴力枚举,并把它运用到了数字筛选、数字配对、三位数密码、因数统计和子集枚举等问题中。

请记住下面这张知识地图:


最后,请把暴力枚举的核心口诀记下来:

范围要完整,循环要清楚;

条件要准确,边界不马虎;

小数据敢枚举,大数据算复杂度。

下一单元,我们将进入算法王国的另一支探险队——贪心算法:每一步都做出选择,为什么有时能得到最优答案,又为什么有时会掉进陷阱?


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询