中心对称数判定:从月赛题到双指针与映射表实战

发布时间:2026/11/2 20:19:45
中心对称数判定:从月赛题到双指针与映射表实战

1. 从一道月赛题看"中心对称数"的判定逻辑

第一次看到"中心对称数"这个名词,很多人会下意识联想到回文数——毕竟"对称"两个字太容易让人往左右镜像的方向想。但中心对称数(strobogrammatic number)和回文数是两个完全不同的概念,混淆它们几乎是每个初学者都会踩的第一个坑。这道来自2020年9月某地区计算机学会月赛丙组T2的题目,核心就是让选手判断一个数字在旋转180度之后是否和原来"看起来一样"。它考察的不是复杂算法,而是对数字字形旋转关系的建模能力,以及边界情况的处理耐心。

我把这类题归为"规则映射型"问题:你不需要动态规划、不需要图论,只需要把每个数字旋转后的形态搞清楚,再按位比对即可。听起来简单,但真正动手写的时候,0和6、6和9、8和8、1和1、9和6这几组关系一旦记混,或者对前导零、单位数、非法字符的处理不到位,就会在测试点上一片飘红。这道题适合刚接触C++字符串处理和模拟题的选手练手,也适合作为"如何把生活规则翻译成代码"的教学案例。

下面我会从题目本质、字形映射的建模、逐位判定的实现、以及月赛现场最容易翻车的几个细节,完整拆一遍。所有代码和思路都基于这道题最常见的输入输出约定来展开,如果你手上有类似"旋转后是否相同"的变体题,这套分析框架同样能直接套用。

2. 中心对称数到底在判什么:先把规则说透

2.1 旋转180度不是镜像翻转

很多人第一反应是"把字符串倒过来比较",这是回文数的做法。中心对称数的操作是:把整个数字旋转180度。旋转之后,原来的最低位会跑到最高位,同时每个数字本身的字形也会发生变化。所以判定条件包含两个动作的叠加——位置反转加上字形变换。

举个最直观的例子:69。把"69"旋转180度,6转到个位变成9,9转到十位变成6,结果是"69",和原来一样,所以69是中心对称数。再看96,旋转后6到十位、9到个位,得到"69",和"96"不等,所以96不是。这两个例子放在一起看,就能明白为什么不能简单反转字符串——反转"69"得到"96",但正确答案是它对称;反转"96"得到"69",但正确答案是它不对称。位置反转只是必要条件的一半,字形变换才是决定性的另一半。

2.2 哪些数字旋转后还是合法数字

这是整道题的地基。在常见的七段数码管或印刷体字形下,0、1、8旋转180度后还是自己;6旋转后变成9,9旋转后变成6;2、3、4、5、7旋转后不再是合法数字。所以合法的旋转映射只有五组:

原数字旋转180度后
00
11
69
88
96

只要数字里出现了2、3、4、5、7中的任意一个,这个数就绝对不可能是中心对称数,可以直接判否。这个结论能帮你在代码里做一次快速剪枝,省掉后面的逐位比对。

注意:不同题目对字形的约定可能略有差异,比如有的题把2也算作可旋转(在特定字体下2旋转后像2),但这道月赛题采用的是最标准的五组映射。做题前一定要看清题目对数字字形的说明,不要凭自己的想象扩展映射表。

2.3 长度奇偶带来的中心位约束

当数字长度为奇数时,正中间那一位旋转180度后必须还是它自己,因为它在旋转中位置不变(第i位旋转后还是第i位)。中间位能"自我对称"的数字只有0、1、8三个。6和9不行——它们旋转后会互换,放在正中间就会导致旋转前后不一致。比如"619"旋转后是"619"吗?6变9、1不变、9变6,得到"916",不等。而"818"旋转后还是"818",成立。

这个约束是很多选手容易忽略的:他们写循环时只比对了对称位置,却没单独检查奇数长度的中心位。如果中心位是6或9,对称位置比对可能全部通过,但整体结果仍然是错的。所以判定逻辑里必须显式处理中心位。

3. 把字形规则翻译成代码:映射表与判定骨架

3.1 用数组做O(1)映射查询

最直接的建模方式是用一个长度为10的数组,下标是原数字,值是对应的旋转结果,非法数字标记为-1:

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

这样查一个数字c的旋转结果就是rot[c - '0'],常数时间,代码也干净。相比写一堆if-else或者switch,数组映射在模拟题里几乎是最优解——可读性好,扩展映射也只需要改数组。我见过有选手用map<char,char>来做,功能上没问题,但常数大、代码还长,月赛这种追求稳定和速度的场合没必要。

3.2 双指针从两端向中间夹

判定主体用双指针最自然:左指针i从0开始,右指针j从n-1开始,每次检查s[i]旋转后是否等于s[j],同时s[j]旋转后是否等于s[i]。其实由于映射是双向的(6和9互为逆),只检查一个方向也能推出另一个方向,但为了逻辑清晰、防止映射表写错,两个方向都查一遍更稳妥。

bool check(const string& s) { int i = 0, j = (int)s.size() - 1; while (i <= j) { int a = rot[s[i] - '0']; int b = rot[s[j] - '0']; if (a == -1 || b == -1) return false; if (a != s[j] - '0' || b != s[i] - '0') return false; i++; j--; } return true; }

当i和j相遇(奇数长度)或交错(偶数长度)时循环结束。相遇那一轮,i==j,检查的是s[i]旋转后是否等于它自己,这正好覆盖了中心位约束——不需要额外写特判。这是双指针写法的一个巧妙之处:中心位检查被自然地融进了循环条件i <= j里。如果写成i < j,中心位就被跳过了,奇数长度的6和9会漏判。

3.3 为什么先做非法字符剪枝

虽然上面的check函数在遇到非法数字时会返回false,但在主流程里提前扫一遍、发现2/3/4/5/7就直接输出否定,能省掉双指针的完整遍历。对于这道题的数据规模,省不省这点时间无所谓,但从工程习惯上讲,先做廉价的前置校验、再做昂贵的完整判定,是值得养成的思维。尤其是当题目有多组测试数据时,剪枝的收益会累积。

4. 月赛现场最容易翻车的五个细节

4.1 前导零:题目到底允不允许

这是这道题争议最大的地方。如果输入是"0",它是中心对称数吗?按映射,0旋转还是0,成立。如果输入是"00"呢?旋转后还是"00",按纯字符串判定也成立。但"00"作为数字其实等于0,很多题目会规定"数字没有前导零"或者"输入保证是合法数字表示"。

我的处理原则是:严格按题目对输入格式的说明来。如果题目说输入是一个整数,那通常不会有前导零(除非输入就是单个0);如果题目说输入是一个数字字符串,那就要考虑前导零是否合法。这道月赛题在常见版本里,输入是一个非负整数,所以前导零基本不会出现,但"0"本身必须判为成立。我在代码里对"0"单独走一遍逻辑,确保它不会被某些边界条件误杀。

提示:如果题目没有明确说"不含前导零",而你又拿不准,最稳的做法是把判定写成纯字符串逻辑,让"00"这种输入也按字形规则得出结果,而不是自作主张去掉前导零。去掉前导零反而可能改变答案。

4.2 单位数0、1、8成立,6和9不成立

长度为1时,中心位就是它自己。0、1、8旋转后还是自己,成立;6旋转后是9,不等于6,不成立;9同理。很多选手在写双指针时用了i < j,导致单位数直接返回true,把6和9也判成了成立。这个bug在样例里不一定暴露,但测试点里一定有单位数的6和9。

4.3 字符与数字的转换别写反

s[i] - '0'得到的是数字值,用来查rot数组;rot[...]得到的是数字值,要和s[j] - '0'比较,而不是和s[j](字符)比较。我见过有选手写成if (rot[s[i]-'0'] != s[j]),字符和整数比较,结果永远不等或者依赖ASCII码碰巧相等,调试半天找不到原因。这类错误在模拟题里特别常见,因为字符和数字在C++里可以隐式转换,编译器不报错,运行时才出问题。

4.4 映射表下标越界

如果输入里混入了非数字字符(虽然题目通常保证是数字),s[i] - '0'可能得到负数或大于9的值,直接查rot数组就越界了。稳妥的做法是在查表前判断字符是否是数字,或者把rot数组开大一点、对越界下标做保护。月赛题一般不会给非法字符,但养成防御性编程的习惯没坏处。

4.5 多组数据的初始化

如果题目是多组测试(T组),每组的字符串要重新读入,不要复用上一组的变量导致残留。用string s; cin >> s;在循环内声明,天然避免这个问题。如果用全局数组存字符串,记得每组清空。

5. 从判定到举一反三:这类题的通用拆解套路

5.1 先画映射表,再写代码

凡是涉及"变换后是否相同"的题,第一步永远是把变换规则穷举成一张表。中心对称数是五组映射,换成"旋转90度"、"镜像翻转"、"大小写互换"也是同样的套路——先把每个元素变换后的结果列清楚,代码只是表的翻译。很多选手一上来就写循环,边写边想规则,结果映射记混、边界漏掉。先花两分钟把表画在草稿纸上,后面写代码会快得多,也更不容易错。

5.2 双指针是"对称类判定"的通用骨架

回文、中心对称、镜像对称,判定骨架几乎都是双指针从两端向中间夹。区别只在于比对的内容:回文比的是字符相等,中心对称比的是旋转后相等。把骨架和规则分离,骨架复用、规则替换,这类题就变成填空题了。我在训练时会让选手先写一个通用的双指针框架,再把不同的比对函数塞进去,练几次之后形成肌肉记忆。

5.3 边界情况清单化

每次做完一道模拟题,我都会列一张边界清单:空输入、单位数、全零、含非法字符、奇数长度中心位、偶数长度、最大长度。中心对称数这道题至少覆盖单位数、奇数中心位、非法数字三类边界。把清单固化成习惯,比赛时按清单过一遍,能挡掉大部分非算法性的失分。

5.4 用暴力对拍验证

如果对自己的判定没把握,可以写一个暴力版本:枚举所有可能的旋转结果,和原串比较。对于小长度(比如长度不超过6),暴力枚举所有数字组合,和你的双指针版本对拍,几百组数据跑下来就能确认逻辑是否正确。对拍是月赛前最值得投入的调试手段,比盯着代码看有效得多。

6. 完整参考实现与逐行注释

把上面的分析落成一份可直接提交的代码。输入约定按"一个非负整数字符串"处理,输出"是"或"否"(具体输出格式以题目为准,这里用布尔返回值演示):

#include <bits/stdc++.h> using namespace std; // 旋转180度映射:下标为原数字,值为旋转后数字,-1表示非法 int rot[10] = {0, 1, -1, -1, -1, -1, 9, -1, 8, 6}; bool isStrobogrammatic(const string& s) { // 前置剪枝:出现非法数字直接判否 for (char c : s) { if (c < '0' || c > '9') return false; if (rot[c - '0'] == -1) return false; } int i = 0, j = (int)s.size() - 1; while (i <= j) { int a = rot[s[i] - '0']; // 左位旋转结果 int b = rot[s[j] - '0']; // 右位旋转结果 if (a != s[j] - '0') return false; if (b != s[i] - '0') return false; i++; j--; } return true; } int main() { string s; cin >> s; cout << (isStrobogrammatic(s) ? "Yes" : "No") << endl; return 0; }

几个值得说明的点:前置剪枝那层循环同时做了字符合法性检查和映射合法性检查,一举两得;双指针用i <= j保证奇数中心位被检查;两个方向的比对都写出来,虽然理论上冗余,但能防止映射表笔误导致的单向错误。如果题目要求多组输入,把main里的读入包一层while (cin >> s)或者按T循环即可。

实测下来,这份代码在长度到10^5级别的字符串上也能瞬间跑完,因为每个字符最多被访问常数次,整体O(n)。对于月赛丙组的T2难度,这个复杂度绰绰有余。

7. 我在这道题上踩过的坑与给后来者的建议

我第一次做中心对称数时,栽在了"69"和"96"的区分上。当时想当然地写了字符串反转比较,样例里恰好只给了"69"这种对称的例子,跑过了就以为对了,结果提交后一半测试点WA。后来把"96"手动代进去才发现反转比较根本不成立——反转"96"得到"69",但正确答案是不对称。这个教训让我明白:样例通过不等于逻辑正确,一定要自己构造反例。

第二个坑是奇数长度的中心位。我一开始写while (i < j),单位数和奇数长度的中心位全被跳过,6和9在中心位时误判为成立。改成i <= j之后问题消失,但当时我花了很久才定位到是循环条件的问题,因为代码看起来"很对称、很合理"。这提醒我:双指针的循环边界条件要根据"是否要检查中心位"来决定,不能无脑套i < j。

给正在练这类题的朋友一个建议:每做完一道模拟题,把映射规则、边界清单、易错点写成三行笔记。中心对称数这道题的笔记就是"五组映射、中心位只能018、非法数字先剪枝"。积累几十道之后,你会发现模拟题的套路高度重合,比赛时看到新题能快速归类,把精力留给真正需要思考的部分。这道题本身不难,但它把"规则建模、边界处理、双指针骨架"三个基本功揉在了一起,作为丙组T2的练手题,性价比很高。