
PAT乙级的题库刷到1014这道“福尔摩斯的约会”时我停了一下。不是因为这道题算法有多难而是它的题目描述实在是太像一道“脑筋急转弯”四行字符串长得跟乱码似的却要从中读出星期几、几点几分。当时我第一遍读题差点把规则看混对照样例推了两遍才真正搞清楚每一对字符的匹配边界。等你把这题吃透了会发现它覆盖了PAT乙级字符串题里最常见的三类坑范围限定、位置连续性、字符到数值的映射。这篇文章我会从题目规则开始一步步拆到完整代码再把刷题时踩过的坑逐个讲明白给正在准备PAT乙级真题的读者一份可以直接照抄的解题模板。1. 一道字符串处理题为什么值得逐字拆解1.1 题面回顾乱码背后的约会线索大侦探福尔摩斯接到一张奇怪的字条“我们约会吧3485djDkxh4hhGE 2984akDfkkkkggEdsb shgsfdk dHyscvnm”。大侦探很快就明白了字条上奇怪的乱码实际上就是约会的时间“星期四 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逐位比较索引03与2不同索引14与9不同索引28与8相同但8不是大写字母跳过索引35与4不同索引4d与a不同索引5j与k不同索引6D与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’映射关系如下字符小时字符小时000A10101B11202C12303D13404E14505F15606G16707H17808I18909J19K20L21M22N23为什么用‘A’-‘N’补足10到23因为24小时制里数值0到23一共24个取值数字字符0-9占了10个剩下的14个位置刚好用‘A’到‘N’这14个连续大写字母填充。这是一个很典型的PAT式设计用连续的字符区间映射连续的数值区间。继续看示例。第一对合法字符在索引6找到所以从索引7开始继续索引7k与f不同索引8x与k不同索引9h与k不同索引104与k不同索引11h与k不同索引12h与g不同索引13G与g相同但G不在合法范围内它大于N而且注意这里是一个大写G配一个小写g字符相同但一个是字母的情况并不影响比较结果关键是它不在0-9或A-N区间内必须忽略继续往后找索引14E与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 shgsfdks4 dHyscvnm逐位比较索引0s与d不同索引1与相同但不是英文字母跳过索引2h与H不同注意这里虽然都是字母且可以看作同一个字母的两种大小写但题目要求“完全相同”所以不算配对继续索引3g与y不同索引4s与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; } } // 规则二从 i1 继续找第二对相同的 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停在第一对字符的索引上规则二从i1继续正好符合“在第一对之后继续找”的要求。这里如果一不小心把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 # 规则二从 i1 继续找第二对相同的 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处breaki就是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等于7week[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那j1输出01j输出00一眼就能看出区别但如果匹配位置在索引1j1输出02看起来也像是合理的分钟数极难排查。我当时是拿示例数据验证的示例匹配在索引4输出04看起来没问题结果j1把04变05样例直接不过才发现的。如果你用j1这种写法样例就过不了就怕有的人样例碰巧过了提交后却因为别的测试点挂掉那种情况才叫真的难查。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 1j索引从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开始养成的。