PAT基础级L1-070这道吃火锅题当年第一次在考场里看到题面我是先愣了一下的。吃火锅也能出成编程题等我把题面读完才反应过来这其实是一道非常典型的字符串查找题——给你一段社交平台文本统计多少条消息里包含了“chi1 huo3 guo1”这串经过数字和空格“加密”的吃火锅拼音并输出第一条匹配消息的编号。整道题15分代码量不大但输入处理的细节没拿捏好照样会丢分。这篇文章不是要把一道15分小题讲出花来而是借着这道题把PAT基础题背后的读题方法、多行输入处理、子串查找的基本操作以及我在实际答题和带人刷题时反复踩过的坑一起捋一遍。如果你正准备考PAT或者刚学编程还不知道怎么处理多行文本输入这篇能帮你把这类“文本统计型”题目的套路摸清楚如果你是刷题老手也可以直接跳到第四节那些排查经验是真的容易翻车。1. 题目还原与核心考点拆解1.1 原题场景到底长什么样题目本身包装得很生活化某火锅店老板想知道朋友圈里有多少条消息提到了“吃火锅”但社交平台上刷到的消息并不是直接打出汉字而是一串看起来很奇怪的“chi1 huo3 guo1”。你需要从标准输入一段文本每行是一条消息文本的结尾是单独成行的一个英文句点“.”。最后输出两行第一行是包含关键词“chi1 huo3 guo1”的消息总数如果总数大于0第二行输出第一条包含该关键词的消息编号编号从1开始如果总数等于0第二行输出题面给定的特殊占位符“-_#”。很多初学者第一眼看到这个题会犯一个方向性错误光盯着“火锅”两个字看以为要找的是中文“火锅”或者“chi1 huo3 guo1”里面的某个词。实际上关键词是整串字符中间的数字1、空格、字母一个都不能少。也就是说你要统计的不是“哪条消息在说吃火锅”而是“哪条消息完整包含这串固定字符串”。这其实是一个精确子串查找问题不是分词问题更不是语义判断问题。理清这一点整个题目就成功了一半。还有一个读题时需要捕捉的信息点结尾的“.”这一行是结束标志本身不参与统计也不占行号。这个规则如果不仔细看后面的边界处理会很别扭。PAT题库特别偏爱这类“用单独一行作为输入结束标志”的设定所以一旦题面出现类似描述直接建立条件反射读入一行先判断是不是结束符再决定要不要处理。1.2 15分到底考了哪些能力这道题只有15分在PAT基础级里属于“送分题”阵营。但送分题不代表没有陷阱它考察的能力点其实非常集中多行文本的读取能力。很多新手只会用 cin 或者 input() 读固定数量的输入遇到“读若干行直到某一行结束”这种非固定行数输入就直接懵了。字符串精确匹配能力。知道用 find() 或 in 运算符去找子串而不是自己写两层循环傻乎乎地比对。状态维护能力。用两个变量分别记录“总数量”和“第一次出现的位置”这是很多简单题的核心逻辑。条件输出能力。根据总数是否为0切换不同的输出格式属于最基本的控制流。这四点没有一个涉及复杂算法但任何一点做不到位都会导致拿不满分。从分值结构来看出题人给的15分里读入和输出格式大约占一半真正的匹配逻辑只占另一半。我经常跟刷题的朋友说PAT基础级考的不是你会不会KMP而是你能不能把一件简单的事情做严谨。吃火锅就是最标准的例子。2. 实现思路从读题到动手的推理过程2.1 先确认要维护的数据拿到题目不要急着写代码先在草稿纸上把状态变量列出来。这道题需要维护三个东西当前是第几行用一个行号计数器 idx初始为0。已经匹配到的总数量 cnt初始为0。第一条匹配消息的编号 first初始可以设为-1因为行号从1开始-1表示“还没找到过”。为什么 first 要用 -1 而不是0因为如果第一条有效消息正好是第1行那 first 的值就是1不是0。用0作为初始值的话如果第一行就匹配你没法区分“找到的第一条是第0行”和“还没找到”。用-1做哨兵值是最稳妥的。这个小设计在竞赛代码里非常常见也建议新手养成用哨兵值标记“尚未出现”的习惯。cnt 和 first 不是同一个东西cnt 需要累加所有匹配的行first 只在第一次匹配时被赋值。有人会想我匹配到第一条之后直接输出行号不就行了不行因为你必须等输入全部结束、统计完总数以后才能统一输出两行结果所以不能提前输出只能把第一条的行号暂存到变量里。2.2 逐行读入和终止判断的细节这道题输入的行数不固定每一行还可能包含空格所以整行读入是唯一正确的姿势。C里用 getline(cin, s)Python里用 sys.stdin 逐行遍历Java里用 BufferedReader.readLine()。千万不要用 cin s 或者 scanf(%s)因为它们是按空白符切分的一行里有空格就会被拆成多个输入段整个行号体系瞬间崩掉。终止判断的代码位置也有讲究。正确流程是读入一行先判断这一行是不是单独一个“.”如果是就直接 break不再执行后续的行号累加和匹配判断如果不是再进行 idx、字符串查找这些操作。这个先后顺序看起来微不足道却是很多人丢分的第一个坑。如果先把 idx 放在判断之前结束行本身也会占一个行号后续所有行编号都会偏大1输出结果自然错位。Python 里有一个额外细节sys.stdin 读到的字符串末尾是带着换行符 \n 的。所以判断结束符时不能直接写 line .要先 line.rstrip(\n) 再比较或者用 line.strip() 也可以。如果忘了去换行符你就永远等不到那行句号程序会一直读到 EOF 为止。2.3 什么时候用 find什么时候用正则字符串匹配说起来简单但不同场景要用的工具不一样。本题的关键词是一段固定字符串没有任何通配符和可变部分用标准库自带的子串查找函数就足够了。C 里是 s.find(chi1 huo3 guo1) ! string::nposPython 里是 chi1 huo3 guo1 in lineJava 里是 line.contains(...)Go 里是 strings.Contains(line, ...)。千万不要一上来就想着用正则表达式。有些同学学了正则以后看到“文本中找特定模式”就觉得应该用正则但在竞赛环境里正则引擎的构建和匹配开销比普通 find 慢得多而且还需要处理转义、分隔符等问题完全没必要。正则适合的是一类更复杂的需求比如“匹配所有形如 chi 后跟一个数字的模式”或者“匹配位于句子边界的完整单词”这类带规则的匹配才轮到正则上场。把简单问题用简单工具解决是竞赛和工程里共通的判断力。3. 代码实现与逐行解析3.1 一个标准且简洁的 C 版本下面这段是我在考场上比较推荐的写法短小直接不容易出错。#include iostream #include string using namespace std; int main() { string s; int cnt 0; int first -1; int idx 0; while (getline(cin, s)) { if (s .) break; // 结束行直接退出 idx; // 当前行的真实编号 if (s.find(chi1 huo3 guo1) ! string::npos) { cnt; if (first -1) first idx; // 只记录第一次出现 } } cout cnt \n; if (cnt 0) { cout -_# \n; } else { cout first \n; } return 0; }逐行看getline 负责整行读取包括行内的空格循环体一开始就检查是不是结束行idx 只在有效消息行上递增find 返回值不等于 npos 就说明找到了first 用 -1 做哨兵保证只记录第一次出现的位置。输出部分先输出总数再根据总数是否为0决定输出占位符还是行号。整个逻辑闭环没有任何多余操作。这里提一个关于 getline 的细节getline 读取成功会返回流对象本身所以 while 循环条件里的 getline(cin, s) 会在文件结束或者读取失败时自动变成 false退出循环。这意味着如果输入没有那个“.”结束行程序也能在读到 EOF 后正常结束。在 PAT 环境下这个行为是安全的不用担心死循环。3.2 Python 实现的简洁优势Python 版本的代码更短但读入和换行符处理需要多留个心眼。import sys cnt 0 first -1 idx 0 for line in sys.stdin: line line.rstrip(\n) if line .: break idx 1 if chi1 huo3 guo1 in line: cnt 1 if first -1: first idx print(cnt) if cnt 0: print(-_#) else: print(first)很多人第一次写 Python 版时读入用的是 input()。input() 每次只读一行倒也够用而且会自动去掉末尾换行符。但如果输入量比较大sys.stdin 的遍历效率更高也更贴近“持续读入直到结束”这一语义。用 for line in sys.stdin 的时候line 末尾有换行符rstrip(\n) 把它去掉这样判断结束行才能成立。有人图省事直接写 line.strip() .也能用但如果某条文本行首尾本身有空格strip 会把有效空格也删掉影响后续匹配判断。虽然本题关键词不会出现在行首行尾的额外空格里但严谨一点还是只删换行符为好。3.3 Java 和 Go 的实现思路用 Java 写的话核心是 BufferedReader 和 readLine。readLine 返回的字符串同样不带换行符判断结束行时直接比较即可。匹配用 String 的 contains 方法。代码骨架大致是BufferedReader br new BufferedReader(new InputStreamReader(System.in)); String line; int idx 0, cnt 0, first -1; while ((line br.readLine()) ! null) { if (line.equals(.)) break; idx; if (line.contains(chi1 huo3 guo1)) { cnt; if (first -1) first idx; } }Go 版本用 bufio.Scanner逐行 Scan然后 Text() 拿到当前行剩余变量逻辑完全一致。到这里你应该能发现这道题的语言差异只体现在读入 API 的名称上核心的算法思路是跨语言通用的。这就是我经常说的刷题练的是思路语言只是表达思路的工具。吃火锅这种题只要思路正确换任何一门主流语言都能在几分钟内写完。4. 实操心得我在这个题上踩过的坑4.1 编号差一的惨案先说这个最容易踩的坑行号为什么差了1。错误代码通常是这样的while (getline(cin, s)) { idx; if (s .) break; // 后续匹配 }表面看起来没问题先给行号加1再判断结束行但问题是“.”这一行也被算成了有效消息。假设输入总共有5行第5行是“.”那么循环结束时 idx 的最大值是5。如果第一条匹配的消息出现在第1行输出倒是正常但如果第一条匹配出现在最后一行之前输出的编号就会比真实编号大1。更隐蔽的是如果“.”这一行前面恰好有一条消息匹配编号错误直接导致测试点WA而且很难肉眼察觉。正确做法就是把 idx 放到结束判断之后。这个顺序问题在上机考试中特别容易犯因为人紧张的时候会下意识按“读一行计一行”的惯性来写。我建议在练习时就刻意养成固定顺序先判断后计数。4.2 特殊输出“-_#”的严格性当总数等于0时第二行要输出“-#”这个字符串第一次看到的人多少会愣一下——它长得像一个颜文字。恰恰是因为它太像表情符号了很多人在考场上一紧张就打成“#-”或者“-#”。PAT 的评测器对输出是逐字符严格比对的多一个空格、少一个下划线、顺序不对全部判错。怎么避免我的经验是把这类固定输出字符串单独从代码逻辑里抽出来写成独立的常量或者直接放在输出语句里不要靠记忆临时敲。交卷前再花十秒钟把输出格式的字符逐字核对一遍。这种特殊输出在PAT基础题里出现频率不低比如某些题要求输出“N/A”或者“-1”越是看起来像乱码的越是要警惕。4.3 空行到底算不算一行输入数据里可能出现空行也就是只有换行符、没有任何可见字符的行。这条空行算不算一条消息答案是要算的。它占一个行号只是不包含关键词不影响 cnt 和 first。如果用 getline 或 readLine 读入空行会被正常读取行号累计也对但如果你用了 cin s 或者 Scanner 的 next() 这类按空白符分隔的读入方式空行会被直接跳过行号整个错乱。这也是我反复强调整行读入的原因。顺便说一个希望能“骗过测试数据”的误区有人以为空行可以忽略理由是题面说“每行是一条消息”空行不是消息。但评测数据的生成往往不会考虑这么细致它会原样构造文本空行就是文本的一部分。按“所有非结束行都参与行号累计”的规则处理一定是对的。4.4 关键词内部的空格不能乱动“chi1 huo3 guo1”看起来像是被空格分成了三段于是有同学想用 split 把每行按空格切开然后逐个单词比较。这个思路能走通但非常麻烦你不得不处理连续多个空格、行首行尾空格、单词与单词之间多余空格等情况。而且如果关键词中间的某个空格在原始文本里被替换成了其他空白符split 方案还要做额外归一化。直接整串 find 的好处是它只关心这串字符是不是连续出现在这一行中其他什么都不管。不管这一行开头有没有空格不管关键词两边是什么符号只要连续出现“chi1 huo3 guo1”这14个字符含空格和数字就是一个有效匹配。这就是精确子串匹配的威力。工程上经常有人说“能用现成库函数解决的就不要自己造轮子”竞赛也是一样。4.5 数据规模和运行时间原题中每行消息长度不超过80个字符行数虽然可能不少但整体规模并不大。字符串 find 在最坏情况下的时间复杂度是 O(n*m)其中 n 是文本长度m 是关键词长度。对于80个字符的行和14个字符的关键词这个开销几乎可以忽略。所以这道题完全没有必要上 KMP 或 Sunday 之类的优化算法那属于杀鸡用牛刀。不过这个点倒是可以顺便延伸一下如果有一天你需要在上万行日志里快速统计大量关键词的出现位置那 find 就真不够用了更合理的选择是前缀树加 AC 自动机把模式匹配的时间复杂度压到文本长度的线性级别。从15分的吃火锅到工业级的关键词过滤模型是同一个只是规模变了。这也是我为什么说基础题有基础题的价值它把简单场景下的逻辑讲透了复杂场景只是在这个逻辑上做性能和扩展性的升级。5. 从这道基础题延伸开PAT L1 的套路与策略5.1 L1部分到底在考察什么PAT 的基础级 L1 题目整体难度都不高考的不是算法智力而是“准确理解题意 严谨处理边界 熟练使用基础库”这三件事。题面往往用生活化故事包装一个简单操作吃火锅是典型例子类似的还有输出 GPLT、6翻了、胎压监测等等。这些题目有一个共同的出题偏好不直接告诉你“统计包含某子串的行”而是包装成火锅店、朋友圈、传感器这些场景。你要做的第一步永远是剥掉包装找到真正的操作对象。有时候光这一点就能淘汰一批人——不是不会写代码而是读不懂题。我的建议是拿到题先圈出三个东西输入是什么、要算的是什么、输出格式是什么。把这三个问题的答案写在草稿纸上再动笔准确率会高很多。5.2 考试时间分配和拿分策略PAT 考试按测试点给分不是按AC先后排名所以最划算的策略永远是先把能拿的分稳稳装进口袋。像吃火锅这种15分基础题读题加写代码加调试正常水平10分钟内应该完成。如果一道简单题卡了20分钟还没过赶紧换下一题回头再来看避免在送分题上透支时间。我个人的做题顺序是先把所有题目通读一遍标记出明显简单的题优先做掉再做中档题最后剩余时间攻难题的部分测试点。这个顺序能保证你在考试结束前手里已经捏着大量确定的分后面做难题时心态也会稳很多。别小看心态实际考试里因为一道题卡住导致后面简单题都没写的人大有人在。5.3 这类题在真实开发中的影子吃火锅这道题的模型放到真实开发里对应场景可太多了。运营后台统计包含特定关键词的评论条数日志系统扫描某一错误码第一次出现的位置数据清洗时定位异常样本首次出现在哪一行内容安全系统过滤敏感词并报告命中数量……这些需求本质上都是“在文本流中查找固定子串并维护统计状态”。所以别因为题目简单就觉得刷了没用。写业务代码和写竞赛代码的区别只在于工程约束更多、数据量更大但核心的“扫描文本、判断匹配、维护状态”能力是一脉相承的。我后来带团队做日志告警功能时第一个版本的核心循环就跟这道题的思路一模一样逐行读、判断关键字、记录第一次命中位置和总次数。能把自己的竞赛直觉迁移到工程实践上这才算是真正刷懂了这类题。最后分享一点个人习惯我现在带新人入门刷题还经常拿这道吃火锅当第一道练习。原因很简单它够小、够具体能把多行输入、字符串查找、计数、特殊输出这些最基础的操作全部串起来又不会让零基础的人一上来就崩溃。最后再分享一个小技巧刷 PAT 基础题一定要自己手动构造边界数据去测。比如空行出现在中间、结束行前一行正好有匹配、第一行就匹配、全部不匹配、匹配行紧挨着连续出现——把这几种情况跑一遍吃火锅这种题基本就是稳的。边界数据测出来的问题往往比主流程跑出来的问题值钱得多。