北大OJ 3405 W的密码:字符串分组循环移位详解
2026/9/15 6:46:47 网站建设 项目流程

这道题我印象太深了。每年准备北大计算机考研复试的同学,基本都会在OJ上撞见它——题目编号3405,名字叫“W的密码”。字符串长度不超过100,三个移位参数,输出变换后的结果。听起来像个五分钟能写完的签到题,但真正动手做的时候,不少人会在分组下标、移位取模、多组输入这几个地方翻车。我当年第一次做这题的时候也被“循环右移”这个表述坑过,后来在论坛上看到好几个人问同样的问题,才知道这是题目翻译导致的经典歧义。今天就把这道题的完整思路、实现细节、常见坑一次讲透,不管你是刚开始刷机试还是准备冲刺复试,这篇都值得收藏。

1. 这道题到底在考什么:先读懂题面

1.1 题目规则梳理

先冷静把题目规则拆开看。输入数据是这么构成的:每一组测试数据包含一个字符串,以及三个整数a、b、c。字符串由字母和数字等字符组成,长度不超过100。a、b、c分别表示对三组字符进行循环右移的位数。当a、b、c同时为0时,整个输入结束。

关键规则有两条。

第一条是分组规则。字符串中的字符按位置从1开始计数,第1、4、7、10...个字符属于第一组,第2、5、8、11...个字符属于第二组,第3、6、9、12...个字符属于第三组。说白了就是把整个字符串按“位置模3”分成三个子序列。

第二条是移位规则。第一组中的字母循环右移a位,第二组中的字母循环右移b位,第三组中的字母循环右移c位。这个“循环右移”是凯撒密码式的位移,也就是每个字母独立在字母表里向后移动指定位置,超过末尾就从开头继续。比如'a'右移1位变成'b','z'右移1位变成'a'。大写字母在大写字母表里循环,小写字母在小写字母表里循环。数字、标点这类非字母字符保持原样。

1.2 容易被绕晕的三个点

第一个坑是分组下标的起点。题目说的是第1、4、7个字符属于第一组,但写代码时数组下标从0开始。很多新手直接拿i%3来分组,却忘了对应关系:下标0、3、6才是“第1、4、7个”。换句话说,第一组对应的是i%3==0,第二组对应i%3==1,第三组对应i%3==2。用错了分组顺序,整个输出就全错位了。

第二个坑是“字母循环右移”的理解。我见过有人把题目理解成“每组字符序列整体向右轮转”——比如第一组是"aXc",右移1位变成"caX"。这不是本意。原题的规则是每个字母自己在字母表里向后移动,非字母不参与移动也不参与计数。这种歧义大部分来自网上中文题面翻译不精确,后来在北大OJ的原题语境里默认就是逐个字母做凯撒位移。

第三个坑是参数a、b、c可能很大。字母表一共26个字母,所以移动26位等于没动,移动27位等于移动1位。如果直接拿超大整数去算(char + bigNum) % 26,很容易越界。正确做法是先把步数对26取模,再参与计算。

1.3 多组输入和结束条件

还有一个很容易在考试时突然卡住的点:输入是多组数据,且以“a==0 && b==0 && c==0”作为结束标志。每组数据是“一行字符串 + 一行三个整数”的结构。正常用cin >> str读取字符串没有问题,但如果题目里字符串可能包含空格,就得改用getline。北大机试通常不会在字符串里塞空格,不过我还是建议你养成用getline+ignore的习惯,后面会专门讲这个坑。

2. 整体设计思路与方案选型

2.1 直接遍历法:最符合直觉的做法

这题最直接的思路就是遍历原字符串,对每个字符判断它属于第几组,然后调用一个移位函数处理它。

以0下标为例:

  • i % 3 == 0,属于第一组,使用参数a;
  • i % 3 == 1,属于第二组,使用参数b;
  • i % 3 == 2,属于第三组,使用参数c。

对每个字符,先判断它是不是字母,如果是就根据它属于第几组做对应的凯撒位移;不是字母就直接保留。

这种做法的优势是逻辑简单,不需要额外的存储空间,时间复杂度O(n),空间复杂度O(1)。原题字符串长度不超过100,性能完全不是问题。

2.2 分组取出再放回法:另一种常见思路

网上还有一种解法,先把第一组、第二组、第三组的字符分别取出来放到三个临时数组里,然后对每个数组中的字母进行移位,最后再按原位置放回去。

这个思路在逻辑上更贴近题面描述,但实现起来多了一步“放回”操作,稍不留神就会把位置搞混。比如你用一个数组保存了所有属于第一组的字符位置,移位之后又得遍历这个位置数组,把修改后的字符填回原字符串,容易多写不少代码。

我个人更推荐第一种直接遍历法。既然可以一次扫描解决问题,就不必引入额外结构徒增风险。机试场上时间紧,代码越短越不容易出bug。

2.3 复杂度分析和边界条件

时间复杂度自然是O(n),这里n是字符串长度。对每个字符只做常数次判断和一次赋值,哪怕字符串长度拉满到1000甚至10000,也毫无压力。

边界条件值得提前想清楚:

  • 字符串为空时,循环不执行,直接输出空行;
  • a、b、c为0时,输出原字符串原样;
  • 字符串中全是数字或标点,没有任何字母,所有字符保持原样输出;
  • 字符串长度不足3时,比如长度为1,那么只有下标0属于第一组,其他组没有字符,程序依然正常工作。

这些边界情况虽然简单,但往往就是测试数据里最阴人的部分。

2.4 移位函数的核心逻辑

移位函数是整个程序的核心,代码很短但要注意细节:

char shift(char ch, int k) { k %= 26; if (ch >= 'a' && ch <= 'z') { return 'a' + (ch - 'a' + k) % 26; } if (ch >= 'A' && ch <= 'Z') { return 'A' + (ch - 'A' + k) % 26; } return ch; }

这里先用k %= 26把步数压缩到0到25,避免后面计算数值溢出。然后判断大小写字母,用字符减去基准字符得到相对偏移,加上步数后对26取模,再加回基准字符。非字母直接返回原字符。

好多新手会问:为什么不能直接ch + k再加判断?因为'a'的ASCII码是97,'z'是122,如果ch是'z',k是1,直接相加得到123,这个值对应的是'{',不是'a'。所以必须用相对偏移的方式处理。

3. 完整代码实现(C++版)与逐段讲解

3.1 基础版代码:直接读取字符串

下面这段代码适合题目明确说明字符串不含空格的情况。它是机试里最稳妥的写法,简单直接:

#include <iostream> #include <string> using namespace std; char shift(char ch, int k) { k %= 26; if (ch >= 'a' && ch <= 'z') { return 'a' + (ch - 'a' + k) % 26; } if (ch >= 'A' && ch <= 'Z') { return 'A' + (ch - 'A' + k) % 26; } return ch; } int main() { string str; int a, b, c; while (cin >> str >> a >> b >> c) { if (a == 0 && b == 0 && c == 0) { break; } for (int i = 0; i < (int)str.size(); i++) { if (i % 3 == 0) { str[i] = shift(str[i], a); } else if (i % 3 == 1) { str[i] = shift(str[i], b); } else { str[i] = shift(str[i], c); } } cout << str << endl; } return 0; }

这段代码有个细节值得注意:while (cin >> str >> a >> b >> c)一次性读取字符串和三个整数。如果遇到文件结束或者读取失败,cin会返回false,循环自动结束。这样即使没有0 0 0结束标志,也能安全退出。

3.2 支持空格的进阶版代码

如果你在OJ上遇到字符串可能包含空格的题目,就得换一种读取方式。典型写法是:

#include <iostream> #include <string> using namespace std; char shift(char ch, int k) { k %= 26; if (ch >= 'a' && ch <= 'z') { return 'a' + (ch - 'a' + k) % 26; } if (ch >= 'A' && ch <= 'Z') { return 'A' + (ch - 'A' + k) % 26; } return ch; } int main() { string str; int a, b, c; while (getline(cin, str)) { cin >> a >> b >> c; cin.ignore(); if (a == 0 && b == 0 && c == 0) { break; } for (int i = 0; i < (int)str.size(); i++) { if (i % 3 == 0) { str[i] = shift(str[i], a); } else if (i % 3 == 1) { str[i] = shift(str[i], b); } else { str[i] = shift(str[i], c); } } cout << str << endl; } return 0; }

这里的关键是cin.ignore()cin >> a >> b >> c读完三个整数后,行尾的换行符还留在输入缓冲区里,如果不用ignore把它丢掉,下一轮getline就会读到一个空行,导致程序逻辑错乱。这是新手最容易踩的坑。

3.3 为什么建议用int(str.size())做类型转换

str.size()返回的是size_t类型,本质是unsigned long long之类的无符号整数。如果字符串长度为0,i < (int)str.size()中的i是int,两者比较没问题。但如果直接写str.size() - 1这种表达式,在字符串为空时会出现无符号整数下溢,得到一个巨大的数,循环就乱了。

我在代码里习惯写(int)str.size(),就是提前把这个隐患掐掉。虽然本体的for (int i = 0; i < str.size(); i++)在很多编译器下也能正常跑,但既然有更稳的写法,为什么不直接用呢。

3.4 验证样例

拿最简单的样例验证一下:

输入:

abcabcabc 1 1 1

字符串"abcabcabc",三个参数都是1。按下标0到8遍历:

  • 下标0是'a',属于第一组,移位1位变成'b';
  • 下标1是'b',属于第二组,移位1位变成'c';
  • 下标2是'c',属于第三组,移位1位变成'd';
  • 下标3是'a',属于第一组,变成'b';
  • 以此类推。

输出就是:

bcdbcdbcd

再测试一个包含数字的用例:

输入:

a1b2c3 1 1 1
  • 下标0是'a',第一组,变成'b';
  • 下标1是'1',第二组,非字母,保持'1';
  • 下标2是'b',第三组,变成'c';
  • 下标3是'2',第一组,非字母,保持'2';
  • 下标4是'c',第二组,变成'd';
  • 下标5是'3',第三组,非字母,保持'3'。

输出:

b1c2d3

这两个自测用例能覆盖字母移位和非字母保持两种核心行为,建议你在本地把它跑通再说。

4. 常见错误与调试实录

4.1 分组下标错位:一错错一排

这是我看到过最多的错误。因为字符串在程序里从下标0开始,新手很容易把第一组写成i%3==1,结果整个字符串的处理全部错位。比如字符串"abcdef",正确分组应该是:

原字符下标正确分组错误分组(如果i%3==1当第一组)
a0第一组第三组
b1第二组第一组
c2第三组第二组
d3第一组第三组

一眼就能看出,一旦分组起点错了,每个字符用的移位参数全都不对,输出面目全非。调试的时候如果发现输出结果规律性地“整体错位”,优先检查分组判断条件。

4.2 移位参数没有先取模

a、b、c的值可能非常大,比如10000。如果不先对26取模,在做(ch - 'a' + k)时,k的数值会很大,虽然char加int会自动提升为int,但最后对26取模的结果不会错,问题是有些编译器或者后续处理可能出现意外。更重要的是,如果某个版本的实现里你直接对char类型做运算再赋回char,可能因为溢出得到负数,最后变成乱码。

我在实际写代码时习惯在shift函数第一行就写k %= 26,这属于防守型编程,成本几乎为零,却能避免很多隐蔽问题。

4.3 多组输入的读取死循环

while (getline(cin, str))配合cin >> a >> b >> c时,如果忘记cin.ignore(),第一次读取正常,第二次getline会直接读到上一次残留的换行符,返回一个空字符串,然后整组数据全部错乱。更严重的可能造成死循环,程序一直空转。

有个简单的验证方法:在本地调试时输入两组数据,观察第二次读到的str是不是空串。如果是,就说明缓冲区的换行符没清干净。

4.4 非字母字符处理遗漏

有个不太容易想到的错误是:有的同学写了移位函数,但只处理了小写字母,忘记大写字母。或者反过来。题目明确说了字符可能包含数字,且大小写字母都可能出现,所以移位函数必须同时覆盖'a'-'z''A'-'Z'两个区间。

我自己在机试中遇到过一个大写字母测试点,当时因为只处理了小写字母,WA到怀疑人生。后来养成了习惯:凡是跟字符相关的题目,读题后第一件事就是确认大小写是否都要处理。

4.5 常见问题速查表

症状可能原因排查思路
输出整体错位分组判断的模数起点写错打印i和i%3,对比题目分组
字母移位结果乱码移位参数未取模或字符运算溢出检查shift函数第一行是否有k%=26
第二组测试数据读取错误缺少cin.ignore()在cin >> a >> b >> c后加cin.ignore()
大写字母没变移位函数漏掉大写分支检查是否同时处理'A'-'Z'
数字或标点被改动非字母分支处理错误确认非字母原样返回

5. 举一反三:这类题的变形与刷题建议

5.1 和本题相似的机试题目

这道题是字符串处理类题目里的经典代表。和它相似的题还有几类:

  • 纯凯撒密码题:给定一个字符串和一个偏移量,把所有字母统一移位;
  • 分组加密类:把字符串按一定规则分组,每组用不同参数处理;
  • 字符串原地修改类:要求在不增加额外数组的情况下完成字符替换。

这些题核心考点都一样:字符ASCII操作、取模运算、下标映射、输入输出处理。把“W的密码”吃透,这些题基本就是改改参数的事情。

5.2 我在刷题时的一些习惯

第一,拿到题目先在草稿纸上手动模拟一个样例,不要急着写代码。把分组、移位、非字母保留这些步骤在纸上过一遍,很多逻辑错误在写代码之前就能暴露。

第二,写完代码后用几组边界数据自测,包括空字符串、全数字字符串、只有一个字符的字符串、参数为0的情况。这些数据用不了几分钟,但能帮你挡掉一半以上的WA。

第三,如果提交后WA,不要盲目改代码。先构造一个自己能算出答案的小样例,一步步跟踪程序输出,定位第一个出错的字符,再反向找原因。字符串处理题调试起来其实很快,关键是别慌。

5.3 推荐的学习路线

如果你正在准备考研机试,我的建议是把这类基础题放在刷题计划的第一周攻克。字符串处理、简单模拟、排序、查找这些基本功过了之后,后面做图论、动态规划才会顺手。这道题完全可以作为热身题,每天默写一遍,直到不用看代码就能一次通过。

我自己刷题多了之后,慢慢发现一个规律:机试场上真正卡住人的,往往不是算法本身,而是这种“明明看懂了题,代码写出来却不对”的挫败感。而消除这种感觉的唯一办法,就是把基础题练到形成肌肉记忆。“W的密码”就是值得形成肌肉记忆的题目之一。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询