GESP一级真题解析:小明的幸运数,从if嵌套到循环拆位
2023年9月那次GESP一级考完我带的几个学生走出考场第一句话不是“考得怎么样”而是“老师小明的幸运数那题我全用if嵌套写的写了快一百行”。我听完哭笑不得但也觉得这题出得确实典型——它把一个本该用循环和函数解决的问题包装成了一个让新手忍不住硬怼if的题目。B3864这道题在洛谷题库里挂着难度放在GESP一级里不算高但它几乎就是一级编程题的标准样本数字处理、条件判断、循环筛选三个核心考点全占了。这几天网上从gesp四级冒泡排序交换次数到gesp八级都在被讨论反而一级的题目没什么人细聊。所以这篇文章我就拿这道题做引子把我带学生刷一级真题时的完整思路、参考代码和翻车教训一次写清楚。1. 这道真题在GESP一级里是什么分量1.1 GESP一级编程题到底在考什么GESPCCF编程能力等级认证分一到八级一级是面向刚接触编程没多久的入门级考生。考试形式一般是客观题加编程题客观题考概念、语法和简单程序阅读真正拉分的都在编程题上。一级的编程题出题范围非常收敛顺序结构、分支结构、循环结构、简单数组、字符串基础输入输出基本不超出这些。换句话说凡是能用“读入几个数、做一次判断、循环处理一遍、按格式输出”解决的题目就是一级的绝对主力题型。《B3864 小明的幸运数》恰好就落在这个范围正中间。它没有复杂的算法不需要高深的数学知识更不考什么STL容器但如果你对循环、分支、整除取余这几个基础概念不熟那它又能把你卡得死死的。我常跟学生说一句话GESP一级的编程题不是在考你会不会“炫技”而是在考你脑子里有没有一套稳定的“解题肌肉记忆”——拿到题先分析输入输出再把规则翻译成代码最后用边界数据验证。这道题就是训练这套流程的完美载体。1.2 为什么我把它划成“必会题”而非“难题”我给学生的刷题清单里题目是分档的热身题、必会题、挑战题。《小明的幸运数》属于必会题里靠前的位置理由有三个。第一考点密度高。一道题同时覆盖了取余判断x % 7 0、数位分解循环while (x 0) { x % 10; x / 10; }、多数据筛选输出for循环加条件输出、无解处理特判输出占位符。这四个点一级考试里不管怎么出题都绕不开等于做一道题复习了半张考卷。第二它特别适合暴露新手思维的漏洞。我在1.1里说的“硬怼if”就是最典型的例子——新手看到“幸运数”三个字第一反应是把每一种情况都写成一个if根本想不到用循环去拆解数位。能意识到“一个数的每一位都要检查一遍”这件事本身就是一级水平的分水岭。第三它可以无缝升级。把“判断n个整数”改成“输出一个区间内所有幸运数”难度立刻变成二级初段的水平再加一个“数位和”条件、或者按幸运程度排序就能摸到二级中后段的边。一道题能横向扩展到整个数字处理题型这对复习效率来说太值了。2. 解题前的两个关键决策读题顺序与思路选型2.1 拿到题先做两件事手推样例、圈出输出格式很多学生拿到编程题第一件事是打开编辑器开始敲代码。这个习惯在GESP一级尤其吃亏。一级题目描述通常很短但里面有大量决定分数的细节最典型的坑就是输出格式。拿这道题来说我在教学时参照洛谷和其他OJ上的常见版本核心逻辑差不多是这样给定若干个整数把其中“幸运数”按输入顺序输出如果没有任何幸运数则要输出一个指定的占位符。这个“没有幸运数时输出什么”的要求如果审题不仔细丢分直接且无解。所以我要求学生拿到题先做两步第一拿一支笔把题目的样例输入和样例输出手动走一遍用自己的话复述一遍“幸运数到底是什么规则”第二把题目里关于输出的每个字都圈出来包括换行、空格、占位符、大小写。这么做不是为了形式主义而是因为比赛评测只看输出。你代码写得再漂亮多打一个空格、少输出一行、该大写写成小写结果就是零分。手动走一遍样例脑子里对这个程序的行为就有了一个具体预期后面写代码的时候心里是有底的。2.2 把“幸运数”翻译成C能判断的表达式题目里“幸运数”是自然语言C不认识自然语言必须翻译成布尔表达式。我教学时习惯用一个等价版本一个数是幸运数当且仅当它能被7整除或者它的十进制表示的某一位上含有数字7。这里的“或”在C里就是 ||。第一个条件“能被7整除”很简单x % 7 0。注意是取余%不是除法/。新手最容易在这里把%写成/一换就错得离谱。第二个条件“某一位上含有数字7”要稍微动点脑筋。它的本质是“把这个数的每一位拆出来逐个检查”拆位用的是反复取余和整除x % 10 拿到个位x / 10 去掉个位。整个过程用一个while循环包住直到x变成0为止。举个例子x 73第一次循环x % 10 3不是7x / 10 变成7第二次循环x % 10 7是7直接返回true。这个“边拆边查”的过程不需要数组也不需要字符串是一级范围内必须掌握的套路。2.3 为什么我建议一级就把判断逻辑拆成函数这里要说一个看起来“超纲”但其实很实在的建议把“判断一个数是不是幸运数”这件事单独写成函数哪怕你还没系统学过函数。原因很简单你的main函数里只需要负责读数和输出判断细节全部封装进一个bool isLucky(int x)里面。这样写有肉眼可见的好处——思维清晰逻辑隔离调试的时候一眼就知道问题出在输入输出还是判断条件。而且如果原题版本的幸运数规则和你手头题不一样比如改成“数位和为7”或“能被3整除且不含4”你只需要改这一个函数体框架完全不用动。这不是什么高深的软件工程思想就是一个很朴素的经验程序越长越要把“做什么”和“怎么判断”分开。一级的题虽然短但从一开始养成这个习惯后面刷二级三级题会顺很多。3. 参考实现与评测机视角的代码打磨3.1 一份能直接交的C参考代码下面这份代码是我建议学生在考场上写的版本逻辑清晰通过样例和边界数据都没有问题#include iostream using namespace std; bool isLucky(int x) { if (x % 7 0) { return true; } while (x 0) { if (x % 10 7) { return true; } x / 10; } return false; } int main() { int n; cin n; bool found false; for (int i 0; i n; i) { int x; cin x; if (isLucky(x)) { cout x endl; found true; } } if (!found) { cout -1 endl; } return 0; }假设输入5 7 17 21 34 70程序会依次判断7能被7整除输出17含数字7输出21能被7整除输出34两个条件都不满足跳过70既能被7整除又含7输出。最终输出是7 17 21 70整个过程就是“循环读入、逐个判断、遇到就输出、最后看有没有输出过”。3.2 三个容易在评测机上挂掉的细节第一个细节无解标志。代码里我用了一个bool found初始是false只要输出过一个幸运数就改成true。等循环全部走完再检查found如果还是false就输出占位符-1。这个标志位的逻辑一定要放在循环外面同时必须用“是否输出过”而不是“是否遇到过”来定义否则很容易写出重复输出、或者无解时什么都不输出的代码。第二个细节endl和\n的区别。输出时我写的是cout x endl其实改成cout x \n更好。endl在每次输出后都会强制刷新输出缓冲区数据量小的时候无伤大雅但多组输出时效率差一点。一级考试数据量通常很小用哪个都行但养成用\n的习惯后面写文件读写、大批量输出时会少踩不少性能坑。第三个细节数位循环的边界。while (x 0) 这个条件意味着 x 0 时循环体一次都不执行。对这道题的规则来说问题不大因为0 % 7 0直接就在前面返回了。但如果你想写一个“判断一个数是否包含某数字”的通用函数记得单独处理0。如果输入可能出现负数比如-17那么x % 10的结果在C里是-7判断等于7就会漏掉。稳妥的做法是进函数先取绝对值int y abs(x);后面所有操作都用y。一级数据基本不会出负数但把函数写通用点没什么坏处。3.3 从“能过样例”到“敢提交”造边界数据过样例只是最低要求离“敢提交”还差一步这一步叫造数据。我要求学生在提交前必须自己造三组数据。第一组是普通数据验证基本逻辑对不对比如上面那组5个数。第二组是边界数据包括只有1个数且它是幸运数、只有1个数且它不是幸运数、全部都是幸运数、一个幸运数都没有。对这道题来说尤其要测“无幸运数”的情况因为无解输出是独立分支最容易漏。第三组是极端数据比如n取到上限时程序会不会超时、会不会因为数组开小了而崩。这道题根本不需要数组一个变量边走边判断就够了不存在越界问题但你要有这个意识。造完数据后对照预期结果手算一遍再跑程序比对。这个过程看着笨却是性价比最高的提分手段。我见过太多学生样例过了就沾沾自喜结果评测出来零分一问就愣住“我在自己电脑上明明能跑啊”。问题往往就出在没测边界。4. 我带考时见过的翻车现场与调试方法4.1 四个真实翻车案例翻车案例一把%写成/。有学生判断“能被7整除”写成了x / 7 0。这个表达式的含义是“x除以7商为0”只有x在0到6之间才成立完全背离原意。我提醒过很多次整除的数学表达是“除得尽”翻译成C就是取余结果为0。写完之后自己用7、14、15各测一遍这种低级错误立刻就能暴露。翻车案例二数位分解时改了循环变量。有学生写while (x 0) { if (x % 10 7) return true; x / 10; }单看没问题但他是放在for循环里直接写的外层的循环变量也叫x内层一改外层循环直接报废。这种“变量名撞车”问题在一级里非常常见。解决办法就一个拆数的时候单独拿一个临时变量y x对y做拆位别动原始数据或者干脆按我前面的建议写函数函数参数是值传递天然隔离。翻车案例三没有处理无解情况。学生判断完所有数发现没有幸运数程序静悄悄地结束屏幕上什么都没有。题目要求输出占位符这行代码漏了就是0分。无解处理有时候藏得很深很多经典题比如“找最大”“判断素数”都爱带一个“如果没有则输出XX”的条件审题时一定要专门盯这一句。翻车案例四输出格式多打了空格。题目要求每个数占一行学生写成cout x 数之间用空格隔开。在自己电脑上看着挺整齐但评测机不认——它按行逐字比对多一个字符都算错。记住评测机的对比是逐字节的不是“人看着差不多”就行。4.2 本地自测的两种土办法手造数据和简单对拍自测最简单的办法是手造数据。这个我在3.3里已经说了关键是造完以后要手算出正确答案再和程序输出比对别偷懒用程序输出当正确答案。如果以后刷题量大了想验证自己的程序和一份“暴力但正确”的写法是否一致可以玩对拍。思路很简单写一个生成随机输入的小程序把同一份输入分别喂给你的程序和暴力程序然后比对输出。只要随机测试反复跑几百组输出都一样你的程序大概率是对的。一级阶段不一定用得上对拍但知道这个概念很有用因为到了二级三级很多题都需要对拍来找反例。4.3 提交前的五分钟自查清单我最后给学生整理了五条自查项目每次提交前按顺序过一遍基本能拦住80%的愚蠢错误编译能过吗变量名有没有拼错头文件够不够是不是忘了using namespace std。样例能过吗这是最底线的一条。边界数据造过吗至少测一组“没有幸运数”的输入。输出格式逐字符核对过吗占位符是不是题目要求的那个有没有多余的换行和空格。代码里有没有改掉循环变量、全局变量这些“地雷”这套清单不花时间做熟练了两三分钟就能过完但对稳定拿分的帮助极大。5. 从这一题铺开一级复习地图5.1 一级考点树从“幸运数”长出来的分支做题不能只做一道做完一道题要往四周看一看看它能长出多少分支。《小明的幸运数》不是孤立的它背后是一整棵一级考点树。“数位分解”这个技能延伸出去就是数位和、倒序数、回文数判断、统计某数字出现次数。“整除判断”延伸出去就是因子枚举、质数判断、最大公约数。再加上“区间筛选”“数组标记”这些操作一级后半程到二级前半程的题目基本都在这棵树上。我在带学生复习时习惯画一张简单的对照表把这道题涉及的每一个点都对应到可能出现的变形本题考点常见变形题x % 7 0 整除判断判断闰年、判断质数、求公约数while (x 0) 拆位数位和、倒序数、统计数字出现次数循环读入筛选输出输出区间内所有偶数/素数/回文数无解占位符输出找最大值时处理空序列、查找失败输出这个表不需要背但心里得有一级考来考去就这些组合。吃透一道《小明的幸运数》比盲目刷二十道互相重复的题有效得多。5.2 不同起点的复习节奏和刷题建议如果你是完全零基础刚开始学C我的建议是语法学完顺序、分支、循环之后立刻拿这道题当第一个综合练习不要等学完数组再动。它不需要数组一个变量边走边判断就能写出来非常合适。如果你之前学过Python现在转C冲GESP一级重点别放在“会不会写”上而是放在C的语法细节上int和long long的范围、除法取整方向、负数的取余行为、cin/cout和scanf/printf的区别。Python转过来的学生最容易在负数和溢出这两个地方吃亏。如果你是冲二级也可以回头做这道题但要求要升级把“判断n个整数”改成“输出L到R之间所有幸运数”再把条件改成“数位和为7且能被7整除”或者把输出改成“先按幸运程度排序再依次输出”。这几个变形做完你等于用一道题复习了二级前半段最常考的区间枚举、多条件组合和简单排序。我个人的习惯是刷题一定要留档。每做完一道有价值的题把代码、当时的错误、这道题的变形思路记在一个文档里。GESP各等级之间考点是螺旋上升的你现在记下的“幸运数”笔记到考二级三级的时候翻出来看依然有用。最后再分享一个小技巧考试时把这类“规则判断”的题哪怕最后全写在main函数里也先想清楚“判断条件能不能单独拿出来写”。把这个习惯保持住你的代码永远比别人好调一半。这道题能不能满分其实不取决于你会不会写if而取决于你有没有把“拆问题”这件事刻进肌肉里。