每次市级小学生编程比赛结束我都会习惯性地把当天题目在脑子里过一遍挑几道典型的留给下一届学生当素材。今年这道B4471 “数字糖果”就是我一直想坐下来细聊的题。题目不难给出一袋糖果每颗糖果上印着一个数字数字的“甜度”是它各个数位上数字相加的和最后要求找出甜度最高的那颗糖。听起来像数学小游戏可它把小学 C 入门里几个最要紧的考点全串了起来取模、整除、循环、比较、最值维护。对刚学完 if 和循环的孩子来说这题是检验基本功最合适的试金石对带竞赛班的人来说它又是能轻松变出五六个变式的母题怎么讲都不浪费。1. 题目拆解这颗“数字糖果”里到底包着什么1.1 题面还原与核心考点先把我理解的题面完整还原一遍。题目大意是这样的小明有一袋糖果每颗糖果上印着一个正整数。小明规定一颗糖果的“甜度”等于它每一位数字相加的和比如 123 的甜度就是 1236999 的甜度是 99927。现在先输入一个整数 n表示袋子里有 n 颗糖果接下来一行依次给出 n 颗糖果上的数字。请输出甜度最大的那一颗糖果上印着的数字如果多颗糖果甜度一样大就输出数字更大的那一颗。第一眼看上去这就是一道普通的循环题很多孩子扫一眼就能上手。但每年这道题在赛场上的得分率都不是最高的丢分的点几乎不在思路而在下面这些细节数字拆分能力能不能用%10取末位、/10去掉末位交替操作写成一个循环。循环停止条件循环里判断的是原数字是否变成 0而不是按次数循环。最值维护的框架是否熟悉“擂台法”先假设一个答案再逐个挑战。并列条件的处理甜度相同时还要比较数字本身大小条件要写成包含两个判断的复合表达式。这几个考点单独拎出来哪个都不难合在一起就容易乱。孩子最容易犯的毛病是写完了感觉自己“会了”一交上去才发现漏了某个分支。1.2 为什么小学生题目里这么爱考“数位和”很多家长问我数字糖果这种题有什么实际意义又不能拿去解奥数。我一般这样解释第一拆位是后面学二进制、位运算、字符串处理的前奏。小学阶段用十进制拆位来练“用一个循环逐次缩小一个量”的思维是最自然不过的。第二数位满足“个位、十位、百位”这种直观顺序孩子能把代码里的每一步和心里想的数学过程对上号不容易产生“算法黑箱”。第三数位和本身很小就算数字给到几十亿甚至更大数位和也不会爆类型出题人能放心地把输入范围放大一点避免大家纠结 int 和 long long。所以我常给刚入门的学生提一个要求遇到“数字”相关的题目先条件反射想到拆位三件套——%10取位、/10删位、循环到 0。这三件套用熟了数字糖果、回文数、数字反转、进制转换这些题就能一路平推。2. 从题目到代码一条完整的思考路径2.1 “甜度”是怎么算出来的拆位三件套要算一颗糖果的甜度得先把数字的每一位取出来。以 123 为例整个过程可以做成一张表轮次当前数字 xx % 10取到的位累加甜度x / 10 之后1123331221225131160看不懂这张表的人我先做一个生活化类比把数字 123 想成一摞糖果个位是最上面那颗每次取一颗看编号取完就扔掉这样一摞糖会越来越少直到拿空。写成代码就是int s 0; while (x 0) { s x % 10; // 取出当前最右边的数位 x / 10; // 删掉这个数位 }这里有一个非常关键的认识x 在循环过程里被不断改小最终会变成 0。所以如果你后面还要用这个数字本身就不能直接拿它进循环。这也是我在下一章要重点讲的丢分点之一。2.2 找最甜的糖擂台法维护最大值“在 n 个东西里找最大”是入门阶段的标配套路我们叫它擂台法。思路很简单先让第一个看到的数字当擂主后面的数字一个一个上来挑战挑战者更厉害就换擂主否则擂主不动。这道题里有两种“大小”要比较先比甜度甜度一样再比数字本身。所以擂台上要同时记住两个信息当前最大甜度以及对应糖果上的数字。初始化方式用哨兵法int bestNum -1; // 甜度最大的糖果数字 int bestSum -1; // 当前最大甜度因为题意保证糖果上的数字都是正整数用 -1 作为初始值不会影响最终结果。如果题目允许数字 0或者你想写得更稳妥也可以先读入第一颗糖用它初始化再处理剩下的数据。我倾向让低年级选手学会哨兵法它更简洁也更好背。比较的代码核心就一句话if (s bestSum || (s bestSum x bestNum)) { bestSum s; bestNum x; }这句话的优先级很容易出问题所以我在给孩子们讲的时候会要求他们必须写括号把并列条件括起来。别依赖记忆也别觉得“我懂优先级就行”。考场上紧张的时候一个括号能省下五分钟的纠结时间。2.3 完整参考代码下面是一份可以直接提交的完整参考实现。我把计算甜度封装成了函数后面讲变式时会说明为什么这样做更划算#include iostream using namespace std; int digitSum(int x) { int sum 0; while (x 0) { sum x % 10; x / 10; } return sum; } int main() { int n; cin n; int bestNum -1; int bestSum -1; for (int i 1; i n; i) { int x; cin x; int s digitSum(x); if (s bestSum || (s bestSum x bestNum)) { bestSum s; bestNum x; } } cout bestNum endl; return 0; }从我的带队经验看用标准头文件iostream比直接用一些编译器才支持的万能头文件更稳。部分在线评测环境对万能头支持不统一既然这题只需要输入输出老老实实写#include iostream最不容易节外生枝。这里再提一个高频改动需求如果题目改一改要求输出甜度最大的糖果是“第几颗”而不是它上面的数字那就把bestNum换成bestIdx在循环里用i记录当前下标。注意比较的时候仍然要比甜度和数字大小只是最后输出的是下标。每年都有人把这两个概念混在一起明明求编号输出的却是数字或者求数字却输出了编号。3. 丢分排行榜考场上真实踩过的坑3.1 一个 while 循环把原数字“吃”掉了这是这套题里最经典的翻车现场。有位选手交上来的代码长这样int s 0; while (x 0) { s x % 10; x / 10; } if (s bestSum) { bestSum s; bestNum x; // 问题出在这里 }看出来了吗等循环跑完x 已经被改成 0 了再去用 x 更新bestNum等于把一颗好好的糖果悄悄换成了 0。这种错误在几十个测试点里可能只挂两三个数不仔细看根本发现不了但一挂就是零分。整个二年级我带过的班里至少三四位选手在这上面栽过。解决办法也很简单拆位之前先备份一份或者干脆像参考代码那样把拆位写成一个带返回值的函数。函数参数是原数字的副本你在函数体内怎么改都不影响外面的变量。这一条我建议所有刚学循环的孩子直接刻进脑子里。3.2 甜度一样时忘了比数字本身还有一类错误不是语法问题而是条件漏了一半if (s bestSum) { bestSum s; bestNum x; }这串代码看起来天衣无缝但遇到 19 和 28 这种甜度都是 10 的并列情况时题目要求输出更大的那个 28而这串代码只会固执地保留先遇到的 19。小学生的题目就是这样刻意在同甜度的数据上做文章。你要是嫌麻烦不写后半句并列条件输出就一定会偏。我给学生总结过一道口诀“先比权重指标再比并列时的数字或编号。”这句话放到后面学结构体排序时照样好用权重指标就是排序关键字并列时的数字就是次级关键字。3.3 把“糖果上的数字”看成“糖果的编号”这是读题问题但每年都有人踩。题目说的是输出糖果上印着的那个数字可有些孩子做着做着就以为自己要找的是第 n 颗糖果最后输出的全是下标。判断自己有没有看错题有个很土但有效的办法把题目最后一句圈出来再问自己一遍到底让输出“数字”还是“第几颗”。如果题面里反复提到“编号”两个字才需要考虑输出下标。3.4 数据范围int 会爆吗很多刚入门的孩子看到数字就习惯性开 int。这道题如果输入范围给到 10 的 9 次方左右int 完全扛得住但课后变式里一旦把数据拉长到 10 的 18 次方就必须用 long long。有意思的是数位和本身再怎么算也很小哪怕数字有 19 位数位和最大也只有 9×19171真正需要操心的是读入变量的类型而不是甜度变量的类型。我的建议很直白养成看数据范围的习惯。看到 10 的 9 次方级别直接 int看到 10 的 18 次方级别条件反射写 long long。在类型上抠那点内存毫无必要。3.5 常见错误速查表错误现象典型原因正确做法输出全是 0拆位循环把 x 改成 0 后仍在用 x备份原数或把拆位封装成函数并列情况答案不对只比较甜度没比较数字大小条件写成s bestSum || (s bestSum x bestNum)输出结果是编号而不是数字读题不清概念混淆圈出题目要求确认输出对象最后一颗糖没被处理循环边界写成i n而非i n核对循环起点和终点答案时对时错初始值设置不当或没考虑并列用哨兵初始化或先读第一个数再比较4. 从“做对”到“满分”几个能长期受益的习惯4.1 把“算甜度”单独抽成一个函数虽然这道题不抽函数也能写但我要求我的学生从这一题开始就养成抽函数的习惯。原因有四条第一函数形参是 x 的副本在函数内部随便/10外面的变量都不会被污染第二主函数会变得非常清爽读代码就像在读题面哪个环节出问题一眼能定位第三这个函数写好之后后面所有的变式题都能直接复用第四如果答案不对可以单独用小数据测这个函数而不必连带着主流程一起排查。我甚至会跟学生说把拆位函数练到像背乘法口诀一样熟。因为它太常用了几乎每一场入门级比赛都会见到它的身影。4.2 边读入边处理还是存数组再处理这题只需要维护一个最大值读到一颗处理一颗就够完全不需要开数组。但不少孩子被老师讲过“输入的数据要存起来”于是不管三七二十一先把 n 个数全部装进数组再跑第二遍循环。不是不行但白白浪费了内存和时间还引入了数组开多大、下标从几开始的额外问题。判断标准其实就一条后面的计算是不是只依赖当前这一颗糖如果是就边读边处理如果后面要把所有糖按甜度排序或者要二次遍历那才需要考虑用数组存下来。这个判断习惯比这道题本身更有价值。4.3 用“手工样例”和“极限数据”验证代码我一般会让选手写完代码后先在草稿纸上手动推四类数据再上机器编译最小规模n1输入 7输出应该是 7用来验证程序能正常启动流程。并列甜度连续输入 5、14、23、32、41五颗糖的甜度都是 5最终应该输出最大的 41专门验证并列条件。较大的甜度输入 123456789甜度是 45数一数就知道自己拆位有没有拆错。边界形态个位数 7、整十数 10、整百数 100它们的甜度分别是 7、1、1这类数据最容易暴露循环退出条件写错的问题。这四类数据基本能覆盖这道题里所有容易出错的细节。很多孩子喜欢一写完就提交然后盯着评测界面发呆其实在草稿纸上多花两分钟比反复交十次都管用。4.4 调试时大胆用 cout 打印中间量如果答案还是不对别急着瞎改。一个笨办法反而最有效在循环里加一句cout x s endl;看着屏幕上的输出对比你手算的拆位过程很快就能发现是哪一个环节偏离了预期。打印语句对小学生的直观程度远超那些花哨的调试器界面。确认没问题后记得把打印语句删掉再提交。这里我想多说一句竞赛不是比谁先按提交按钮而是比谁先真正想清楚。先理解再写写的时候每一步都知道自己在干什么这比刷一百道题更重要。5. 变式训练一颗糖果能玩出多少种考法5.1 变式一统计甜度大于 10 的糖果有几颗把“找最大”改成“数个数”考点就变成了计数器的使用int cnt 0; for (int i 1; i n; i) { int x; cin x; if (digitSum(x) 10) cnt; } cout cnt endl;这是最自然的延伸孩子只需要把擂台法的比较条件换成计数条件几乎不用额外学习新知识。5.2 变式二把甜度是偶数的糖果数字累加起来这个变式在“拆位”之外多了一个条件判断和一个累加器long long total 0; for (int i 1; i n; i) { int x; cin x; if (digitSum(x) % 2 0) total x; } cout total endl;这里我会刻意让学生观察累加的对象是糖果数字本身而不是它的甜度。所以 total 可能非常大类型要按数据范围决定不能想当然用 int。这种“把条件加在正确对象上”的意识是很多孩子后续学一些复杂结构时会遇到的坎。5.3 变式三判断糖果数字是不是回文数拆位三件套再往前走一步就是数字反转。判断回文数的时候把原数反转回去和原数比较即可int rev 0, y x; while (y 0) { rev rev * 10 y % 10; y / 10; } if (rev x) { // 这是一颗回文糖果 }这个反转模板非常重要以后学进制转换、大整数处理都会用到。它和数字糖果的核心思路完全一致区别只是把“累加”改成了“拼数”。5.4 变式四按甜度排序所有糖果再进一步就是结构体排序。把数字和甜度打包成一个结构体再用自定义比较函数排序。小学组一般不要求到这个深度但对一部分学得快的孩子来说这是非常自然的进阶路径。数据结构从来不是凭空冒出来的都是在一个个基础模板上慢慢长出来的。四个变式对照下来变化规律非常清楚变式变化点新增考点统计甜度大于 10 的糖比较改计数计数器累加偶数甜度的糖加条件与累加奇偶判断、类型范围判断回文糖果累加改反转数字反转模板按甜度排序需要保存全部数据结构体、排序函数6. 关于这道题我最想说的一句很多家长和孩子都爱追难题觉得会了难题才显水平。但我长期带训练之后发现恰恰相反像数字糖果这种基础题才是真正的分水岭因为它的正确率直接反映孩子对循环和变量的理解是不是扎实。从经验看能把拆位三件套、擂台法、并列条件这三样东西闭着眼写对的孩子后面学数组、学排序、学结构体都特别顺畅一直在这些细节上栽跟头的难题刷得再多也容易在关键比赛上翻车。如果你现在正在备赛或者刚带孩子入门不妨回到这道题上亲手把代码默写一遍再用上面说的四类测试数据跑一遍看看能不能一口气全对。我个人觉得这才是这道题最有价值的地方——它给了我们一个机会把编程路上最基础的那几块砖认认真真铺稳。