ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

中心对称数判定:从回文数到字符映射与双指针实现

中心对称数判定:从回文数到字符映射与双指针实现

1. 从一道月赛题看“中心对称数”的判定思路

第一次看到“中心对称数”这个题目名,很多人脑子里冒出来的第一反应是“回文数”,觉得把数字反过来读一遍比对一下不就完了。真动手写的时候才发现,回文和中心对称压根不是一回事。回文是“正着读反着读一样”,比如 121、1331;而中心对称数说的是把整个数字旋转 180 度之后,看起来还是原来那个数。这两个概念在纸面上很像,落到代码里判定逻辑完全不同,这也是这道题当年让不少丙组选手栽跟头的地方。

我先把结论摆出来:中心对称数的本质是一次“字符映射 + 顺序翻转”的联合变换,映射规则只有固定的几组,翻转则是把整个字符串倒序。任何一位上出现了不在映射表里的数字,直接判否;映射完之后再和原串比对,一致才算数。听起来简单,但真正写起来,边界条件、前导零、单位数、非法字符这几处全是坑。

这篇内容适合谁看?如果你正在带学生打基础算法赛、自己刷字符串处理题、或者想给孩子讲清楚“回文”和“旋转对称”的区别,那这篇基本能覆盖你需要的全部细节。我会从题目背后的判定模型讲起,把映射表怎么来的、代码怎么组织、哪些地方容易写错,一层层拆开。哪怕你之前完全没接触过这类题,跟着走一遍也能自己写出一个稳的版本。

需要先说明一点:下面涉及的代码结构、测试用例、排查方法,都是基于这类字符串映射题在常见教学和训练场景下的通用做法做的合理补全,不是对某份原始题解的照搬。重点在于把“为什么这么设计”讲透,让你能迁移到同类型的其他题目上。

2. 核心判定模型拆解:为什么不能当成回文来做

2.1 中心对称数的数学定义与直观理解

先把概念钉死。一个数如果把它整体旋转 180 度后,得到的图形和原来完全重合,它就是中心对称数。注意这里说的是“数字的图形”,不是数值运算。0 转 180 度还是 0,1 还是 1,8 还是 8,这三个是“自对称”的;6 转过来变成 9,9 转过来变成 6,这两个是“互换对称”的;2、3、4、5、7 这五个数字转 180 度之后根本不成形,属于非法字符。

所以判定一个数是不是中心对称数,等价于做两件事:

  1. 逐位检查每个数字是否在合法集合 {0, 1, 6, 8, 9} 里;
  2. 把每一位按映射规则替换(0→0,1→1,6→9,8→8,9→6),同时把整个序列倒序,看结果是否和原序列完全一致。

这里有个特别容易被忽略的点:倒序和映射是同时发生的,不是先映射再倒序或者先倒序再映射的简单叠加。从结果上看两者等价,但从理解上必须清楚,旋转 180 度这个动作本身就包含了“位置翻转”和“字符变换”两个维度。很多同学写代码时只做了映射忘了倒序,或者只倒序忘了映射,测试用例一跑就露馅。

举个具体例子。数字 69:逐位检查 6 和 9 都合法;映射后 6→9、9→6,得到 96;再倒序得到 69,和原数一致,所以 69 是中心对称数。再看 96:映射后 9→6、6→9 得 69,倒序得 96,也一致,所以 96 也是。但 66 呢?映射后 6→9、6→9 得 99,倒序还是 99,和原数 66 不等,所以 66 不是中心对称数。这个例子最能说明问题——66 是回文,但不是中心对称数,两个概念在这里彻底分道扬镳。

2.2 与回文数判定的本质差异

把差异列成表会更清楚。回文判定只关心“正反读是否一致”,中心对称判定关心的是“旋转后是否一致”,旋转这个动作引入了字符映射,这是回文完全没有的环节。

对比维度回文数中心对称数
核心操作字符串反转后比对字符映射 + 反转后比对
合法字符集0-9 全部仅 {0,1,6,8,9}
典型正例121、1331、9969、88、609、1001
典型反例123、45666、121、123
前导零敏感度一般不敏感高度敏感

这张表里最值得盯的是最后一行。回文题里前导零通常不是大问题,因为数值本身不会带前导零;但中心对称数如果按字符串处理,前导零会直接影响判定结果。比如 “010”,映射后 0→0、1→1、0→0 得 010,倒序还是 010,按字符串规则它是中心对称的;但如果当成整数 10 来处理,情况就变了。所以输入到底按字符串读还是按整数读,必须在动手前想清楚,这是这类题第一个分水岭。

2.3 判定流程的伪代码级梳理

把上面的逻辑整理成流程,大概是这个顺序:

  1. 读入待判定的数,统一按字符串保存(避免数值运算丢失前导零或溢出);
  2. 从左到右扫描每一位,遇到不在 {0,1,6,8,9} 里的字符,立即返回“否”;
  3. 对每一位做映射替换,得到一个新串;
  4. 把新串整体反转;
  5. 比较反转后的新串与原串是否相等,相等为“是”,否则为“否”。

这个流程里第 2 步和第 3 步可以合并成一次遍历,边检查边映射,效率更高。第 4 步和第 5 步也可以合并,用双指针从两端向中间夹,一边映射左端一边映射右端,同时比对。合并之后代码更短,但可读性会下降,初学者建议先按五步走通,再考虑优化。

提示:判定流程里“先检查合法性再映射”这个顺序不能反。如果先映射再检查,遇到非法字符时映射表里查不到对应项,程序行为就不可控了,轻则输出错误,重则数组越界。

3. 映射表设计与代码实现细节

3.1 映射表的三种组织方式与选型理由

映射关系只有五组,怎么存都行,但不同存法在代码清晰度和运行效率上有差别。常见的有三种:

  • 数组直存法:开一个长度 10 的数组,下标是原数字,值是映射后的数字,非法位置填 -1 做标记。查表 O(1),代码最直观。
  • 哈希表法:用 map 或 unordered_map 存五组键值对,查不到就说明非法。灵活但有小常数开销,对这道题的数据规模来说属于杀鸡用牛刀。
  • 条件分支法:写一串 if-else 或者 switch,把五种情况列出来。代码最短,但扩展性差,加一组映射就要改结构。

我个人的选择是数组直存法,理由很实在:数字只有 0 到 9 十个,数组天然贴合下标语义,查表就是一次内存访问,没有任何额外开销;非法位置用 -1 标记,判断逻辑统一成“查表结果是否为 -1”,不用写一堆分支。哈希表在这道题里属于过度设计,条件分支则在需要维护时容易漏改。

数组的初始化大概长这样:

int mp[10] = {0, 1, -1, -1, -1, -1, 9, -1, 8, 6};

逐个解释:下标 0 对应 0,下标 1 对应 1,下标 2 到 5 全是 -1(非法),下标 6 对应 9,下标 7 是 -1,下标 8 对应 8,下标 9 对应 6。写完之后拿几个数验一下:mp[6] 应该是 9,mp[9] 应该是 6,mp[2] 应该是 -1,对上了就说明表没写错。

注意:这张表最容易写错的是 6 和 9 的位置。有人习惯按“6 变 9、9 变 6”的顺序写,结果把 mp[6] 写成 6、mp[9] 写成 9,那就变成自对称了,整个判定全错。写完一定拿 69 这个数跑一遍验证。

3.2 双指针夹逼实现与单次遍历实现对比

实现层面有两条路。第一条是双指针夹逼:左指针从头、右指针从尾,同时向中间走,每次检查左指针字符的映射是否等于右指针字符,同时检查右指针字符的映射是否等于左指针字符。两个条件都满足才继续,任一不满足直接判否。

bool isCentrosymmetric(const string& s) { int mp[10] = {0, 1, -1, -1, -1, -1, 9, -1, 8, 6}; int i = 0, j = (int)s.size() - 1; while (i <= j) { int a = s[i] - '0'; int b = s[j] - '0'; if (a < 0 || a > 9 || b < 0 || b > 9) return false; if (mp[a] == -1 || mp[b] == -1) return false; if (mp[a] != b || mp[b] != a) return false; i++; j--; } return true; }

这段代码的关键在mp[a] != b || mp[b] != a这一行。为什么两个方向都要比?因为旋转是对称操作,左边的字符映射后应该等于右边的字符,右边的字符映射后也应该等于左边的字符,两个条件缺一不可。只比一个方向的话,像 68 这种“左映射等于右但右映射不等于左”的情况就会漏判——68 里 6 映射成 9 不等于 8,其实第一个条件就挂了,但换成别的组合就可能出问题,所以两个方向都写上是稳妥做法。

第二条路是单次遍历构造新串再比对,代码更贴近前面讲的五步流程,可读性好但要多开一个字符串空间。数据规模小的时候两者没差别,规模大的时候双指针省空间。我一般教学时先讲单次遍历版,让学生把逻辑走通,再讲双指针版做优化,这样理解曲线更平缓。

3.3 边界条件清单:单位数、偶数位、含零串

边界条件是这类题的重灾区,我整理了一份清单,写代码前对着过一遍能省很多调试时间。

边界情形示例判定结果说明
单个自对称数字0、1、8是映射后不变,天然满足
单个互换对称数字6、9是映射后互换,但单位数反转后位置不变,需特殊确认
含非法数字2、3、4、5、7否直接判否
偶数位全合法69、88、609视情况需完整走映射加反转
奇数位中间位60809视情况中间位必须是自对称数字
含前导零010视情况按字符串处理时需明确规则

这里单独说奇数位的情况。长度为奇数的串,正中间那一位在旋转后位置不变,所以它必须映射到自己,也就是只能是 0、1、8 这三个自对称数字。6 和 9 放在中间位是不行的,因为旋转后 6 变 9、9 变 6,中间位就变了,整个数不可能和原来重合。这个细节在双指针实现里会自动被覆盖——当 i 等于 j 时,条件退化成mp[a] == a,正好把中间位的约束表达出来了,不用额外写判断。这也是双指针写法优雅的地方。

单位数 6 和 9 值得单独拎出来说。按双指针逻辑,i 和 j 都指向同一个字符,条件变成 mp[6] 是否等于 6,答案是否,所以 6 会被判成“否”。但直觉上 6 旋转 180 度确实变成 9,不是 6,所以判否是对的。这里容易产生的困惑是“6 和 9 互为对称,那 6 自己算不算中心对称”,答案是不算,因为它旋转后变成了另一个数。只有 0、1、8 这三个单位数算中心对称数。

4. 完整实操流程与测试验证

4.1 从读入到输出的完整代码骨架

把前面的零件组装起来,一个可直接运行的版本大概是这样。我按“读入、判定、输出”三段式组织,中间判定部分封装成函数,方便单独测试。

#include <iostream> #include <string> using namespace std; bool isCentrosymmetric(const string& s) { int mp[10] = {0, 1, -1, -1, -1, -1, 9, -1, 8, 6}; int i = 0, j = (int)s.size() - 1; while (i <= j) { int a = s[i] - '0'; int b = s[j] - '0'; if (a < 0 || a > 9 || b < 0 || b > 9) return false; if (mp[a] == -1 || mp[b] == -1) return false; if (mp[a] != b || mp[b] != a) return false; i++; j--; } return true; } int main() { string s; cin >> s; cout << (isCentrosymmetric(s) ? "Yes" : "No") << endl; return 0; }

这段代码有几个设计取舍值得说明。第一,输入用 string 而不是 long long,原因是题目里的数可能很长,超出整数范围,而且前导零在字符串里能原样保留,判定语义更清晰。第二,判定函数参数用 const 引用,避免拷贝开销,也表明函数内部不修改原串。第三,输出用三元表达式一行搞定,不写多余的 if-else,保持主干清爽。

如果题目要求输出的是“是/否”之外的具体信息,比如“第几位非法”或者“映射后的结果”,那就在判定函数里加输出或者改成返回结构体,主干逻辑不变。这种分层写法在比赛里改需求时特别省事。

4.2 手工推演三个典型用例

代码写完不能直接交,得手工推几个用例确认逻辑。我挑三个最有代表性的走一遍。

用例一:69。i=0 指向 6,j=1 指向 9。a=6,b=9。mp[6]=9,mp[9]=6,都合法。检查 mp[a]==b 即 9==9 成立,mp[b]==a 即 6==6 成立。i 增到 1,j 减到 0,循环结束,返回真。结论:69 是中心对称数,符合预期。

用例二:66。i=0 指向 6,j=1 指向 6。a=6,b=6。mp[6]=9,mp[6]=9,都合法。检查 mp[a]==b 即 9==6 不成立,直接返回假。结论:66 不是中心对称数。这个用例专门用来抓“把中心对称当回文做”的错误,如果代码判 66 为真,说明映射环节漏了。

用例三:60809。长度 5,中间位是 8。i=0 对 j=4:6 对 9,mp[6]=9 等于 9,mp[9]=6 等于 6,通过。i=1 对 j=3:0 对 0,mp[0]=0 等于 0,通过。i=2 对 j=2:8 对 8,mp[8]=8 等于 8,通过。循环结束返回真。结论:60809 是中心对称数。这个用例覆盖了奇数位和中间位自对称的情况。

三个用例跑完,基本逻辑就验证得差不多了。如果还有余力,可以再加一个含非法字符的用例,比如 123,预期在 i=0 时 mp[1]=1 合法但 mp[3]=-1 非法,返回假,确认非法字符拦截生效。

4.3 批量测试与对拍思路

单个用例过了不代表全对,稳妥做法是造一批数据批量跑。手工造数据可以从几个维度覆盖:长度从 1 到 6 各来几个,合法字符和非法字符混合,前导零单独造一组,全自对称数字造一组,全互换对称数字造一组。

更进一步的做法是写一个暴力对拍程序。暴力版的思路很直接:把原串按映射规则逐位替换得到新串,再把新串反转,最后和原串做字符串比较。这个暴力版逻辑简单不容易写错,用它去和双指针版对拍,随机生成几千组数据,结果不一致就打印出来人工看。对拍是发现边界 bug 最有效的手段,尤其是前导零和奇数位这两类情况,肉眼很难穷举,交给随机数据去撞。

提示:对拍时随机数据的字符集要包含非法数字,否则永远测不出非法字符拦截逻辑的问题。建议按 70% 合法字符加 30% 非法字符的比例生成,覆盖面比较均衡。

5. 常见错误与排查实录

5.1 五类高频错误速查表

带过几轮基础训练之后,我发现这类题的错误高度集中,翻来覆去就那几类。整理成表,写完之后对着自查一遍,能拦下大部分问题。

错误类型典型表现根因修正方法
混淆回文与中心对称66 被判为真只做了反转没做映射补上映射表,两个方向都比对
映射表写反69 被判为假mp[6] 和 mp[9] 写反或写成自对称用 69 单测验证映射方向
漏判非法字符含 2、3、4、5、7 的串被判为真查表没判 -1映射前先检查是否为 -1
前导零处理不当010 判定结果与预期不符输入按整数读导致丢零统一按字符串读入
奇数位中间位漏判668 被判为真中间位没检查自对称双指针 i==j 时条件自动覆盖

这张表里最隐蔽的是最后一行。668 这个串,两端 6 和 8 的映射检查:mp[6]=9 不等于 8,第一个条件就挂了,所以其实不会误判。真正会误判的是 606 这种,两端 6 和 6 映射后都是 9,互相检查 9 对 6 不成立,也会挂。中间位漏判的典型是 616 之外的组合,比如 686,两端 6 和 6 检查时 mp[6]=9 不等于 6,挂。所以严格说中间位漏判在双指针写法里不太容易出现,但在“先构造新串再比对”的写法里,如果构造时把中间位映射错了,就会出问题。这也是我推荐双指针写法的原因之一,边界约束被结构本身兜住了。

5.2 调试时怎么快速定位是哪一位出的问题

判定函数返回假的时候,光知道“假”没用,得知道是哪一位挂的。最快的办法是在判定循环里加临时输出,把 i、j、a、b、mp[a]、mp[b] 都打出来,跑一个失败用例,一眼就能看出是映射错还是比对错。

比如跑 69 输出假,打印出来发现 mp[6] 是 6 而不是 9,那就定位到映射表写错了。跑 66 输出真,打印出来发现根本没进映射比对分支,那就定位到条件判断写漏了。这种“打印中间状态”的土办法在字符串题里比单步调试还快,因为字符串短,打印出来一目了然。

另一个技巧是把判定函数单独抽出来写个小 main 测试,不要和输入输出混在一起。比赛时时间紧可能顾不上,但平时训练一定要养成这个习惯,函数级测试比整体测试定位快得多。

5.3 从这道题能迁移出去的通用套路

中心对称数这道题的价值不在题目本身,而在于它示范了一类“字符映射加序列变换”的题型。同类型的还有镜像字符串、旋转字符串、字母异位词判定等等,套路高度一致:先定映射表,再定变换规则,最后做比对。把这道题的映射表设计和双指针比对吃透,遇到同类题基本就是换张表的事。

我在实际带练时的体会是,这类题最该练的不是写代码的速度,而是动手前把映射关系和变换顺序在纸上画清楚。画清楚了代码二十分钟能写完,画不清楚写两小时还在调边界。中心对称数这道题看着简单,但 66 和 69 这两个用例往那一摆,就能筛出到底有没有真正理解“旋转”和“反转”的区别。这个区分能力,比多做十道回文题有用得多。

RELATED READING

延伸阅读

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