ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

B2115密码翻译题解:字符串处理、取模回绕与输入读取的坑

B2115密码翻译题解:字符串处理、取模回绕与输入读取的坑 如果你刚开始刷算法题多半会在某个OJ的入门字符串题单里碰到一个叫“密码翻译”的题目它的编号可能是B2115也可能叫别的但考察的东西一样给一行字符串把英文字母按字母表循环后移两位非字母字符原样输出。这题看起来就是“遍历一遍改一下字符”的事但我翻了翻周围人的提交第一次做的人普遍要WA个一两回才过而且大多数WA不是栽在译码逻辑上而是栽在输入读取上。这篇就从一个过来人的角度把这题的考点、写法、坑和延伸一起捋清楚。B2115这类题最迷惑人的地方在于它太简单了简单到让人觉得不需要动脑子。可越是这样越容易在细节上翻车。我见过有人把字母加2就提交结果y变成了{也见过有人用scanf读字符串结果带空格的测试数据一进来就错。等你把这些问题全部解决完才会意识到这道题真正想训练的不是“你知道凯撒密码”而是“你能不能把字符串处理得滴水不漏”。1. 看懂题目真正的考点字符串替换背后的基本功1.1 题目究竟让你做什么先按最常见的题目描述来拆解。B2115密码翻译的规则通常是这样的输入一行字符串其中可能包含大写字母、小写字母、数字、空格和各种标点。你要做的是把每一个英文字母替换成字母表中它后面第二个位置的字母。举个例子a变成cb变成dX变成Z。问题是到了边界怎么办y和z后面已经没有字母了所以题目规定循环处理——y变成az变成b。非英文字符比如空格、数字、逗号、句号一律原样输出不改变。不同OJ上这题的描述会有细微差别有的版本把后移位数改成3有的版本会给你加密函数让你写解密函数还有的版本要求字母表整体倒序映射。但核心逻辑完全一样这本质上是凯撒密码Caesar Cipher的变种把明文字母沿着字母表平移固定的位数。要是题目把规则换成后移三位你就想象成古罗马的将领在传递军令换成后移两位其实就是常见的“往前推两个字”的小把戏。你只要抓住“字符到数字、数字加偏移量、取模回绕、再变回字符”这一条主线不管题目怎么改写代码骨架都不用动。1.2 题目真正想考察的三个层次这道题在竞赛和OJ入门题单里出现频率极高不是因为它有难度而是因为它是很好的“字符串基本功试金石”。按我的理解它一共考了三层东西第一层是输入输出能力。OJ的测试数据往往不止一组而且字符串可能自带空格怎么把一整行完整读进来怎么检测文件结束这是第一个要被拷问的点。习惯用scanf(%s)读字符串的初学者在这一步就会爆雷。第二层是字符运算能力。英文字母在计算机里本质是整数对应ASCII码。你需要明白A是65、a是97并且能熟练地在字符和0到25的数字之间来回转换。取模运算在这里的核心作用是处理“循环”不理解这一点边界字母永远处理不对。第三层是程序结构能力。一个成熟的解法应该把“字符翻译”抽成独立的逻辑而不是把住main函数里塞一堆if-else。这样代码不仅可读性高将来规则一变你只需要改一个参数。这三层能力在B2115里都能得到充分训练。所以别嫌这道题简单它就是用来检验你基础扎不扎实的。2. 三种主流解法拆解从C语言到Python的取舍2.1 最朴素的逐字符判断写法C/C实现如果你参加的是C/C比赛最常见的写法就是逐字符遍历判断大小写后分别处理。我先把完整代码放出来#include stdio.h int main() { char s[1005]; while (fgets(s, sizeof(s), stdin)) { for (int i 0; s[i] s[i] ! \n; i) { if (s[i] a s[i] z) { s[i] (s[i] - a 2) % 26 a; } else if (s[i] A s[i] Z) { s[i] (s[i] - A 2) % 26 A; } } printf(%s, s); } return 0; }这段代码里有几个地方值得单独拎出来讲。fgets(s, sizeof(s), stdin)负责读取一整行包括空格。这是处理带空格字符串的可靠方案。fgets会把换行符也读进来放进数组末尾所以我用s[i] ! \n作为循环停止条件之一否则你可能会错误地把换行符当空格处理。最核心的是那一句(s[i] - a 2) % 26 a。它的意思是先把字符转成它在字母表中的序号比如a是0b是1……z是25然后加2得到平移后的序号用% 26做回绕这样25加2变成27取模后变成1对应b最后再加上a的ASCII码把序号变回字符。大写同理只是基准值从a换成了A。这个写法的好处是直白、没有多余依赖、执行效率也高。坏处是如果你第一次接触可能看不懂那一长串表达式是在干嘛。建议你在草稿纸上拿y手推一遍y的序号是242422626%26009797也就是a完全正确。2.2 Python风格的简洁实现及其适合人群对于刷Python的选手代码会更简洁也更贴近自然语言import sys for line in sys.stdin: res [] for ch in line: if a ch z: res.append(chr((ord(ch) - ord(a) 2) % 26 ord(a))) elif A ch Z: res.append(chr((ord(ch) - ord(A) 2) % 26 ord(A))) else: res.append(ch) print(.join(res), end)Python里没有C语言那种“字符就是整数”的隐式转换所以需要ord()把字符转成ASCII码处理完再用chr()转回来。没有end的话print会自动追加一个换行符而原字符串末尾本身已经有换行符了会多出一个空行。这种细节不是题目考察的重点但却是实际提交时最容易影响结果的隐藏因素。从学习角度说Python写起来舒服能帮你更专注于字符运算的逻辑本身。如果目标是为了熟悉算法竞赛我还是建议你用C/C把同样的逻辑也实现一遍。用两种语言各写一次你会对“字符与整数转换”“取模回绕”这些概念理解得更透。2.3 为什么不建议一上来就查表和硬编码我还见过一种解法直接把映射关系写成一个查表字符串比如用cdefghijklmnopqrstuvwxyzab来映射小写字母大写同理。这样做在小偏移量下确实能过但我不推荐因为这里有个性价比问题。硬编码查表有三个明显的坑。第一是可扩展性差题目如果改成偏移三位、偏移五位你得重新写一遍映射表还很容易写错。第二是肉眼检查困难一串26个字母你很难一眼看出哪里丢了字符或者多错位了一位排错成本很高。第三是它完全绕开了字符运算的训练失去了这道题本来的练习价值。从工程角度讲查表法在特定场景比如替换规则极其复杂、无法用公式表达是有意义的但对于B2115这种规则清晰的题目数学表达式才是最稳妥、最优雅的方案。我把三种写法摆在一起对比一下写法可读性扩展性出错风险适用场景字符加减取模中等需要理解ASCII强改一个数字即可低OJ刷题、竞赛Python字符处理高接近自然语言强低快速原型、日常脚本硬编码查表高但维护性差弱每次规则变化都要重写较高规则复杂且固定的业务3. 我在提交时反复踩的坑输入陷阱与边界条件排错实录3.1 带空格的整行字符串scanf的失效现场先说我自己第一次WA的经历。当时我用的是scanf(%s, s)然后在本地试了abc输出cde一切正常。我很自信地提交结果判了个WA而且完全不知道错在哪。后来我拿测试数据改成本地文件一跑发现输入hello world程序只输出了jgnnq后面的world整个消失了。查了文档才反应过来scanf(%s)是按空白字符切分的读到空格就停所以它只处理了“hello”这一小段。这个坑是所有C语言初学者都会踩的差别只是早晚。解决办法很简单用fgets读整行或者用getchar()配合循环自己拼字符串。之后我就形成了条件反射——只要题目说“给定一行字符串”且可能含空格绝不碰scanf(%s)。这里还要提一下EOF。OJ通常会有多组测试数据代码得一直读到输入结束才能停。fgets读到文件尾时会返回NULL所以while (fgets(...))就是标准的读满全部数据的写法。换成Python就是sys.stdin的迭代。3.2 循环移位的取模运算字母表“转圈”的数学处理如果你理解不了取模边界永远是个坎。有人图省事直接写s[i] s[i] 2结果y加2变成ASCII码123对应的{直接原地爆炸。问题就出在没有人告诉程序“字母表是循环的”它也不知道到了z要折回a。取模运算% 26是处理这种循环的数学手段。关键是先做“字符到序号”的转换再进行加减和取模最后还原成字符。还有个进阶细节如果题目要求的是“前移”比如反向翻译你会写出(s[i] - a - 2) % 26但C语言里负数取模的结果是负数比如(0 - 2) % 26在C里是-2而不是24。这时必须做“加26再取模”的修正写成(s[i] - a - 2 26) % 26。当时我盯着负数的测试数据想了好一阵还以为是取模运算坏了。后来才意识到这是C语言负数整除的规则问题。你如果也遇到类似现象记住一句话先加一个模数把负数修正掉再取模就不会出错。3.3 大小写分支与非法字符处理三个if的顺序问题大小写和非法字符的处理看起来是纯体力活但分支顺序直接影响正确性。我的习惯是先判断a到z的范围再判断A到Z的范围最后剩下的全部原样保留。这里有个容易被忽略的点Z的ASCII码90其实是小于a的ASCII码97的所以不要想当然地认为“先大写后小写”就一定正确关键是范围判断要写完整。比如有人漏写了 Z只写了if (s[i] A)那么所有小写字母也会落入这个分支得到一堆乱码。还有一种高级写法是用ctype.h里的isalpha、islower、isupper能省不少事但要注意在标准C里这些函数只保证对“属于该分类的字符”返回非零结果对非字母字符直接调用tolower如果没有配合判断在某些实现下会有未定义行为风险。稳妥的做法永远是先判断范围再改字符。4. 从B2115延伸出去凯撒密码、ROT13与OJ字符串题的通用套路4.1 把规则泛化成“偏移量”一套代码通吃B2115这题实际上就是一个固定偏移量的凯撒密码。如果你看穿了这一点完全可以把代码里的“2”提炼成一个参数写一个通用的翻译函数。比如Python版本def caesar_shift(text, offset): result [] for ch in text: if a ch z: result.append(chr((ord(ch) - ord(a) offset) % 26 ord(a))) elif A ch Z: result.append(chr((ord(ch) - ord(A) offset) % 26 ord(A))) else: result.append(ch) return .join(result)这样一改B2115的答案是caesar_shift(line, 2)如果哪天题目要求后移3位就是把第二个参数改成3其他完全不动。以后遇到任何“偏移式”的密码题你都可以直接复用。更妙的是这个函数同时支持加密和解密。偏移量为正就是加密为负就是解密。你只需要注意负数偏移要像前面说的那样修正取模比如(ord(ch) - ord(a) offset) % 26在Python里本身对负数取模会得到正确结果但在C语言里就必须写成(ord(ch) - ord(a) offset 26) % 26。跨语言对比一下你会发现语言特性差异也能帮你加深对取模运算的理解。4.2 真正能拉开差距的扩展思考B2115这种题做一遍就够了吗从刷题策略上讲它只是个起点。围绕它至少可以延伸出三类值得继续钻的方向。第一类是变种规则。比如ROT13它是偏移量固定为13的凯撒密码因为26的一半是13所以加密两次就得到原文。这种对称性很有趣在论坛上有人用它来隐藏剧透内容背后就是B2115的同一套原理。你可以在本机把刚才的函数offset改成13试试把一段英文加密再解密验证一下效果。第二类是性能优化。如果输入数据量极大逐字符用printf输出会非常慢因为每次调用都有I/O开销。实际竞赛中可以把结果先存进一个大的缓冲区最后一次性输出或者使用更快的输入输出函数。这种“I/O即性能瓶颈”的思路对后面做更难的题很有帮助。第三类是字符串题目的通用解法框架。B2115要求“保留原格式、只改字母”这一模式和很多字符串处理题一脉相承先确定字符子集是不是字母再按规则做变换最后拼回去。你看回文判断、单词反转、大小写转换、进制转换底层都是这套东西。把B2115的骨架吃透等于给你的字符串处理内功做了一次小升级。4.3 实测下来我对这题的最终体会最后一次过掉这题之后我又专门试了几种写法。实测下来的体会是这个题的难度不在题本身而在你能不能一次想清楚边界条件。我的最终固定写法就三步——先把“字符转序号”想成把字母映射到一圈数字上再规定好移动方向和步数最后解决走过头了怎么办。只要这三步在脑子里跑通提交就不会慌。对刚开始刷题的朋友我有一个朴素建议AC之后别急着看下一题把代码里处理输入的部分注释掉换一种输入方式再写一遍再把这题改成偏移三位重新跑一遍。这种“强迫自己换个姿势写同一道题”的做法比连续刷三道同类型题还涨功夫。我当年就是靠着这种笨办法把字符串处理的基础打得比较扎实后面遇到类似的题目基本都是一次过不再需要反复试错。最后分享一个自检小技巧。你可以把自己代码的输出结果重新作为输入用同样的翻译规则再处理一遍看能不能还原出原字符串。这个“往返测试”对凯撒密码类题目尤其好用写完代码后在本地跑一遍这个测试90%的边界问题都能提前暴露。这套方法不光适用于B2115以后做任何有对称性的编码解码题都可以照葫芦画瓢。
RELATED READING

延伸阅读

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