你还在用“聪明办法”解鸡兔同笼?有时候“笨”才是真智慧
你有没有过这种时刻——明明一道题想破了脑袋要找最优解,结果旁边一个人慢悠悠地说:“直接全部试一遍不就行了?”
然后你愣住了,对啊,为什么不呢?
在编程的世界里,这种“全部试一遍”的策略有个正式的名字:枚举。很多初学者对它嗤之以鼻,觉得这是“笨办法”,不够优雅。但我想告诉你一个反直觉的事实:在C++三级考试的战场上,枚举往往是你最可靠的朋友,没有之一。
枚举不是“傻”,而是“全”
先看一个生活场景。你面前有一个三位数的密码锁,密码忘了。聪明人可能会分析制造商的习惯、按键磨损程度……但最朴素也最有效的办法是什么?从000试到999。最多1000次,你一定能打开。这就是枚举。
它看起来“笨”,但它的核心优势是确定性——只要枚举范围没有遗漏,答案就必然在其中。这是一种“以空间换安心”的策略,在竞赛中,能拿到的分数才是硬道理。
三个问题,想清楚就成功了一半
别急着写代码。写枚举代码之前,先逼自己回答三个问题:
- 枚举什么?(对象是数字、下标,还是组合?)
- 范围多大?(从哪到哪?边界含不含?)
- 满足什么条件?(用哪个
if来筛选?)
就拿经典的“水仙花数”来说:一个三位数,各位数字的立方和等于它本身。
- 枚举对象:所有的三位数,即
num - 范围:100 到 999
- 条件:百位立方 + 十位立方 + 个位立方 ==
num
你看,思路一旦清晰,代码就是水到渠成的事。核心就这几行:
for (int num = 100; num <= 999; num++) {
int a = num / 100; // 百位
int b = (num / 10) % 10; // 十位
int c = num % 10; // 个位
if (a*a*a + b*b*b + c*c*c == num) {
cout << num << " ";
}
}
一道题告诉你:枚举的“范围”决定你的成败
我见过太多学生,题目分析得头头是道,一写代码就翻车。翻车点往往不是逻辑,而是边界。
看这道题:给定三角形两条边的长度 a 和 b(正整数),第三条边也是正整数,问能组成多少种不同的三角形?
考察点:这题表面考三角形,实际上考的是枚举范围的定义。你首先得知道三角形三边关系的定理:两边之和大于第三边,两边之差小于第三边。
那么第三条边 c 的范围是什么?不是 1 到无穷大,而是 abs(a - b) < c < a + b。
为什么? 因为 c 必须大于两边之差(否则短边加第三条边够不到长边),同时必须小于两边之和(否则两条短边合起来也够不到第三条边)。
所以,枚举范围就是 c 从 abs(a - b) + 1 到 a + b - 1。这里如果范围想错了,哪怕你判断条件写得再对,答案也是错的。这就是枚举的残酷之处——范围错,全盘输。
嵌套枚举:当一层循环不够用
有时候,一层循环解决不了问题,比如“鸡兔同笼”。你有头数 head 和脚数 foot,问鸡和兔各几只?
如果只枚举鸡的数量 chicken,那么兔子数量 rabbit 就不是被枚举的,而是被计算出来的:rabbit = head - chicken。然后你只需要判断脚数是否匹配。
for (int chicken = 0; chicken <= head; chicken++) {
int rabbit = head - chicken;
if (chicken * 2 + rabbit * 4 == foot) {
cout << "鸡:" << chicken << ",兔:" << rabbit;
break; // 找到一组解,跳出
}
}
这里有个小技巧:如果你能通过数学关系减少一个枚举变量,就不要傻傻地用两层循环。 这不仅是效率问题,更是思维深度的体现。
但有些问题无法避免嵌套,比如“奇怪的车牌号”那道题:车牌是8位数,前4位是递增自然数,后4位也是递增自然数,且所有数字之和是某个整数的平方。
分析:前4位递增,最多只能是 0123 到 6789。后4位同理。如果你枚举整个8位数,从00000000到99999999,那就是一亿次循环,虽然理论上能跑完,但效率极低。聪明的做法是分别枚举前4位和后4位,再组合判断。这就把一亿次降到了几千次。
这道题的精髓在于:枚举的对象不是“整个车牌”,而是“前四位”和“后四位”这两个独立的部分。 这告诉我们,枚举对象选得好,效率翻倍;选得差,程序跑到天荒地老。
一个容易忽视的坑:完全数的“因子范围”
来看“完全数”——一个数的所有真因子(除了它本身)之和等于它自己。比如 6 = 1 + 2 + 3。
很多新手会这样写内层循环:
for (int factor = 1; factor < num; factor++) {
if (num % factor == 0) sum += factor;
}
这没错,但效率低。你只需要枚举到 num / 2 就够了——因为一个数除了它自己,最大的因子不可能超过它的一半。
for (int factor = 1; factor <= num / 2; factor++) {
if (num % factor == 0) sum_factor += factor;
}
这就是枚举的优化思想:在不漏掉答案的前提下,尽可能缩小范围。 考试中不会让你枚举到一亿,但如果你能主动缩小范围,这本身就是一种能力体现。
最后,聊聊枚举的本质
很多人觉得枚举是“没办法的办法”。但我想说,枚举是算法思维的基石。你想想,动态规划的本质是什么?是枚举所有状态并记录最优值。搜索的本质是什么?是枚举所有路径并剪枝。你连“把所有情况列出来”都做不到,又怎么谈优化?
所以,别急着追求“巧办法”。先把枚举练到炉火纯青——边界不丢、条件不漏、范围精准。当你发现枚举太慢的时候,你自然就有动力去学那些更“快”的算法了。但在此之前,枚举是你最坚实的后盾。
考试时,遇到不会的题,先问自己一句:我能枚举什么? 很多时候,答案就藏在问题里。
关于作者
我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。
这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。
如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)