我备考PAT乙级的时候,有一道题让我印象特别深——1014. 福尔摩斯的约会。这道题在牛客网和PAT官网上都挂着“20分”的牌子,看起来人畜无害,实际上暗坑不少,字符串处理稍微马虎一点,轻则超时找不到错,重则整个逻辑绕进去出不来。今天我就把这道PAT乙级真题从头到尾拆一遍,不仅讲怎么做对,还把做题时的思路断点和常见错法一并拿出来遛遛,给正在刷题的朋友做个参考。
这道题的核心是字符串匹配和时间格式转化,考点集中在字符比较、ASCII码范围判断和输出格式控制。适合刚接触PAT乙级、正在刷字符串类型题目的考生,也适合那些做题时总在细节上翻车的老手拿来查漏补缺。我会先讲清楚题目的逻辑结构,再给出一份可直接复现的C语言代码,最后把我实际调错过程中遇到的高频问题整理成速查表。
1. 题目分析与考点拆解
1.1 这道题到底在考什么
很多第一次看到“福尔摩斯的约会”这个题名的人会以为是一道推理题,实际上它跟福尔摩斯半点关系都没有,纯粹是借用了一个故事背景。题目给了四行字符串,前两行推日期,后两行推具体时间,最后输出一个类似THU 14:04的结果。听起来很简单,但真正的考点并不在于遍历字符串本身,而在于多个匹配条件的叠加与顺序控制。
从PAT乙级的出题风格来看,这种题属于“模拟题”大类,核心考察三个能力:
- 遍历字符串时能不能准确区分“第一个匹配”和“第二个匹配”
- 字符范围判断是否严谨(比如星期几只认A到G,小时只认0到9和A到N)
- 格式化输出是否规范(小时和分钟必须补零到两位)
第二个值得说的点是,这道题非常爱在“顺序”上做文章。题目要求先找第一对相同的大写字母,再继续找第二对相同的字符,而不是从开头同时找两对。这就意味着你需要用状态机式的思路去遍历:用一个标志位记住当前已经找到了星期几,再去处理小时;或者干脆用两个独立的循环分阶段完成。
1.2 为什么PAT乙级爱出这种题
刷过PAT乙级的人应该都能感觉到,字符串处理类题目占比很高。原因是这类题不需要复杂的算法基础,但对细节的考察非常到位。比如这道题里的“第二对字符”如果不加注意,很容易把第一次遇到的相同字符当作小时,然后输出一个完全错误的时间,而编译器还不会报错——它只会让你的答案在OJ上显示一片红。
另外,这道题的难度设置也有讲究。20分的题在PAT乙级中属于中等偏易的档次,但通过率并不是特别高。主要原因就是“看起来简单,实际容易漏条件”。只要输出格式少了一个零,或者把分钟数算错一位,整道题就白做了。这种题放在真实考场环境中,其实是在考抗压能力和细致程度。
我个人觉得,做这种题最好的策略不是急着写代码,而是先把题目里的每条规则在草稿纸上翻译成逻辑判断,然后再动手。很多人一上来就写循环,结果越写越乱。
2. 规则逐字拆解:把题目翻译成代码逻辑
2.1 星期几的判定细节
题目给的前两行字符串,需要找到第一对相同的大写英文字母,而且这个字母必须在A到G之间,分别对应星期一到星期日。这里有几个容易踩的点。
第一个点是“第一对相同”。注意是“对”,也就是说两个字符串在同一个位置上的字符要相同,而不是在一个字符串里找重复字符。我见过有人理解成在一个字符串中找第一个重复字母,在另一个字符串中找对应关系,那是完全不对的。
第二个点是范围限制A到G。如果没有这个限制,那么第一对相同的任意大写字母都行,但题目明确要求A到G。所以在遍历时必须加上str1[i] >= 'A' && str1[i] <= 'G'这样的判断。有些实现不加这个限制,直接用if (isupper(str1[i]) && str1[i] == str2[i]),这样当字符串前面恰好有一对相同的大写字母但不在A到G范围内时,就会提前跳出循环,导致答案错误。
这里我当时的处理方式是单独写一个函数做映射,把字符转成数字再转成星期字符串。比如'A'对应MON,'B'对应TUE,依次类推到'G'对应SUN。
2.2 小时判定的顺序陷阱
找到星期几之后,注意是“继续”在同一对字符串中往后找,而不是重新从开头找。所以这里需要一个状态切换:用一个flag变量记录是否已经找到了星期的字符,一旦找到,后续再遇到的符合小时条件的字符才会被当作小时来解析。
小时字符的范围是0到9和A到N。0到9映射到0点到9点,A到N映射到10点到23点。注意这里没有O到Z,所以判断条件要限制好。另外还有一个隐藏规则:在找小时字符时,不再要求是大写字母,因为数字字符也是合法的。这一点很多教程没有强调,但实际编码时容易漏掉,导致把数字字符跳过,输出错误。
下面是我当时梳理的顺序逻辑:
- 遍历前两行字符串,逐位比较
- 如果还没有找到星期字符,且当前两位字符相同且是大写字母且在
A-G范围,则记录星期并标记已找到 - 如果已经找到星期字符,继续往后比较,遇到第一个相同且满足小时字符范围的字符,则记录小时并停止这一个阶段的遍历
这样就可以避免用复杂的状态机写法,普通循环加一个布尔标记就能搞定。
2.3 分钟判定的隐藏条件
分钟部分由后两行字符串决定,找的是第一对相同的英文字母(不区分大小写),然后取它们在字符串中的位置(从0开始计数)作为分钟数。
这里的隐藏条件有三个:
- 必须是英文字母,数字、空格、符号都不算
- 字母不区分大小写,但判定时需要用
isalpha()函数或者手动判断a-z和A-Z - 位置是从0开始计数,所以如果第一对相同字母出现在下标4的位置,那分钟数就是
04,输出时要补零
还要注意,分钟部分是从后两行字符串的开头开始找,不是接着前两行的位置继续。这道题的规则设计很容易让人惯性思维地往后延续,但实际上前两行和后两行是两组完全独立的比较对象。
3. 完整解题实现与逐步讲解
3.1 C语言参考代码
我把自己的实现贴出来。这段代码在PAT官网上测试过,可以直接AC。为了读起来方便,我把星期映射单独拎出来做数组,这样比switch-case更清爽。
#include <stdio.h> #include <string.h> #include <ctype.h> int main() { char week[7][4] = {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; char s1[70], s2[70], s3[70], s4[70]; scanf("%s", s1); scanf("%s", s2); scanf("%s", s3); scanf("%s", s4); int len1 = strlen(s1) < strlen(s2) ? strlen(s1) : strlen(s2); int len2 = strlen(s3) < strlen(s4) ? strlen(s3) : strlen(s4); int i, flag = 0; int day = 0, hour = 0, minute = 0; for (i = 0; i < len1; i++) { if (s1[i] == s2[i]) { if (!flag) { if (s1[i] >= 'A' && s1[i] <= 'G') { day = s1[i] - 'A'; flag = 1; } } else { if (s1[i] >= '0' && s1[i] <= '9') { hour = s1[i] - '0'; break; } else if (s1[i] >= 'A' && s1[i] <= 'N') { hour = s1[i] - 'A' + 10; break; } } } } for (i = 0; i < len2; i++) { if (s3[i] == s4[i] && isalpha(s3[i])) { minute = i; break; } } printf("%s %02d:%02d\n", week[day], hour, minute); return 0; }注意我用了scanf("%s")来读字符串,这里有一个前提:题目给的每行字符串不包含空格。如果一行里面有空格,就必须改用gets或fgets。我在PAT官网和牛客网都提交过,这两处题目的数据都没有空格,所以scanf足够用。但你如果是在其他OJ或自己造测试数据,就要留意这一点。
3.2 关键代码行深度讲解
if (!flag)这个逻辑是整个代码的灵魂所在。第一次进入且字符满足A-G时,flag置1;后续循环走到相同字符时,由于flag已经是1,就进入else分支去解析小时。这里很多人写错,是因为他们用了一个continue语句想跳过,结果跳过了本轮,下一轮又回到了判断星期的分支,导致状态错乱。
小时判断的两个分支顺序也很讲究。我先把数字字符放在前面判断,再把大写字母放在后面判断。为什么这样写?因为数字字符的范围和小写字母的范围没有交集,先判断哪个都行,但如果有人用if (s1[i] >= '0' && s1[i] <= 'N')这种写法就会出错——'9'的ASCII码是57,'A'是65,中间还隔着一些字符,但'9' < 'A',所以数字字符到'9'就结束了,不会影响大写字母的判断。不过为了逻辑清晰,还是分开写比较好。
3.3 如何用其他语言实现类似逻辑
如果你用的是Java,思路完全一样,只是把字符串读取方式换成Scanner.nextLine(),把字符判断方法换成Character.isLetter()和Character.isUpperCase()。Python就更简单了,因为有内置的isalpha()和isupper()方法,甚至可以用ord()函数直接拿到ASCII码。不过Python有些实现容易掉进一个坑:字符串是不可变的,不能用下标直接修改,但我们的需求只是读取和比较,所以没问题。
下面是一个Python风格的参考片段,方便做横向对比:
week = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"] s1 = input().strip() s2 = input().strip() s3 = input().strip() s4 = input().strip() day = "" hour = "" flag = False for a, b in zip(s1, s2): if not flag: if a == b and 'A' <= a <= 'G': day = week[ord(a) - ord('A')] flag = True else: if a == b and ('0' <= a <= '9'): hour = "0" + a break elif a == b and ('A' <= a <= 'N'): hour = str(ord(a) - ord('A') + 10) break minute = 0 for i, (a, b) in enumerate(zip(s3, s4)): if a == b and a.isalpha(): minute = i break print(f"{day} {hour}:{minute:02d}")有一点要注意,Python中的zip会以较短的字符串长度为界,所以不需要手动取min(len1, len2),这算是Python写法的一个小便利。但如果你是C语言思路转过来的,建议还是显式计算长度,这样不容易弄混边界。
4. 调试实录:我在提交时踩过的坑
4.1 常见错误汇总速查表
我把自己实际提交中遇到的高频错误整理成了表格,这些错误在牛客网的讨论区和CSDN博客里也非常常见。如果你在OJ上反复提交不通过,可以对照这个表逐一排查。
| 错误现象 | 根本原因 | 解决方式 |
|---|---|---|
| 输出结果中星期错误 | 没有限制字母范围在A到G,把其他大写字母当成星期 | 添加s1[i] >= 'A' && s1[i] <= 'G'判断 |
| 小时结果总是10点以后 | 忘记了数字字符0-9也是合法小时字符 | 小时判断时增加'0'到'9'的映射 |
| 分钟结果总是0 | 没有判断字符必须是英文字母,匹配到了数字或空格 | 分钟判断时使用isalpha()函数或手动判断字母范围 |
| 输出小时和分钟只有一位数 | 格式化输出使用了%d而非%02d | 使用printf("%02d:%02d") |
| 读取字符串时出现乱码或超时 | 输入行中包含空格时误用scanf("%s") | 改用fgets()或getline()读取一整行 |
| 小时或分钟出现重复覆盖 | 找到第一个匹配后没有及时break退出循环 | 在hour匹配成功后立即break |
这张表值得你在提交前从头到尾过一遍。我敢说,至少有一半人第一次AC不了,就是因为表格里的某一行。
4.2 两个容易忽视的逻辑盲区
第一个盲区是“找小时字符时是否可以从已经匹配过星期的那一位开始继续”。答案是可以,因为当flag变成1以后,循环仍然从当前i的下一位继续执行,并不会重新从头扫描。但如果你的实现是在找到星期后把索引重置为0,那就会重复匹配前面对字符,导致小时判断错乱。所以这里要特别注意:千万不要重置索引。
第二个盲区是“分钟的位置下标从0开始还是从1开始”。题目明确是从0开始,但很多人在手算样例时习惯从1数,导致结果差一位。比如样例中如果第一对相同字母出现在下标4,那么分钟应该是04而不是05。这个只能靠细心,没有别的技巧。
另外还有一个做题习惯层面的问题:在本地编译器上跑通并不代表OJ上一定正确。因为有些编译器对未初始化的变量会给默认值0,而OJ上可能给随机值。我早期写代码时,day、hour、minute有时候会忘记初始化,本地运行恰好不报错,提交上去却出现异常结果。所以无论使用哪个变量,都建议先赋初值。
5. 从真题出发:这类题的通用解法与训练建议
这道题做明白之后,其实可以总结出一套处理PAT乙级字符串模拟题的通用套路。我以前认为刷题就是刷题,刷多了才发现,类似题目的解法骨架高度一致,无非是“遍历 + 条件判断 + 格式化输出”。
这类题目的通用步骤可以归纳为:
- 明确要匹配的字符范围和出现顺序
- 用布尔标记或状态变量区分不同匹配阶段
- 在匹配成功后立刻跳出或进入下一阶段,避免覆盖
- 注意输出格式中的前导零、大小写等细节
- 用边界数据自测,包括最小、最大、以及字符范围边缘
如果把这个套路应用到同类题目上,比如“旧键盘”“人口普查”“查验身份证”等,你会发现很多题的解法是相通的。无非是旧键盘考集合查找,人口普查考日期字符串比较,而这道题考的是多阶段匹配。本质上都是对“边界条件”的敏感度考察。
所以我的建议是,做完这道题以后,不要急着往下刷,而是用它作为模板去练习同类字符串模拟题。在PAT乙级的题单里,这类题大约占了三分之一的比重。把这类题吃透了,你的分数下限就有了保障。
我在实际做这道题时,前后提交了4次才完全通过。第一次挂在星期范围没限制,第二次挂在小时映射漏了数字,第三次挂在分钟输出没有补零,第四次才真正AC。说实话,这种题目做错并不丢人,丢人的是反复在同一个细节上失分。希望这篇文章能帮你少走几步弯路,争取一把过。