PAT乙级的题库刷到1014这道“福尔摩斯的约会”时,我停了一下。不是因为这道题算法有多难,而是它的题目描述实在是太像一道“脑筋急转弯”:四行字符串,长得跟乱码似的,却要从中读出星期几、几点几分。当时我第一遍读题,差点把规则看混,对照样例推了两遍才真正搞清楚每一对字符的匹配边界。等你把这题吃透了会发现,它覆盖了PAT乙级字符串题里最常见的三类坑:范围限定、位置连续性、字符到数值的映射。这篇文章我会从题目规则开始,一步步拆到完整代码,再把刷题时踩过的坑逐个讲明白,给正在准备PAT乙级真题的读者一份可以直接照抄的解题模板。
1. 一道字符串处理题,为什么值得逐字拆解
1.1 题面回顾:乱码背后的约会线索
大侦探福尔摩斯接到一张奇怪的字条:“我们约会吧!3485djDkxh4hhGE 2984akDfkkkkggEdsb s&hgsfdk d&Hyscvnm”。大侦探很快就明白了,字条上奇怪的乱码实际上就是约会的时间“星期四 14:04”。
很多第一次看到这道题的人都会觉得莫名其妙:这四坨字符串到底是怎么转换成“THU 14:04”的?其实题目描述里已经给出了完整的解码规则,只是这三条规则全都藏在文字里,不逐字抠出来很容易漏。
用PAT标准格式来说:
- 输入:四行字符串,每行不超过60个字符,不含空格。
- 输出:一行,格式为“DAY HH:MM”。其中DAY是星期的三位缩写:MON表示星期一,TUE表示星期二,WED表示星期三,THU表示星期四,FRI表示星期五,SAT表示星期六,SUN表示星期日。题目保证每个测试点存在唯一解。
这道题的输入格式有一个很友好的地方:四行字符串都不含空格。这意味着在C语言里直接用scanf("%s")按空白字符切分就能一次性读完,不用考虑带空格的读入问题。这一点在PAT乙级里算是相当良心的设置了。
1.2 这道题在考什么:不是算法,是耐心
PAT乙级整体的难度定位是“基础数据结构和简单模拟”,1014就是典型的模拟题。模拟题有一个共同点:你不需要会回溯、动态规划、图论这些进阶内容,但你必须把题目描述中的每一句话都精确翻译成代码逻辑。一个条件写错,整个输出就废了。
具体到这道题,核心考点可以拆成三块:
- 字符串逐字符配对:拿s1和s2逐位比较,拿s3和s4逐位比较。
- 范围限定条件:不是任何相同字符都能用,必须是A-G、0-9或A-N、英文字母这些指定区间内的字符。
- 字符到数值或枚举的映射:把字母映射成星期几,把字符映射成小时,把位置映射成分钟。
这三块单独拿出来都不难,但组合到一起,就会让粗心的人连环翻车。我后面会详细讲我在实际刷题过程中踩过的四个坑,每一个都是血泪教训。
另外多说一句,很多同学是因为翁恺老师的C语言课程才开始接触PAT练习题的,PAT平台本身就是浙大出的一套在线评测系统,乙级题目对初学者非常友好。1014这道题在翁恺老师的课程讨论区里也是高频问题,基本每次开课都有人问“为什么我的代码样例能过,提交就是错”。原因大多出在我后面要讲的边界条件上。
2. 约会时间的四行输入与三条解码规则
2.1 规则一:前两行中第一对A-G大写字母决定星期
把s1和s2逐位比较,找到第一对完全相同的字符。但并不是任意相同字符都能用来推断星期——这个字符必须是大写英文字母,并且范围限定在‘A’到‘G’之间。
为什么是‘A’到‘G’而不是‘A’到‘Z’?因为一周只有7天。‘A’到‘G’正好对应星期一(MON)到星期日(SUN)。如果第一对相同的字符是‘X’或者‘H’,那它们即使完全相同也不能作为星期的依据,必须继续往后找,直到遇到一个A-G范围内的大写字母为止。
用题目示例来走一遍:
s1 =3485djDkxh4hhGEs2 =2984akDfkkkkggEdsb
逐位比较:
- 索引0:
3与2不同 - 索引1:
4与9不同 - 索引2:
8与8相同,但8不是大写字母,跳过 - 索引3:
5与4不同 - 索引4:
d与a不同 - 索引5:
j与k不同 - 索引6:
D与D相同,且D落在‘A’到‘G’区间内
所以第一对合法字符是索引6的D。D是字母表第4个字母,对应星期缩写THU(星期四)。
这里代码里一定要写完整的两个条件:s1[i] == s2[i] && s1[i] >= 'A' && s1[i] <= 'G'。只写s1[i] == s2[i],会在遇到8这个字符时误判;只写大写字母判断但忘了范围限制,又会在遇到H及以后的字母时误判。两个条件缺一不可。
2.2 规则二:第一对之后,第二对0-9或A-N字符决定小时
这一条是整个题目里最阴险的地方。
题目描述说的是“第2对相同的字符”,但这个“第2对”的查找起点,并不是从字符串开头重新找,而是在已经找到第一对字符的位置之后继续找。也就是说,扫描过程是连续的:先在s1和s2的相同索引上找到第一对合法大写字母(A-G),然后从这个位置的下一个索引开始继续往后扫,找到下一对符合小时范围的相同字符。
小时字符的合法范围是‘0’-‘9’或者‘A’-‘N’,映射关系如下:
| 字符 | 小时 | 字符 | 小时 |
|---|---|---|---|
| 0 | 00 | A | 10 |
| 1 | 01 | B | 11 |
| 2 | 02 | C | 12 |
| 3 | 03 | D | 13 |
| 4 | 04 | E | 14 |
| 5 | 05 | F | 15 |
| 6 | 06 | G | 16 |
| 7 | 07 | H | 17 |
| 8 | 08 | I | 18 |
| 9 | 09 | J | 19 |
| K | 20 | ||
| L | 21 | ||
| M | 22 | ||
| N | 23 |
为什么用‘A’-‘N’补足10到23?因为24小时制里数值0到23一共24个取值,数字字符0-9占了10个,剩下的14个位置刚好用‘A’到‘N’这14个连续大写字母填充。这是一个很典型的PAT式设计:用连续的字符区间映射连续的数值区间。
继续看示例。第一对合法字符在索引6找到,所以从索引7开始继续:
- 索引7:
k与f不同 - 索引8:
x与k不同 - 索引9:
h与k不同 - 索引10:
4与k不同 - 索引11:
h与k不同 - 索引12:
h与g不同 - 索引13:
G与g相同!但G不在合法范围内(它大于N),而且注意这里是一个大写G配一个小写g,字符相同但一个是字母的情况并不影响比较结果,关键是它不在0-9或A-N区间内,必须忽略,继续往后找 - 索引14:
E与E相同,且E落在‘A’-‘N’范围内,E - 'A' + 10 = 14
所以小时是14。如果不处理索引13那个“看起来相同但不合法”的G,直接把它当作小时字符处理,G - 'A' + 10 = 16,输出就会变成16点,整道题直接错掉。
2.3 规则三:后两行中第一对相同英文字母的位置决定分钟
分钟部分和前两行完全独立,只看s3和s4这两行。规则是:找到第一对完全相同的英文字母(大写A-Z或小写a-z都行),然后记录它在字符串中的索引位置,这个索引值从0开始计数,就是分钟数。
示例中:
s3 =s&hgsfdks4 =d&Hyscvnm
逐位比较:
- 索引0:
s与d不同 - 索引1:
&与&相同,但&不是英文字母,跳过 - 索引2:
h与H不同,注意这里虽然都是字母且可以看作同一个字母的两种大小写,但题目要求“完全相同”,所以不算配对,继续 - 索引3:
g与y不同 - 索引4:
s与s相同,且是小写字母,符合条件,分钟就是4
这里最关键的一点是:分钟数就是索引位置本身,而不是位置+1。所以如果第一对相同字母出现在索引0,分钟就是0,输出“00”。我见过不少人在这一步加了个1,导致所有涉及分钟的输出都差了一位。
2.4 三条规则的完整串联
把三条规则拼起来,整个解码过程就是:
- 在s1和s2上从左到右扫描,找第一对相同的A-G大写字母,映射为星期。
- 从上一对的位置之后继续扫描,找下一对相同的0-9或A-N字符,映射为小时。
- 在s3和s4上从左到右扫描,找第一对相同的英文字母,用索引值作为分钟。
- 按“DAY HH:MM”格式输出,小时和分钟都要两位,不足补0。
示例最终输出就是THU 14:04。这和你手推的结果完全一致。
3. 完整实现:C语言与Python两个版本的逐步讲解
3.1 C语言版本:最贴近PAT评测机的写法
PAT的评测环境对C语言支持非常友好,大部分考生首选也是C或C++。下面这个版本我用C语言写,逻辑上和上面拆解的规则一一对应。
#include <stdio.h> int main() { char s1[61], s2[61], s3[61], s4[61]; scanf("%s %s %s %s", s1, s2, s3, s4); char *week[] = {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; int i, j; // 规则一:找第一对相同的 A-G 大写字母,确定星期 for (i = 0; s1[i] != '\0' && s2[i] != '\0'; i++) { if (s1[i] == s2[i] && s1[i] >= 'A' && s1[i] <= 'G') { printf("%s ", week[s1[i] - 'A']); break; } } // 规则二:从 i+1 继续找第二对相同的 0-9 或 A-N 字符,确定小时 for (i++; s1[i] != '\0' && s2[i] != '\0'; i++) { if (s1[i] == s2[i]) { if (s1[i] >= '0' && s1[i] <= '9') { printf("%02d:", s1[i] - '0'); break; } else if (s1[i] >= 'A' && s1[i] <= 'N') { printf("%02d:", s1[i] - 'A' + 10); break; } } } // 规则三:在 s3、s4 中找第一对相同的英文字母,索引值即分钟 for (j = 0; s3[j] != '\0' && s4[j] != '\0'; j++) { if (s3[j] == s4[j]) { if ((s3[j] >= 'A' && s3[j] <= 'Z') || (s3[j] >= 'a' && s3[j] <= 'z')) { printf("%02d\n", j); break; } } } return 0; }这段代码有几个细节值得单独说明:
第一,数组长度开61。题目说每行不超过60个字符,C风格字符串末尾还要有个\0,所以开61最稳。如果开60,字符串满长度时\0会写到越界位置,虽然很多情况下不报错,但属于未定义行为,PAT评测机上可能出现诡异错误。
第二,小时输出用%02d而不是手写补零。当字符是'0'到'9'时,s1[i] - '0'得到整数0到9,%02d自动补成00到09。当字符是'A'到'N'时,s1[i] - 'A' + 10得到整数10到23,本来就需要两位。这样统一用%02d格式化,代码最简洁。
第三,规则二的for循环里,i++是直接在原来基础上加1。因为规则一的循环在break时,i停在第一对字符的索引上,规则二从i+1继续,正好符合“在第一对之后继续找”的要求。这里如果一不小心把i++写成i = 0,就会从头开始,大概率找到的还是第一对那个字符,直接死循环。
第四,规则三的字符范围判断。很多初学者会用isalpha()函数来判断英文字母,但在C语言的ctype.h里,isalpha()对某些本地化环境下的扩展字符也可能返回真。PAT的输入虽然只包含ASCII字符,用isalpha()其实也没问题,但在严谨的工程视角下,自己写范围判断更可靠,而且不依赖额外头文件。
3.2 Python版本:适合快速写题和本地验证
Python在PAT平台上的运行速度比C慢,但乙级题目的数据量通常不大,1014这题用Python完全没有性能压力。下面这个版本逻辑和C语言版完全等价,适合作为对照理解。
s1 = input().strip() s2 = input().strip() s3 = input().strip() s4 = input().strip() week = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"] # 规则一:找第一对相同的 A-G 大写字母 for i in range(min(len(s1), len(s2))): if s1[i] == s2[i] and 'A' <= s1[i] <= 'G': print(week[ord(s1[i]) - ord('A')], end=' ') break # 规则二:从 i+1 继续找第二对相同的 0-9 或 A-N 字符 for i in range(i + 1, min(len(s1), len(s2))): if s1[i] == s2[i]: if '0' <= s1[i] <= '9': print(f"{s1[i]}:", end='') break elif 'A' <= s1[i] <= 'N': print(f"{ord(s1[i]) - ord('A') + 10:02d}:", end='') break # 规则三:在 s3、s4 中找第一对相同的英文字母 for j in range(min(len(s3), len(s4))): if s3[j] == s4[j] and s3[j].isalpha(): print(f"{j:02d}") breakPython版本的几个注意点:
第一,input().strip()很有必要。虽然输入不含空格,但行尾可能带有换行符,strip()可以把换行去掉,避免字符串末尾混入\n导致比较错位。在C语言里scanf("%s")会自动跳过空白符并加\0,所以不会有这个问题,但Python的input()不会自动去除行尾的\n,必须手动处理。
第二,range(i + 1, ...)这里的i是规则一循环结束后的值。Python和C一样,循环结束后循环变量会保留最后一次的值。如果规则一在索引6处break,i就是6,规则二就会从7开始。这个语法特性在Python里完全支持,放心用。
第三,s3[j].isalpha()可以判断字母,但它同时覆盖大写和小写。这道题只要求“英文字母”,所以用isalpha()是正好的。如果你担心输入里混入其他语言的字符,可以自己写范围判断,但PAT的输入数据不会出现这种情况。
3.3 两个版本之间的差异点总结
C语言和Python在实现这道题时最大的差异,是如何处理空字符和字符串结束。
C语言用'\0'判断字符串结束,所以循环条件是s1[i] != '\0' && s2[i] != '\0'。这样写意味着如果两个字符串长度不同,循环会在较短的那个字符串结束时停下来,避免越界访问。
Python没有'\0'的概念,字符串长度由len()获得。所以循环边界用range(min(len(s1), len(s2))),只在两个字符串都有字符的范围内比较。这个差异在逻辑上是等价的,但初学Python的人在写时容易忽略min,导致索引越界。
还有一个小细节:C语言版本里,规则三的输出用了"%02d\n",后面带换行;规则一和规则二末尾分别用了空格和冒号,最后在三段输出之间恰好拼成“THU 14:04”。Python版本也是同样的思路:规则一结尾用end=' ',规则二结尾用end=':',规则三默认换行。这种“分段拼接”的方式比用一个printf打印完整字符串要更直观,也更好debug——每一段都是独立的逻辑,中间哪一步错了,输出一眼就能看出来。
4. 这四个坑,我刷题时逐一踩过
4.1 坑一:第二对小时字符没有从第一对后面开始找
我第一次写这题的时候,规则一和规则二用了两个完全独立的循环,规则二的循环从i = 0开始。结果样例全过,提交上去却全是错。
为什么样例能过?因为示例里第一对字符是索引6的D,如果从头开始找,第二对会找到索引2的8和8——但等等,8不在0-9或A-N的合法范围内吗?8确实在0-9范围内啊!那按从头找的逻辑,索引2的8就会被认为是小时字符,输出08:xx,然后后面的xx又会被分钟逻辑覆盖掉一部分,最终拼接出来一串完全错误的结果。
问题就出在“第2对”这个描述上。题目说的是“第2对相同的字符”,隐含了“在第一对之后”的位置关系。所以规则二的循环起点必须是规则一找到的位置加1。这里有一个更隐蔽的陷阱:如果从头重新找,而且第一对之后没有合法字符了,你的程序可能找到一对完全错误的字符,却因为看起来“合理”而无法被注意到。
我后来调试时把两个字符串打印出来,逐位标索引,才意识到for (i = 0; ...)和for (i++; ...)这两个写法的差别有多大。这个坑之所以普遍,是因为很多初学者习惯把每一步都写成独立循环,而忽略了题目里“连续扫描”的隐含要求。
4.2 坑二:找到第一对相同字符但范围不合法,直接break
这个坑比坑一更隐蔽。
假设s1和s2在索引5处有一对相同的'H',恰好也是大写字母,但它不在A-G范围内。如果代码写成:
for (i = 0; s1[i] && s2[i]; i++) { if (s1[i] == s2[i] && s1[i] >= 'A' && s1[i] <= 'Z') { // 只判断了大写,没限定A-G printf("%s ", week[s1[i] - 'A']); // 数组越界或者输出错误 break; } }那'H' - 'A'等于7,week[7]已经越界了。就算不越界,输出的也绝对是错误答案。
正确的做法是只在'A'到'G'范围内才break,不在这个范围内的相同字符必须跳过,继续往后找。这个逻辑在规则二里同样重要:示例中索引13的G和g相同,但因为G不在0-9或A-N范围内,必须忽略。
这类“跳过但不终止”的逻辑,是模拟题里最容易出错的地方。很多同学的代码写成了“找到相同字符就停下来判断”,结果遇到一个相同但范围不合法的字符,代码break了,后面的数据全被忽略。
4.3 坑三:分钟索引数成从1开始
分钟部分的规则是“用索引位置作为分钟数”,索引从0开始。但人的直觉总是习惯从1开始数。
示例中,s3和s4的第一对相同英文字母出现在字符串的第5个字符位置,但索引是4。如果你在代码里写printf("%02d\n", j + 1),输出就会是05而不是04,整个时间就错了。
这个坑的迷惑性在于:不是所有测试点都会暴露它。如果某组测试数据中,匹配到的英文字母恰好出现在索引0,那j+1输出01,j输出00,一眼就能看出区别;但如果匹配位置在索引1,j+1输出02,看起来也像是合理的分钟数,极难排查。
我当时是拿示例数据验证的,示例匹配在索引4,输出04看起来没问题,结果j+1把04变05,样例直接不过才发现的。如果你用j+1这种写法,样例就过不了;就怕有的人样例碰巧过了,提交后却因为别的测试点挂掉,那种情况才叫真的难查。
4.4 坑四:小时和分钟的补零格式
PAT的输出格式要求非常严格,“THU 14:04”中间有一个空格,小时和分钟都是两位,不足补0。少了补零,或者多了空格,都会导致Presentation Error,判为格式错误。
这个问题看似简单,但恰恰是初学者最容易忽略的。小时部分如果你用%d:输出14:,没问题;但如果小时是9,%d会输出9:而不是09:,格式错误。分钟部分同理,如果分钟是4,必须输出04而不是4。
我在C语言里统一用%02d处理整数输出,在Python里用f-string的:02d格式,这样就不会漏掉补零。如果你在代码里手写if (hour < 10) printf("0%d", hour);这样的逻辑,也可以,但代码会显得冗长,而且容易在某种分支下漏掉。
4.5 避坑清单:一张表看清所有易错点
| 易错点 | 错误写法 | 正确写法 | 原因 |
|---|---|---|---|
| 规则一范围 | 只判断>= 'A' | 必须同时判断<= 'G' | 只有A-G对应星期一到星期日 |
| 规则二起点 | for (i = 0; ...) | for (i++; ...) | 必须从第一对之后继续扫描 |
| 规则二范围 | 遇到相同就break | 只在0-9或A-N内break | 超出范围的字符必须跳过 |
| 分钟索引 | j + 1 | j | 索引从0开始计数 |
| 输出补零 | %d: | %02d: | 小时和分钟都要两位,不足补0 |
| 数组长度 | char s1[60] | char s1[61] | 需要为\0留一个位置 |
这张表建议刷题前扫一眼,写完代码再对照检查一遍,基本能把1014这题的所有失分点全部堵上。
5. 从福尔摩斯这道题看PAT乙级字符串类题目的通用解法
5.1 字符串配对题的通用三步法
刷多了PAT乙级会发现,像1014这种“字符串配对”题目其实有一条通用的解题路径,可以总结成三步:
第一步,明确扫描区间。题目让你在哪两个字符串之间比较?是一次扫描还是分段扫描?比如1014里,前两行用来推星期和小时,后两行用来推分钟,这就是两个独立的扫描区间。而在前两行的扫描内部,星期和小时的查找又是连续扫描、分两段完成的。把扫描区间画出来,代码结构就会清晰很多。
第二步,明确合法字符范围。题面里出现的每一个“相同字符”都可能有范围限制,例如“A-G的大写字母”“0-9或A-N的字符”“英文字母”。这一步必须在代码里写成显式的判断条件,不能想当然地认为“相同就合法”。我见过太多错误都是在“相同字符但范围不合法”的情况下发生的。
第三步,建立映射关系。字符到数值的映射通常有两种:一种是查表,比如星期的week[]数组;另一种是算术换算,比如'E' - 'A' + 10 = 14。查表直观,算术换算简洁,两种都可以,但一定要保证映射表的边界和题目定义完全一致。
这三步做完,代码的正确性基本就有保障了。剩下的就是格式问题,输出前反复读一遍题目里的“输出格式”,把空格、补零、换行都检查清楚。
5.2 字符串比较时的边界控制技巧
具体到C语言,字符串比较的边界控制有几个通用技巧,我在刷PAT乙级时反复用到:
第一个技巧:用'\0'作为循环终止条件。比如for (i = 0; s1[i] != '\0' && s2[i] != '\0'; i++),当任一字符串结束时循环自动终止,不会越界。这比先算strlen再用索引访问要安全得多,也快得多。
第二个技巧:把字符范围判断写成区间连比。s1[i] >= 'A' && s1[i] <= 'G'这种写法可读性很好,而且不容易漏掉边界。注意不要写成'A' <= s1[i] <= 'G'——这在数学上成立,但在C语言里是另外一层含义,会先算'A' <= s1[i]的结果(0或1),再拿这个结果去和'G'比较,逻辑完全变了。
第三个技巧:一旦确定break条件,循环外面就不要依赖i的值做额外假设。比如规则二里,你必须在规则一的循环里同时保存好i的位置,然后靠i++继续。如果规则一的循环因为某种原因没有break(虽然题目保证有唯一解,理论上不会发生),那i会停留在最后一个索引,规则二的起点就会错。防御性编程可以加一个flag标记是否找到了第一对,但PAT题目不会触发这种异常情况,所以不加也能过。
5.3 刷PAT乙级的经验之谈
说到PAT乙级怎么刷,我的个人体会是:不要只刷题,要把每一道题拆成“考点+坑点”两条线。
考点是题目想考察的知识点,比如1014考的是字符串配对、范围限制、映射。坑点是那些描述里容易忽略、或者实现时容易写错的地方。每做完一道题,我会在代码注释里专门标出这道题的坑点,下一次复习时直接看注释比重新读题快得多。
另外,翁恺老师在MOOC上的C语言课程里专门有一章讲字符串处理,配合PAT乙级题目练习效果很好。很多同学刷PAT都是从翁恺老师的课程开始接触这个平台的,课程里的练习题和PAT题库有部分重叠,可以互相巩固。如果你也是通过课程入门的,建议按“数组、字符串、模拟、排序、简单数学”这几个专题顺序刷乙级,而不是从头到尾按题号刷——后者容易在一个类型上反复栽跟头,效率不高。
5.4 还可以往哪个方向扩展
1014这道题本身是一道纯模拟题,但它涉及的字符串处理技巧可以延伸到很多其他题目。比如:
- 把多行字符串按统一规则逐字符扫描,在PAT甲级的一些题目里也会出现,只是规则更复杂。
- 字符到数值的映射方式,在处理十六进制、进制转换、罗马数字这类题目时非常常见。
- 两个等长字符串的同步比较,在DNA序列匹配、文本查重、简单的编辑距离模拟题里也能看到影子。
所以别小看这道看起来像“脑筋急转弯”的题。你在这道题上养成的边界控制习惯,后面刷几十道字符串题都会受益。我到现在写字符串相关题目时,还会下意识地先画一遍扫描区间、列一遍合法范围,再动手写循环——这个习惯就是从1014开始养成的。