看到“数字黑洞”这道题,不少备考GESP C++二级的同学第一反应是:这名字听着怎么像数学玄学?其实它就是一个单纯的按规则重复运算的模拟题,核心就一句话——输出一个四位数,按位拆开,重新排序得到最大数和最小数,相减得到新数,继续这个流程直到掉进6174这个黑洞,最后输出一共做了几次运算。很多第一次接触这道题的同学,不是不会写代码,而是不知道这个过程会怎么结束、中间那些前导零要不要处理、某个数算到一半变成三位数该怎么办。这篇就把整个题从原理到代码、从边界用例到考场细节完整拆一遍,适合正在备考GESP C++二级的考生、带信息学竞赛的指导老师,以及想拿经典模拟题练手的人。
1. 题目到底在考什么:6174黑洞背后的计算逻辑
1.1 从一道“看起来像玄学”的题说起
数字黑洞在数学圈有个正经名字,叫卡普雷卡常数,指的是四位数的数字经过固定规则重排相减后,最终会收敛到6174这个数。比如你随便写个四位数,按每位数字从大到小排出一个数,再从小到大排出另一个数,两个数相减,得到的结果继续重复这套操作,最后一定会得到6174,而且最多不会超过7次。
GESP C++二级把这道题放进来,不是为了考数学定理,而是用它来考察三个基本功:一是能不能把一个多位数拆成单个数字;二是能不能用排序或者枚举的方式把最大数和最小数构造出来;三是能不能写一个不会死循环的循环结构,并且在循环体里控制好状态的变化。这几样恰好是二级考试里数组、循环、排序、函数综合运用的典型场景。
1.2 数字黑洞过程的逐步拆解
假设输入的数字是3524,整个过程是这样跑的:
- 把3524的四个数字拆出来:3、5、2、4。
- 从大到小排列得到5324,从小到大排列得到2345。
- 做减法:5324 - 2345 = 2979。
- 继续对2979重复第1步到第3步:拆成2、9、7、9,最大数9972,最小数2799,9972 - 2799 = 7173。
- 继续处理7173:7731 - 1377 = 6354。
- 继续处理6354:6543 - 3456 = 3087。
- 继续处理3087:8730 - 0378 = 8352。注意这里出现了0378这个形式,0378作为四位数是378,但排列的时候要当四位数处理。
- 继续处理8352:8532 - 2358 = 6174。
- 继续处理6174:7641 - 1467 = 6174,后续结果不会再变了。
所以输入3524时应输出6次。这个例子拆完之后,你会发现整个过程本质上就是一个while循环,每次循环做同样的四件事:拆位、排序、重组成两个数、相减得到新数,然后判断新数是不是6174。
1.3 与GESP二级考纲的对应关系
这道题在二级考纲里属于“程序设计与算法”的模拟类问题。大部分考生在循环体内部不会写错,真正容易出问题的反而是几个看起来不起眼的点:读入的数是int类型还是按字符串处理、排序函数要用哪一套、高位为零时怎么保证仍然是四位数、循环条件写在哪里才不会漏掉“输入本身的数字就是6174”以及“差值为0”这两种极端情形。
我见过不少备考小组的同学,第一次写完代码跑测试样例,发现死活跳不出循环,追问之后才发现:当两个数相同导致差值为0时,循环条件判断的是“当前数不等于目标数”,结果0既不是6174又不能继续变成其他数,直接卡死。这种问题在考纲里属于典型的“对循环终止条件考虑不周”,而数字黑洞这道题天然就会逼你把这类边界情况想明白。
2. 实现路线怎么选:字符串排序与拆位数组的取舍
2.1 路线一:字符串排序法
把输入的数字直接转成字符串,用sort对整个字符串排两次,一次从小到大、一次从大到小,然后通过stoi转回整数做差。这个方案最大的优势是代码量少、逻辑直观,尤其适合二级阶段的选手,因为不需要自己写拆位循环。
核心代码骨架长这样:
#include <iostream> #include <string> #include <algorithm> using namespace std; int nextValue(int n) { string s = to_string(n); while (s.length() < 4) { s = "0" + s; } sort(s.begin(), s.end(), greater<char>()); int big = stoi(s); sort(s.begin(), s.end()); int small = stoi(s); return big - small; }这里的关键细节是每次循环开始时都要检查字符串长度,不足四位就补前导零。因为四位数相减的结果可能变成三位数,比如1000 - 0001 = 999,如果不把999当0999处理,排序后得到的两个数就会出错。
2.2 路线二:拆位数组法
另一种做法是不断用取余和整除把每一位数字存进数组:
int digits[4]; int tmp = n; for (int i = 0; i < 4; i++) { digits[i] = tmp % 10; tmp /= 10; } sort(digits, digits + 4); int small = digits[0] * 1000 + digits[1] * 100 + digits[2] * 10 + digits[3]; int big = digits[3] * 1000 + digits[2] * 100 + digits[1] * 10 + digits[0];这招的优点是数组排序后天然带升序,反转一下就是降序,不容易出现字符串排序时搞混升序和降序的问题。缺点是要自己处理四位数补位,如果差值是三位数,下次拆位就只拆出三个数字,数组最后一个位置还残留旧数据,必须每次重置或者补零。
2.3 选型建议
从我实际看过的代码来看,字符串排序法对二级考生更友好,犯错概率更低。字符串排序的思路和人们日常处理数字的习惯更接近:先把数字当成一组字符,排完序再转回数值。拆位数组法则适合已经养成了“所有数位问题都用数组解”习惯的同学。
比较推荐的做法是:主函数用字符串排序法,但把“计算下一个数”的逻辑单独抽成一个函数。这样主循环非常清爽,也方便在评测机上打印中间过程来排查问题。另外,不管选哪种路线,都建议把每步的中间结果打印出来做一次小样本验证,而不要直接提交完事。
3. 手敲代码的完整过程:排序复用、零填充与循环安全
3.1 第一版完整实现
下面给出一版可以直接在评测环境里使用的代码,输出的是最终步数:
#include <iostream> #include <string> #include <algorithm> #include <iomanip> using namespace std; int getNext(int n) { string s = to_string(n); while (s.length() < 4) { s = "0" + s; } string up = s; string down = s; sort(up.begin(), up.end(), greater<char>()); sort(down.begin(), down.end()); int big = stoi(up); int small = stoi(down); return big - small; } int main() { int n; cin >> n; int step = 0; while (n != 6174) { n = getNext(n); step++; if (step > 100) { cout << "loop protection" << endl; break; } } cout << step << endl; return 0; }步数最多7次,所以100次循环保护纯粹是给程序兜底,防止输入数据异常时无限循环。评测系统不会触发这个分支,但考试时写不写保护都能过,我建议写上,图个安心。
3.2 补零细节:为什么1000到999的差值必须当成0999处理
很多人第一次跑这个程序会对“1000”这个输入有疑问。1000四个数字是1、0、0、0,最大数1000,最小数0001也就是1,相减等于999。如果下一轮直接把999当三位数处理,排序后最大数999,最小数999,差值0,然后程序就永远停不下。正确做法是把999当成0999,排序后最大数9990,最小数0999,差值是8991,然后继续循环。
这里展示一下加打印输出的调试版本,方便你观察每一步的变化:
int getNext(int n) { string s = to_string(n); while (s.length() < 4) { s = "0" + s; } string up = s; string down = s; sort(up.begin(), up.end(), greater<char>()); sort(down.begin(), down.end()); int big = stoi(up); int small = stoi(down); int diff = big - small; cout << "n=" << n << " up=" << up << " down=" << down << " big=" << big << " small=" << small << " diff=" << diff << endl; return diff; }跑一下3524,你能看到2979、7173、6354、3087、8352、6174这一整条收敛路径。中间的3087再下一次就变成8352,因为8730减0378等于8352。如果你看到某一步的up串以0开头,那说明补零逻辑没生效,需要立刻检查。
3.3 循环终止条件与步数统计
循环的终止条件有两种写法。第一种是先把当前数处理一步,再判断结果;第二种是进入循环前先判断。这两种写法统计步数的方式不一样,很容易绕晕。我的习惯是用while (n != 6174)进入循环,在循环体内部先算下一步再对step加一。这种写法的好处是:如果输入的n本身就是6174,循环一次都不进,步数直接输出0,完全符合“已经掉进黑洞,无需再运算”的语义。
步数统计的位置也很敏感。如果把step加一放在计算前面,那输入6174时步数会错误地变成1;如果漏加,输入非6174时结果会少1。我建议写完代码后用3524验证输出应为6,再输入6174验证输出应为0,两个样例都能过,这道题的步数逻辑就基本稳了。
4. 评测系统里的边界用例与常见扣分点
4.1 输入1000时的坑
前面已经讲过1000这条路径的特殊性,它在评测里出现频率很高,因为它专门考察补零处理。如果不补零,程序会从999陷入0的循环,评测结果要么超时,要么内存爆炸,反正不会给分。
正确链路的完整过程是:1000 → 0999 → 8991 → 8082 → 8532 → 6174,共5步。你可以对照这个答案测试自己的程序。另外注意,这是用四位补零规则计算的结果。如果某一步输出的是三位数,而程序没有在下一步排序前补零,那答案就会偏。
4.2 输入自身就是6174
还有就是输入本身就是6174的情况。按照题意,应该输出0。很多同学会默认“至少要计算一次”,于是把步数初始化为1,跑出来就错了。这类“初始状态已经是目标状态”的题目,在GESP二级里属于高频陷阱,以后遇到类似的模拟题都要有这个意识。
4.3 重复数字的四位数
重复数字的四位数同样常见,比如2222、3333。这类数字拆出来后,最大数和最小数完全相同,差值一定是0,而0不是6174,程序会在0和0之间无限循环。针对这种情况,题目一般会对输入范围做限制,保证不会出现所有数字相同的四位数。但程序里最好还是有循环保护,或者直接判断差值为0时终止,并输出一个约定好的步数。
如果不加保护,程序会一直循环,评测机判定超时才给你一个大大的零分。这提醒我们:在评测平台上,超时往往比答案错误更致命,因为错误答案至少知道是多少分,超时基本就是零分。
4.4 输出格式要求
GESP这道题对输出格式的要求一般是输出一个整数,表示计算到6174的步数。有些备考资料喜欢用“输出每一步的差值”,但这和原题要求不一样,不要把调试用的打印输出保留在最终提交的代码里。提交前把调试打印注释掉或者删掉,只看最终结果。
如果题目输出的是“经过几次变换后得到6174”,那就按本文的写法。如果题目要求“按某种格式输出每次变换中的最大数和最小数”,那就要额外保留输出语句,注意空格和换行。考试时看清题面,比急着写代码更重要。
4.5 用样例表自查
建议准备一张自查表,提交之前逐个跑一遍:
| 输入 | 预期输出 | 说明 |
|---|---|---|
| 6174 | 0 | 目标状态无需计算 |
| 3524 | 6 | 官方常用样例 |
| 1000 | 5 | 考察前导零处理 |
| 2019 | 3 | 普通四位数正确性 |
| 1234 | 3 | 数字连续但规律正常 |
跑完这张表,如果全部通过,这道题的正确性就有比较高的把握了。如果某个输入不对,先打印每一步的中间结果去对比手动计算的过程,而不是盲目改代码。
5. 从6174到495:变体题目与考场应对思路
5.1 三位数版数字黑洞
数字黑洞不只是四位数的专利,三位数也有自己的黑洞常数495。规则非常类似:把三位数的数字排列成最大数和最小数,相减得到新三位数,继续操作直到495。很多GESP模拟卷会把这道题改编成三位数版本,用来防止考生背题。
三位数版本的代码只需要把补零逻辑从“补到四位”改成“补到三位”,把终止判断从6174改为495,其他地方几乎不用动。这说明数字黑洞题的核心价值在于“通用的模拟框架”:读入、拆位、排序、重组、判断、计数,这六个环节只要掌握好,换个数位范围或者换个数都能轻松应对。
这里给一个快速改造思路:把getNext函数写成接收两个参数,一个是要处理的数,另一个是位数n。补零循环用for循环补到指定位数。终止条件也用一个常量表示。这样就能让同一份代码同时适应四位情况和三位情况。
5.2 考场上的排查流程
真上了考场,遇到数字黑洞这类模拟题,我的建议是先别急着写代码,先在草稿纸上手动走一遍题目附带的样例,把每一步的差值写出来。比如样例是3524,手动算到6174需要6步,心里有底了再写代码。
写完代码之后,如果评测结果不对,优先检查三件事:第一,步数统计逻辑是不是和循环条件矛盾;第二,前导零有没有被正确补上;第三,排序方向有没有反。这三件事覆盖了这道题绝大多数的错误来源。
另外,如果本地跑得好好的,提交到评测机上却超时,先别怀疑评测机,先想想是不是有输入会导致循环永远无法终止。评测系统常用的测试数据里一定有重复数字四位数,程序不能靠“题目保证合法输入”来回避死循环问题,还是那句话,加循环保护,或者对差值为0的情况单独做终止处理。
5.3 这道题之后还能怎么练
刷完数字黑洞,还可以做两个容易混淆的扩展练习。一个是“快乐数”问题,规则是用各位数字的平方和反复运算,直到结果为1或者进入一个固定循环;另一个是“最大公约数辗转相除”的流程模拟,这两者和数字黑洞在“循环执行某个变换规则直到进入终止态”这个思路上是一模一样的。
实际上GESP二级真正想考察的从来不是“你知道6174”,而是“你是否具备用程序描述一个明确计算过程的能力”。数字黑洞是一个漂亮的载体,它把拆位、排序、重组、循环、计数、边界判断全部压缩在一道题里,分量比很多纯语法题高得多。如果你能独立完成这道题,并且能讲清楚每一步为什么要这么做,那二级考试里的模拟类编程题基本就没有大问题了。
我自己的习惯是每道模拟题都留一个调试版和提交版两个文件,调试版保留完整的中间输出,提交版删掉所有调试语句。数字黑洞这道题覆盖的坑足够典型,值得把这套流程完整走一次。你做完之后不妨再把三位数版也跑一遍,用同一套代码框架改参数,感受一下什么是“一套思路走天下”。