ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GESP二级拆数题全攻略:核心考点、解题模板与真题变形详解

GESP二级拆数题全攻略:核心考点、解题模板与真题变形详解

带过几轮备考GESP二级的学生之后,我越来越觉得“拆数”两个字值得单独拿出来写一篇。原因很简单:二级的考题里,凡是跟整数打交道的题,十道里有六道绕不开拆数这个操作。而且它两级分化严重——基础好的学生觉得它是送分题,基础不牢的学生一写就错,全场最小的bug往往就藏在一次取模运算里。今天这篇文章我把核心考点、解题模板、模拟题和真题变形一次性讲透,不管你是刚开始备考还是已经刷过一轮,看到最后都能带走一套可以直接用的拆数思路。

GESP二级的算法题并不追求复杂,它考的是“你能不能把数学问题翻译成循环和分支”。拆数题恰好完美承担了这个任务:描述简单、逻辑清晰、但非常考验基本功。很多学生平时能看懂代码,一到考场上自己动手写就卡住,本质上是没有一个稳定的拆数操作流程。我接下来就把这套流程掰开揉碎讲清楚。

1. 拆数题为什么能在二级考卷上反复出现

先说一个很多家长和考生都没意识到的点:GESP二级的题目设计是有明确“考点矩阵”的。一级考顺序结构和简单输入输出,二级突然过渡到分支结构和循环结构,出题人需要在有限代码量里同时验证学生这几个能力。拆数题就成了性价比极高的选择——一句话能讲清楚题面,但背后需要你同时搞定十进制数位原理、整除和取模运算、循环控制、条件判断、输出格式,一个题顶三四个知识点。

从近几年考生反馈的题型分布看,拆数相关题目基本稳定出现在第二题到第四题的位置,分值大约在15到25分之间。它不像最后的综合大题那样需要复杂的数学建模和剪枝优化,属于“占了时间但应该拿下”的保分题。但恰恰因为大家都觉得简单,真正能在考场上一次写对的人反而不多。我统计过自己接触的学生错因,取模整除方向写反、循环边界多退一位、重组数字时忘记还原数位,这三类错误占了八成以上。

另外可以留意一下二级考纲里反复出现的几个方向:数位分离、数位统计、数字重组、回文数判断、水仙花数。这五个方向本质上是同一套核心操作的不同应用——把整数拆成一位一位,然后对每一位做点事情,最后要么输出结果要么拼回一个新数。

从这个角度看,拆数题复习的投入产出比非常划算。你不需要背大量算法模板,只用把“拆位”这一个动作理解透,再配合少量的组合变形,就能覆盖二级里好几类必考题目。这也是我建议备考二级的学生优先花两个小时把拆数模板练成肌肉记忆的原因。

1.1 官方定位:从“照着写”到“自己想”

二级和一级最大的区别在于,一级很多题是给定了明确步骤让你翻译成代码,二级开始需要你自己设计处理逻辑。拆数题恰好站在这个分界线上:题目不会再告诉你“先求个位、再求十位、再求百位”,而是用“请你判断这个数是否满足某种性质”这种描述把拆数方案隐藏起来。

比如“判断一个三位数是否为水仙花数”,你就得自己想到:先把百位、十位、个位拆出来,再分别求立方和,最后和原数比较。这个“自己想到”的过程就是对编程思维最直接的考察。

1.2 哪些题最容易伪装成拆数题

这些年我发现,拆数题的“马甲”特别多。表面上看题目在讲“幸运数字”“超级整数”“反向加法”,剥开外壳,核心还是那套拆位循环。常见的迷惑性表述包括:各位数字之和、每位数字都满足某个条件、把数字倒过来写、重新组合成一个新数、统计包含某一特定数字的个数、判断某一位上是否为偶数。

备考的时候,你不需要被这些花哨的题目名称带偏。看到“每一位”“各位”“数位”“颠倒”“组合”这类关键词,就直接在草稿纸上写出拆位循环框架:while (n > 0) { int d = n % 10; ...; n /= 10; }。这个框架能解决绝大多数二级拆数题。

2. 拆数的三板斧:取模、整除、重组

在讲具体题目之前,有必要把最底层的三个操作彻底讲清楚。很多学生代码写错,不是逻辑不行,而是对“取模”和“整除”这对兄弟的本质理解不到位。我用最简单的两位数举例说明。

对于一个正整数 n,n % 10 得到的是它的个位数字,因为任何整数除以10的余数只可能是0到9,正好对应十进制表示中的最后一位。而 n / 10 得到的是去掉个位之后剩下的部分,因为整数除法会丢掉小数部分。这两个操作配合使用,就能像剥洋葱一样,从个位开始一层一层把数字的全部分解出来。

生活化地理解就是:你想知道一整篮子苹果能装满几个筐、剩几个单独放着。整除告诉你能装满几个筐,取模告诉你还剩下几个。十进制也一样,n除以10的整数商告诉你“把个位拎出去之后还剩多少”,余数就告诉你“拎出去的个位是多少”。

2.1 从个位开始的循环拆位模板

如果要拆一个任意位数的正整数,标准操作是循环里反复做两件事:取出当前最右边的数字,然后把那个数字从原数里去掉。写成C++代码就是最基本的模板:

while (x > 0) { int d = x % 10; // 拿到当前最低位的数字 // 对 d 做你想做的事:判断、求和、统计、存进数组…… x = x / 10; // 去掉最低位 }

这个循环的执行顺序必须刻在脑子里:第一次循环取出的是个位,第二次是十位,第三次是百位,依此类推。也就是说,拆位循环天然是从低位到高位处理的。如果题目要求你按原来的高位到低位顺序输出,那你就不能一边拆一边输出,得先把拆出来的数字装进一个数组里,等循环结束后再倒着访问数组。

我还想强调一个很多基础不牢的学生容易忽略的细节:千万不要把循环条件写错。常见错误是写成 while (x >= 0),这会导致死循环,因为 x 最终会变成0,而0 >= 0 永远成立。正确条件是 while (x > 0),因为最后一次进入循环时 x 还是一个个位数,取出这个个位后 x 归零,循环正好结束。这个归零点就是循环的“刹车”,一定要保证它能刹住。

2.2 重组:把拆出的数位拼回一个新数

拆数并不总是终点,很多题需要你“拆完再拼”。重组操作其实就是一个还原过程:你想拼一个两位数,就用十位乘以10再加个位;拼三位数就用百位乘以100加上十位乘以10再加个位。核心逻辑是每一位都要乘上它所在位置对应的权值。

// 假设已经从 n 中拆出 a(百位)、b(十位)、c(个位) int m = a * 100 + b * 10 + c; // 还原成原数

如果想把三位数的个位挪到最高位重新组成新数,比如123变成312,对应的重组代码就是 c * 100 + a * 10 + b。这类重组题在二级里经常以“数字倒序”“数字位移”的形式出现。写的时候我建议先在草稿纸上手写一个具体数字走一遍,确认每一位乘的权值正确,再敲代码,能省下大量调试时间。

2.3 用字符串拆数可以吗

备考过程中总有学生问我:能不能把整数当字符串读进来,然后直接访问每一位字符?技术上当然可以,字符串按索引访问确实能避免取模整除的逻辑错误。但我个人不推荐在二级考场上这么做,原因有两个。第一,GESP的判题系统会对输入输出格式做严格校验,字符串方式需要额外处理字符到数字的转换(读入的 '5' 和数字5差一个ASCII偏移),多一步就多一个出错点。第二,很多拆数题后续要对每一位做算术运算或比较大小,数字形式直接参与计算最方便,字符串形式还得反复转换,白白浪费时间。

除非题目明确要求“以字符串形式输入”或者处理的是超长整数(二级基本不会出现),否则请坚持用整数思维做拆数题。这也是我认为二级阶段使用C++而不是Python处理这类问题的优势之一——C++的整数运算更贴近数学直觉,类型边界也相对清晰。

3. 两道模拟题拆到底:从读题到跑通全流程

理论讲完,上实战。我从自己给学生练习的题库里挑了两道很有代表性的模拟题,一道考固定位数的拆解与重组,一道考任意位数的循环拆位与统计。两道题都不是纯模板题,第二天赋,第二题加了一点容易被忽略的边界细节,正好能检验你是否真的理解了拆数思维。

3.1 模拟题一:数字换位

题目描述:输入一个三位正整数 n(100 ≤ n ≤ 999),将 n 的百位、十位、个位重新排列成“个位、百位、十位”的顺序,得到一个新三位数 m。如果 m 大于 n,输出 GREATER;如果 m 小于 n,输出 LESSER;如果恰好相等,输出 EQUAL。

比如输入 123,百位是1、十位是2、个位是3,按照个位、百位、十位排列后得到 312,312 大于 123,所以输出 GREATER。

这个题的考点非常清晰:固定位数的拆解、三位数的重组、整数比较与分支嵌套。

先看完整的参考代码:

#include <iostream> using namespace std; int main() { int n; cin >> n; int a = n / 100; // 百位 int b = n / 10 % 10; // 十位 int c = n % 10; // 个位 int m = c * 100 + a * 10 + b; // 按“个位、百位、十位”重排 if (m > n) { cout << "GREATER" << endl; } else if (m < n) { cout << "LESSER" << endl; } else { cout << "EQUAL" << endl; } return 0; }

我带着学生做这道题的时候,重点强调十位的拆法:n / 10 % 10。很多学生第一次会写成 n % 100,那得到的是最后两位组成的两位数,比如123会拆出23,而不是数字2。这里有一个通用口诀:想取第 k 位(从个位开始数第0位),先把这个位右边的数字全扔干净,再对10取余。百位的拆法 n / 100 本质也一样,先把右边两位扔干净,剩下的就是百位。

然后是重组。题目要求的新顺序是“个位、百位、十位”,所以最高位放的就是原来的个位 c,中间放原来的百位 a,最低位放原来的十位 b,对应表达式就是 c * 100 + a * 10 + b。我建议你在草稿纸上把“123 → 312”的每一位对应关系写一遍再动手,别急着敲键盘。

这道题还有一个隐藏的坑点值得单独说明。不要以为“新三位数”一定还是个三位数。举个例子:输入 120,百位是1、十位是2、个位是0,重组后得到 0 * 100 + 1 * 10 + 2 = 12,这是一个两位数。如果题目没特别说明输出时要不要补前导零,你直接用整数输出12就好,不需要补成012。C++的整数输出会自动忽略前导零,这也符合大多数判题系统的预期。但你必须意识到,重组后的数可能位数变少,所以不要为了“保持三位数”强行给 c 补什么逻辑,直接运算就对了。

我实测跑了几组数据:n=123 输出 GREATER;n=120 输出 LESSER;n=111 重组后还是111,输出 EQUAL。这几个样例覆盖了大于、小于、等于三种分支,也覆盖了重组后位数改变的边界情况,考试时如果时间紧张,测验这三组基本就能确认代码没有方向性问题。

3.2 模拟题二:奇数数位统计

题目描述:输入一个正整数 n(0 ≤ n ≤ 1000000),统计 n 的各个十进制数位中,奇数的个数,并输出这些奇数数字的总和。输出一行两个整数,中间用空格隔开。例如输入 2357,各个数位是2、3、5、7,其中3、5、7是奇数,共3个,和为15,所以输出 3 15。

这道题难在两点:第一,n 的位数不固定,可能是1位也可能7位,必须用 while 循环而不是手写固定次数的取位代码;第二,n 可能等于0,这是一个需要特意考虑的边界。

参考答案:

#include <iostream> using namespace std; int main() { int n; cin >> n; int cnt = 0; int sum = 0; if (n == 0) { cout << 0 << " " << 0 << endl; return 0; } while (n > 0) { int d = n % 10; if (d % 2 == 1) { cnt++; sum += d; } n /= 10; } cout << cnt << " " << sum << endl; return 0; }

你可能注意到我在循环前特判了 n == 0。其实从结果上看,不写这个特判也能得到 0 0 的正确输出,因为循环不会进入,cnt 和 sum 保持初值0。但我依然建议保留特判,理由不算性能而是可读性和逻辑安全感:考场上的你多半是紧张的,一个显式的边界处理能让你自己在回读代码时更有信心,也方便检查。这一点我后面讲易错点时会继续展开。

循环体内做的事就两件:取出当前最低位 d,判断它是否为奇数。判断奇偶用的是 d % 2 == 1,偶数就是 d % 2 == 0。这一步很多学生会犯一个低级错误,把取整和取模搞混,写成 d / 2 == 1,那就是完全不同的逻辑了,这里一定要盯紧。

测试数据我建议这样准备:输入 2357 期待输出 3 15;输入 2468 期待输出 0 0;输入 5 期待输出 1 5;输入 1000000 期待输出 0 0。四组数据分别覆盖了多位数、全偶数、一位数和边界最大值,全部通过的话这段代码在考场上基本就稳了。

如果题目要求输出格式是一个空格分隔,千万注意不要多输出行尾空格。上面的代码用 cout << cnt << " " << sum << endl 刚好输出“数字空格数字换行”,和判题系统期望完全一致。我见过有学生为了对齐在 sum 后面也加了个空格,结果被判 Presentation Error,白丢分,这种冤枉分丢得最冤。

4. 真题中的四种拆数变体:换个马甲照样会做

模拟题练的是基本功,但真正上了考场,题目不会直白地写着“请你拆数”。这一节我梳理最近几次考试中出现的拆数类题目变形,题目描述经过转述但核心逻辑保持一致。你会发现,只要能把模板灵活迁移,这些变体都不难。

4.1 变体一:判断某个数字是否出现

这是最直接的变形。题目可能说“统计在1到n之间有多少个数字含有数字7”或者“输入一个数,判断它是否包含数字9”。核心做法还是拆位循环,只不过要在循环里多维护一个布尔变量提前退出。

bool hasSeven = false; while (x > 0) { if (x % 10 == 7) { hasSeven = true; break; } x /= 10; }

这个变体的考点是“是否”型逻辑,输出 YES/NO。很多学生容易在 break 之后忘记 x 已经没有实际意义了,但既然已经 break,后面不再使用 x,就没有问题。真正要注意的只是布尔变量的初始值要设为 false,并且发现符合条件的数位后立即中断循环,节省一点运行时间。

4.2 变体二:倒序输出每一位

题目描述通常是这样:输入一个正整数,按从高位到低位的顺序输出它的每一位数字,中间用空格分隔。比如输入 12345,输出 1 2 3 4 5。

这个题最迷惑人的地方在于,拆位循环天然是先输出个位5的,顺序是反的。解决办法有两种。第一种是先把每位数字存进数组,循环结束后从数组尾部倒着输出。第二种是先算出数字的总位数,再用一个可以控制权重的循环从高位往低位拆。二二级学生我更推荐数组方案,因为它不容易在位数计算上出错。

int digits[10]; int idx = 0; while (x > 0) { digits[idx++] = x % 10; x /= 10; } for (int i = idx - 1; i >= 0; i--) { cout << digits[i]; if (i > 0) cout << " "; }

数组在这里扮演的角色是“倒车缓冲区”——拆进去的时候是反的,取出来的时候再倒一遍就正了。注意输出空格的控制,最后一个数字后面不要跟空格,这也是常见的失分点。

4.3 变体三:找出最大的数位

这类题在真题里经常伪装成“幸运数字”“大数游戏”。比如输入一个正整数,输出它各位数字中的最大值;或者变形成“用最大数位组成一个新数”。核心就是拆位过程中维护一个打擂台变量。

int maxDigit = 0; while (x > 0) { int d = x % 10; if (d > maxDigit) maxDigit = d; x /= 10; }

maxDigit 初始化为0是安全的,因为十进制数字最小就是0。如果把 maxDigit 初始化为1,遇到输入0这种情况就会得到错误结果。虽然0作为数位出现在多位数的中间或末尾是常有的事,但如果题目输入的正整数保证大于0,那么初始化成0还是1都没关系。我建议一律初始化为0,养成习惯,省的某天遇到一个允许输入0的题翻车。

4.4 变体四:数字重组后做数学运算

这一变体综合能力最强,也是二级拆数题里偏难的一档。典型描述是:输入一个两位数,交换十位和个位得到一个新数,输出新数与原数的差(取绝对值);或者输入一个三位数,把个位移动到最高位形成新数,判断新数是否大于原数。

这类题就是前面模拟题一的进阶版,解题关键依然是先把每一位拆出来,然后按题目要求重组。我特别想提醒一点:遇到“差要取绝对值”这类要求时,别自己去分类讨论谁大谁小,直接用 abs(m - n) 一步到位。二级阶段用 abs 函数完全没问题,它来自 或 头文件,很多环境在 里也能直接调用。写代码能简单就简单,不要人为制造分支。

还有一种更进阶的变形,要求你判断一个数是否为“回文数”——正着读和倒着读一样。思路是拆出每一位后,把倒序数重新组装出来,然后和原数比较。组装倒序数的代码可以这样写:

int original = n; int reversed = 0; while (n > 0) { reversed = reversed * 10 + n % 10; n /= 10; } if (original == reversed) cout << "YES" << endl;

这段代码值得反复品。它不借助数组,直接利用“反复乘以10再加新的最低位”来重组数字。第一次循环把个位放到个位,第二次循环原有个位变成十位、新的十位变成个位,到最后整个数就反过来了。这种写法在二级压轴题里很常见,建议你亲手推演一遍121的整个过程,彻底搞懂这个公式,比背下来有用得多。

5. 二级考场里最容易翻车的三个细节

最后这部分我不打算罗列一堆泛泛的“考试技巧”,直接把我这些年看到的真实翻车现场拿出来讲。每一类错误我都见过不止一次,有的学生甚至平时写练习没问题,一上考场就踩同一个坑。

5.1 整除和取模方向写反

说来有点不好意思,这类错误最常见的地方恰恰是两个运算符混用。学生本意是想取个位,却写成了 n / 10 然后拿去用;本意是想去掉个位,却写成了 n % 10。结果逻辑全乱,程序输出一堆诡异的结果。

要克服这个问题,我建议你在考前自己总结一句只有你能记住的话。比如我的口诀是“除以是扔,取模是留”——除以10是把个位扔掉,取模10是把个位留下。每次写完拆位循环,立刻检查一遍循环体里那两行,确认 d 取的是余数,x 更新用的是商。这个检查只需要两秒钟,但能避免一整道题的分数归零。

另外注意,C++里负数参与 % 运算时结果符号是负的,比如 -13 % 10 等于 -3。这类题目通常保证输入为正整数,但你在写边界自测时如果输入了负数,就会看到一些莫名其妙的结果。不是代码的错,是你测试用例不合法,别在这上面浪费时间纠结。

5.2 循环边界:0的处理和多退一位

拆位 while 循环的边界问题是第二大重灾区。我前面说过循环条件要用 x > 0,但在有些学生那儿,他们喜欢先用 if 判断一下位数,再写一个 for 循环按指定次数拆位。问题就出在计算次数上,常见的错误是输入12345,位数应该是5,结果循环跑了4次或者6次。

如果你想用 for 循环固定次数,我建议还是先改成 while 循环,让条件 x > 0 来做终止判断,这样从根源上避免次数算错。如果题目标明输入一定是三位数,那 for 循环写三次没问题;一旦题目说“正整数”,就老老实实用 while。

至于 n == 0 的边界,我在模拟题二里已经演示过。如果题目允许输入0,你最好特判一下,防止 while 循环完全不进入导致某些依赖循环初始化的后续逻辑出错。哪怕有时候不特判结果也对,写上特判也能让阅卷人觉得你考虑周全,对二级这种人工阅卷极少但逻辑要求严的考试来说,代码清晰本身就是得分项。

5.3 输出格式:空格、换行、大小写一个都不能差

GESP 的判题系统对输出格式校验得相当严格。题目要你输出 YES 你输出 Yes 或者 yes,判错;要你输出中间空格你输出换行,判错;行尾多一个空格,可能判 Presentation Error;少一个换行,也可能判错。

拆数题的输出通常很简单,就是几个数字加空格,或者一个单词。我给你的实操建议是:写完代码先对着题目描述逐字核对一遍输出要求的英文单词和大小写,然后在测试样例里亲手敲一遍预期输出的样子,再运行代码对比。因为拆分题的逻辑你已经验证对了,如果最后挂在输出格式上,那真是整场考试最窝囊的丢分方式。

5.4 考场时间分配的私人建议

最后聊点关于考试节奏的真实体会。拆数题作为二级中难度中等的题目,一般建议单题用时控制在十到十五分钟。如果超过二十分钟还没想明白,果断先把简单题做完,回头再来死磕。很多学生考试时容易跟一道题较劲,结果后面两道大题时间不够,这是最不划算的时间管理。

还有一个小技巧,是所有实战经验里我觉得最实用的:拿到题先别盯着屏幕,拿草稿纸手算一遍样例。用手算一遍之后,你对“拆哪一位、重组以后是什么顺序、比较还是统计”都会有一个具象的感知,再写代码时会顺手得多。我见过太多学生一上来就敲 keyboard 敲到一半才发现题意理解错了,重新写反而花掉双倍时间。

写代码之前先想清楚数据流:输入进来之后经过哪几步变换,最后输出什么。这个思考只需要一分钟,但能让你避免在错误思路上狂奔。从这个角度说,拆数题真正考的不是你会不会写代码,而是你能不能像一个工程师一样,把“拆开—处理—重组—输出”这个流水线在头脑里完整地跑一遍再动手。

练到这种程度,二级考卷上不管拆数题穿什么马甲,你都能一眼认出来,然后从容地把它拆开。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进