不知道答案是什么,但知道答案藏在一个有限的范围里——那就把这个范围里的每一个可能都试一遍。
枚举法的思路非常朴素:如果不知道答案具体是什么,但知道答案一定落在某个有限的范围里,那就把这个范围里的每一个可能"挨个试一遍",每试一个就检查它是否满足题目的条件——满足就留下,不满足就跳过。试完整个范围,答案自然就出来了。
其实模块一已经在不知不觉中多次用过这个思路了:1.3 节判断质数最基础的写法,是把 2 到 n-1 之间的每个数都拿来试除;1.4 节求最大公约数最基础的写法,是把 min(a,b) 到 1 之间每个数都拿来试一遍能不能同时整除两个数;1.5 节求最小公倍数,是把较大数的倍数一个个列出来试。这些"基础写法"全部都是枚举法——这一节要做的,是把这个套路单独拿出来,正式地讲一遍。
经典的"鸡兔同笼"问题:笼子里关着鸡和兔,一共数到 35 个头,94 只脚,问鸡和兔各有多少只?
用代数能解,但用枚举法更简单:鸡的数量一定在 0 到 35 之间(不可能比总头数还多)。只要枚举鸡的数量 chicken,兔子的数量自然就是 35 - chicken(因为头数加起来必须是 35);再检查这种情况下脚的总数是不是正好 94——满足就是答案。
| 1 | int heads = 35, legs = 94; |
| 2 | for (int chicken = 0; chicken <= heads; chicken++) |
| 3 | { |
| 4 | int rabbit = heads - chicken; // 头数加起来必须是35 |
| 5 | if (chicken * 2 + rabbit * 4 == legs) |
| 6 | { |
| 7 | cout << "鸡:" << chicken << " 兔:" << rabbit << endl; |
| 8 | } |
| 9 | } |
| chicken | rabbit = 35-chicken | 脚的总数 | 是否等于 94? |
|---|---|---|---|
| 0 | 35 | 140 | 不等,跳过 |
| 1 | 34 | 138 | 不等,跳过 |
| … 中间还要试 21 次,都不满足 … | |||
| 22 | 13 | 96 | 不等,跳过 |
| 23 | 12 | 94 | ✓ 正好相等!找到答案 |
chicken=23 时,rabbit=12,脚的总数 23×2+12×4=46+48=94,正好对上——鸡 23 只,兔 12 只。整个过程只试了 24 次(chicken 从 0 到 23),比凑代数方程直接好懂。但有些问题,几个未知数之间互相独立,没法用一个把另一个"顶"出来,这时候就需要嵌套循环——外层循环枚举第一个变量,内层循环在外层每一种取值下,再枚举第二个变量,把所有组合都试一遍。
经典例子:找出 1 到 20 之间所有的勾股数组——也就是三个数 a < b < c,满足 a² + b² = c²(直角三角形两条直角边的平方和等于斜边的平方,比如经典的 3、4、5)。
| 1 | int limit = 20; |
| 2 | for (int a = 1; a <= limit; a++) |
| 3 | { |
| 4 | for (int b = a + 1; b <= limit; b++) // b 从 a+1 开始,保证 a<b |
| 5 | { |
| 6 | int cSquare = a * a + b * b; |
| 7 | int c = (int)sqrt(cSquare); |
| 8 | if (c * c == cSquare && c <= limit) |
| 9 | { |
| 10 | cout << a << " " << b << " " << c << endl; |
| 11 | } |
| 12 | } |
| 13 | } |
sqrt() 算出来的是浮点数,直接转成 int 可能因为精度误差差 1(比如该是 5 却算成 4.999999)。第 8 行用 c * c == cSquare 再做一次整数验证,就是为了避免被这种浮点误差坑——这和 1.3 节判断质数时强调的 i * i <= n 是同一个道理:能用整数运算验证的地方,尽量不要依赖浮点数。a=3,b=4 那一格写着 5,表示 3²+4²=5²,是一组合法的勾股数组。这张表只展示了命中的部分,实际代码会把 a 从 1 到 20、b 从 a+1 到 20 的所有组合都检查一遍——一共检查了 190 对组合,最终找到 6 组勾股数:(3,4,5) (5,12,13) (6,8,10) (8,15,17) (9,12,15) (12,16,20)。枚举法最大的优点是简单可靠,缺点是慢——尤其是嵌套循环,每多一层循环,总的检查次数往往是相乘而不是相加。如果 a、b 各自的范围是 n,双层枚举大约要检查 n² 次;三层枚举大约要检查 n³ 次——范围一旦变大,次数会爆炸式增长。
竞赛里有一个常用的粗略估算:一台普通电脑每秒大约能执行 1 亿(10⁸)次简单运算。题目通常会限制程序运行时间在 1~2 秒以内,所以枚举的总次数最好控制在 1 亿次左右或以下,超过太多就可能超时。
n 从 1 万变成 10 万(只大了 10 倍),检查次数却从 1 亿变成了 100 亿(大了 100 倍)——这就是嵌套循环"相乘"的威力,增长速度比想象中快得多。遇到范围较大的题目,枚举法往往不够用,需要换更聪明的思路(前面模块一学的优化技巧,以及后面要学的二分、动态规划等,都是为了避开这种"硬枚举"的代价)。| 类型 | 特点 | 典型例子 |
|---|---|---|
| 单层枚举 | 一个变量被另一个变量的关系唯一确定 | 鸡兔同笼、1.3~1.5 节的基础写法 |
| 多层枚举 | 多个变量互相独立,需要嵌套循环试遍组合 | 找勾股数组、找满足条件的数字组合 |