PAT甲级真题解析:字符串处理、排序与哈希统计的工程化思维

2018年冬季的PAT甲级考试里,7-2这道“Decode Registration Card of PAT”让我印象挺深。它不是那种让人挠头的高深算法题,反而像一份送到你面前的“信息管理综合题”,把字符串处理、排序规则、哈希统计这三个基本功全串在一起。说实话,很多同学平时刷题刷惯了图论、动态规划,遇到这种题反而容易栽跟头——不是不会做,而是没想清楚“数据应该怎么组织”。

这篇就把我自己解这道题时的完整思路写出来,从准考证号的信息编码说起,到三种查询类型的实现方案,再到考场上容易翻车的细节,一步步拆开揉碎。如果你正在备战PAT甲级,或者想练一练C++处理字符串和哈希表的基本功,这篇应该能让你少走一点弯路。

1. 准考证号不是一串字符串,而是一张小表格

先看题目里给的“准考证号”,一长串字符,例如A101180510001这种格式。很多人第一反应是:把整个字符串存下来,等到查询的时候再用substr截取想要的信息。这个思路没错,但写起来会非常啰嗦,而且越写到后面越乱。

其实准考证号是有固定编码规则的,常见的拆法是这样的:

  • 第1位:考试级别,取值是A、B、T三种之一;
  • 第2到第4位:考场编号,三位数字;
  • 第5到第10位:考试日期,六位数字,通常是yymmdd这种形式;
  • 第11到第13位:考生编号,三位数字。

所以一个13位的准考证号,本质上是四段独立信息的拼接。你把它当成“一张小表格”而不是“一串字符串”来理解,后面所有逻辑都会顺很多。

我当时的做法是先把每一条考生记录读成一个结构体,把需要的信息字段提前拆好、存好:

struct Student { string id; // 完整准考证号 int score; // 成绩 char level; // 考试级别 A/B/T string room; // 考场编号 string date; // 考试日期 yymmdd string seq; // 考生编号 };

读入数据的时候,直接按位置拆字段:

int n, m; cin >> n >> m; vector<Student> students; students.reserve(n); for (int i = 0; i < n; i++) { string id; int score; cin >> id >> score; Student stu; stu.id = id; stu.score = score; stu.level = id[0]; // 第1位是级别 stu.room = id.substr(1, 3); // 第2~4位是考场 stu.date = id.substr(4, 6); // 第5~10位是日期 stu.seq = id.substr(10, 3); // 第11~13位是考生编号 students.push_back(stu); }

这里看起来只是简单的substr,但它真的能省掉后面很多事。比如类型2查询要按考场统计,如果你存的是完整准考证号,每次都得临时substr(1,3)再去哈希表里累加,代码不优雅,还容易把位置记错。因为这条数据规模并不会大到离谱,所以提前拆字段的开销完全可接受,换来的是后续代码清晰度的大幅提升。

还有个小提醒:日期和考场编号这种字段,千万别用int去存。考场编号可能是001、101这种形式,如果是int,前导零会丢掉,等输出或者排序的时候就对不上了。字符串存最稳,比较起来也是字典序,正好符合题目要求的“考场编号递增”。

2. 三种查询本质上对应三类数据处理模式

题目会给你若干条查询,每个查询由一个类型编号和一个参数组成。先说清楚三种类型分别要干什么:

查询类型查询参数输出要求本质操作
1考试级别A/B/T该级别所有考生,按成绩排名输出过滤 + 排序
2考场编号该考场考生人数和总成绩分组统计
3考试日期该日期下各考场的考生人数,按考场升序两级分组计数

我当时看到这三种查询,第一反应是“啊,这不就是三种题塞在一道题里了嘛”。但冷静下来想一下,它们其实代表了三种非常典型的数据处理模式:

类型1是典型的“先过滤再排序”:给定一个条件,找出符合条件的记录,然后按指定的排序规则输出排名。这个模式在后端开发里太常见了,比如“某个分类下按价格排序的商品列表”。

类型2是典型的“分组聚合”:对每条记录按照某一个字段做分组,然后统计每组的总数、总和。注意它不需要关心单个成绩,只要人数和总分,所以本质上就是个一维的计数器累加。

类型3是“两级分组”:先按日期分一层,再按考场分一层,最后输出第二层的统计结果。它比类型2多了一个维度,但只要你想清楚用什么数据结构去套,也不难。

把它们放在同一道题里,考的就是你面对多维信息时,能不能快速地把不同需求映射到不同的数据组织方式上。说白了,你会不会用哈希表、会不会排序、能不能在两三个结构之间切换。想清楚这一点之后,代码怎么写就变得很明确了。

3. 排序和排名:这个细节最容易丢分

先说排序规则。题目一般要求考生按成绩从高到低排序,如果成绩相同,就按准考证号从小到大排。这里有两个关键点,一个都不能忽略:第一,成绩降序;第二,成绩相同时,准考证号升序。你用sort自定义比较函数的时候,必须同时处理这两个规则。

bool cmp(const Student& a, const Student& b) { if (a.score != b.score) { return a.score > b.score; // 成绩降序 } return a.id < b.id; // 同分时准考证号升序,字典序即可 }

这个比较函数看着简单,但我见过不少同学只写了return a.score > b.score,结果成绩相同的情况完全没处理,排序后顺序不稳定,排名自然跟着错。另外要注意,a.id < b.id字符串比较的时候是按字典序比较的,这对包含字母和数字的准考证号来说是合理的,不需要额外转成数字再去比。

然后是排名计算。PAT这类考试里最常见的排名规则是“并列排名”:同分的人排名一样,下一个不同分数的排名要跳过并列人数。举个例子:

  • 100分:第1名
  • 99分:第2名
  • 99分:第2名
  • 98分:第4名

第二个99分和第1个99分都是第2名,但98分直接排到第4名,因为前面有3个人(100、99、99)。如果你把98分排成第3名,那肯定不对。

具体实现的时候,我建议用一个整数记当前名次,边遍历边输出:

int currentRank = 1; for (int i = 0; i < vec.size(); i++) { if (i > 0 && vec[i].score != vec[i - 1].score) { currentRank = i + 1; // 分数变了,名次跳到实际位置 } cout << currentRank << " " << vec[i].id << " " << vec[i].score << "\n"; }

注意这里的currentRank = i + 1,不是currentRank++。因为当分数变化的时候,i已经是当前元素的下标了,下标从0开始,所以实际名次就是i + 1。如果你写currentRank++,遇到“前面并列的人多”的情况,算出来的名次就会少跳几位。

我当时第一版就栽在这个细节上,成绩单排序没问题,排名却一路错下去,最后只能回头重新排查。这种和业务规则强相关的细节,恰恰是最容易被忽略、也最容易被测试数据逮住的。

4. 三种查询逐一实现:选对结构,代码量直接减半

4.1 类型1:按级别过滤并排名

类型1的查询参数是级别A、B或T,要求输出该级别下所有考生的排名、准考证号和成绩。

最简单的做法是:每次查询来了,遍历所有学生,找到级别匹配的,再排序输出。但如果查询次数很多,每次都排序,那么最坏情况下就是M次O(N log N),很容易超时。

稳妥的做法是提前把学生按照级别分好组,每组排好序,之后每次查询直接输出那一组的成绩列表。反正级别只有三种,用map<char, vector<Student>>就能很自然地组织:

map<char, vector<Student>> byLevel; for (auto& stu : students) { byLevel[stu.level].push_back(stu); } for (auto& entry : byLevel) { sort(entry.second.begin(), entry.second.end(), cmp); }

查询的时候,只需要找到对应级别的vector,然后套用上面的排名输出逻辑就可以了。这样做的好处是,排序只做一次,后续所有类型1的查询都能直接复用结果。

如果你还想更快一点,甚至可以提前把每一组的排名也算好,存到结构体里。不过通常没必要,因为输出本身也是O(考生数),排序只做一次并不算瓶颈。

4.2 类型2:考场人数和总成绩,预计算一次

类型2的查询参数是考场编号,要输出这个考场的考生人数和总成绩。这里绝对不能每来一个查询就遍历一遍所有学生去累加,那样数据量一大就会超时。

正确做法是读入所有学生的时候,顺手就把每个考场的信息统计好。考场编号只有三位数,理论上最多1000个考场,用unordered_map或者map来存都行。

unordered_map<string, pair<int, int>> roomStat; // key: 考场编号 // first: 人数 // second: 总成绩 for (auto& stu : students) { roomStat[stu.room].first++; roomStat[stu.room].second += stu.score; }

查询的时候直接查表输出:

string room; cin >> room; if (roomStat.count(room)) { cout << roomStat[room].first << " " << roomStat[room].second << "\n"; } else { cout << "NA\n"; }

这里有个细节:pair<int,int>的second是总成绩,也就是所有考生成绩直接累加的和。题目要求输出“总成绩”而不是“平均分”,所以不需要除以人数,别画蛇添足。

我当时做这步的时候还想过一个替代方案:用两个独立的unordered_map<string, int>分别存人数和总成绩。后来发现用pair合成一个value更紧凑,查询一次就能拿到两个值,代码也更好读。这种小优化看着不起眼,但在后面写类型3的时候,你会发现统一的数据组织方式会让思路清晰很多。

4.3 类型3:按日期统计考场,注意输出顺序

类型3的查询参数是一个日期,要求输出该日期下每个考场的考生人数,并且考场编号需要升序排列。

这比类型2多了一个层级,所以要用嵌套结构。第一层键是日期,第二层键是考场编号,值是该考场在该日期下的人数。我当时的实现是unordered_map<string, unordered_map<string, int>>:

unordered_map<string, unordered_map<string, int>> dateRoomCnt; for (auto& stu : students) { dateRoomCnt[stu.date][stu.room]++; }

查询某一天时,拿到该日期的内层unordered_map,把里面的键值对拿出来排序输出。因为题目要求考场编号升序,而unordered_map本身是无序的,所以得先转成vector再sort:

string date; cin >> date; if (!dateRoomCnt.count(date)) { cout << "NA\n"; continue; } vector<pair<string, int>> arr(dateRoomCnt[date].begin(), dateRoomCnt[date].end()); sort(arr.begin(), arr.end()); for (auto& item : arr) { cout << item.first << " " << item.second << "\n"; }

这里sort(arr.begin(), arr.end())排序的是pair<string, int>,默认就是按字符串升序排考场编号,不需要额外写比较函数,非常省事。

我见过有的同学用map<string, map<string, int>>来替代unordered_map嵌套。其实也可以,map内部本身就是有序的,输出时直接遍历就行。区别在于map的插入和查询是O(log N),unordered_map是期望O(1)。在这个数据规模下两者都够用。我选unordered_map主要是想强调“能用哈希就别用树的天然排序去掩盖逻辑需求”,好习惯是先把结果数据整理排序,而不是依赖容器的内部顺序。这一点在更复杂的题目里很重要。

4.4 三种查询的复杂度对比如表

查询类型预处理方式单次查询时间复杂度依赖的数据结构
类型1按级别分组,每组排序一次O(该级别人数)map + vector
类型2读入时预计算人数和总分O(1)unordered_map
类型3读入时预计算日期×考场人数O(该日期下考场数)嵌套unordered_map

看完这张表你会发现,其实只要做一次读入时的预处理,后面每次查询都几乎不费力。这个思路不是这道题独有的——很多“查询很多、数据固定”的题,都应该把重心从“怎么查”挪到“怎么预先算好”。这也是PAT甲级非常喜欢考察的工程思维。

5. 考场实战:超时陷阱、边界测试与复盘总结

5.1 输入输出优化,别小看这件事

很多同学在练习时从没被输入输出卡过,但到了PAT考场上,数据量一上来,cin和cout默认的同步模式慢得让人怀疑人生。我当时的第一版代码就是因为这个吃了亏——本地测试数据小,看不出问题,实际跑大数据的时候直接超时。

解决办法有三选一:

  • 用scanf和printf读整型和字符,字符串用cin读,输出用printf;
  • 在main开头加一行ios::sync_with_stdio(false);并且cin.tie(nullptr);,关闭C++标准IO和C标准IO的同步;
  • 输出的时候避免使用endl,改用'\n'。

我比较推荐第二种,代码改动最小,效果也最明显。下面这行是我后来写所有题都会加的口诀式代码:

ios::sync_with_stdio(false); cin.tie(nullptr);

另外,输出大量行的时候,endl会触发缓冲区刷新,速度会慢很多,用'\n'就完全够。这个在本地可能感知不到,在后台评测机上却是实打实的性能差距。

5.2 边界情况:查不到记录一定要输出NA

三种查询里,找不到对应结果的时候题目基本都会要求输出NA。这个分支很容易被遗漏,因为样例数据通常都是能查到的。我建议写代码的时候先把NA的出口写好,再写正常逻辑。

需要注意每个类型的NA场景不太一样:

  • 类型1:该级别下没有任何考生;
  • 类型2:该考场编号不存在,或者虽然有这个考场但没有学生记录;
  • 类型3:该日期下没有任何考场记录。

我当时就犯过一个小错误:类型2把人数统计和总成绩统计分别存在两个map里,结果查询考场时,只检查了人数map,没检查总成绩map,遇到一个没有总成绩但有人的考场,逻辑就乱了。后来统一成pair结构才顺手解决。这些细节看起来很小,但真到考场上,它们才是拉分的地方。

5.3 字符串字段的前导零问题

再强调一次,考场编号和考试日期这种字段必须用字符串存,不能用int。因为考场编号有可能是001,日期也可能是010101这样的前导零数据。一旦转成int,前导零就丢了,和后面输入的查询参数对不上,输出格式也会错。字符串字典序比较恰好还能满足“考场编号升序”的要求,所以别纠结,统一用string。

5.4 复盘:这类“编码解码”题到底在考什么

做完这道题往回看,它其实没有一个算法是你不会的——排序、哈希表、字符串截取,哪一个单独拿出来你都见过。它真正难的地方在于“组合”:你要在短时间内把题目语言描述的规则翻译成数据结构和数据处理流程。

我复盘时给自己总结了三句话,也分享给你:

  • 见到编码类数据,先拆字段,不要拿整串反复截取;
  • 查询多而数据固定时,预处理永远优先,别每次查询都全量遍历;
  • 输出格式相关的细节(排名规则、前导零、NA分支、升序排列)必须在动手写代码前就列出来,写完之后再逐条核对。

后来我再看这道题,觉得它像极了真实工作中“写一个报表查询接口”的简化版:底层数据是固定的,查询条件形形色色,你要做的就是把数据组织成最适合被查询的形态。想通了这一点,你就不会觉得PAT甲级只是刷题,它其实是在提前训练你用工程化的思路去处理数据。

我用这道题当练习题还试过另一种延伸:把类型3的“日期×考场”统计结果做成二维表,预先算好所有行列的累计值,这样就算查询条件从“按日期查考场”扩展成“按日期范围查所有考场”,也能快速给出结果。如果你做完这题还有余力,可以按这个方向再折腾一下,对哈希表的理解会更进一步。

相关新闻

Selenium模拟用户操作实战:从环境搭建到稳定运行的完整指南

Selenium模拟用户操作实战:从环境搭建到稳定运行的完整指南

1. 先说清楚&#xff1a;为什么我们要在 Web 自动化里“模拟用户操作”如果你接触过 Selenium&#xff0c;那么“模拟用户常见操作”这几个字其实已经概括了 Selenium 最核心的价值——它不是在网页上做简单的网络请求&#xff0c;而是像真实用户一样&#xff0c;打开浏览器、点…

2026/10/20 9:08:45 阅读更多 →
服装品类趋势与消费者洞察数据分析可视化系统实战解析

服装品类趋势与消费者洞察数据分析可视化系统实战解析

1. 毕设选题定位&#xff1a;这个系统到底解决什么问题第一次听到“服装品类趋势及消费者洞察数据分析可视化系统”这个题目&#xff0c;可能觉得只是把一堆图表凑在一起&#xff0c;加上个前端页面&#xff0c;套个电商数据就算完事。实际上真正动手做起来会发现&#xff0c;这…

2026/10/20 9:08:45 阅读更多 →
从聊天助手到工程干将:Claude Code冠军配置的三大支柱与落地实践

从聊天助手到工程干将:Claude Code冠军配置的三大支柱与落地实践

在终端里用 Claude Code 干过几星期活的人&#xff0c;多半会经历一个落差&#xff1a;它讲起架构设计头头是道&#xff0c;可你真把一个完整需求丢过去&#xff0c;让它从头改到尾&#xff0c;它常常干到一半就卡住——不是停下来问"下一步怎么做"&#xff0c;就是自…

2026/10/20 9:08:45 阅读更多 →

最新新闻

无源贴片晶振匹配解决方案:负载电容计算与PCB布局实战

无源贴片晶振匹配解决方案:负载电容计算与PCB布局实战

做硬件调试这些年&#xff0c;绕不开一个特别“小”但又特别容易坑人的器件&#xff1a;无源贴片晶振。很多开发者第一次画板子&#xff0c;MCU选好了、代码写好了&#xff0c;上电却发现系统时钟不对&#xff0c;或者干脆不振&#xff0c;最后查来查去&#xff0c;问题往往就出…

2026/10/20 9:46:04 阅读更多 →
密钥不是密码:从加密算法到密钥全生命周期管理

密钥不是密码:从加密算法到密钥全生命周期管理

做安全这么些年&#xff0c;我发现一个特别普遍的现象&#xff1a;很多人一听到“密钥”&#xff0c;第一反应就是“哦&#xff0c;密码嘛”。这个误解比想象中更耽误事。密钥不是你在网站注册时设的那串字符&#xff0c;也不是登录服务器的口令。密钥是加密算法真正依赖的那个…

2026/10/20 9:46:04 阅读更多 →
计算机视觉学术速递拆解:从高频词到落地价值的阅读方法论

计算机视觉学术速递拆解:从高频词到落地价值的阅读方法论

1. 计算机视觉学术速递到底在速递什么第一次看到"计算机视觉学术速递[9.7]"这个标题&#xff0c;很多人会以为就是一个论文列表搬运。但我实际跟踪这类速递内容两年多&#xff0c;发现真正有价值的部分根本不是"有哪些新论文"&#xff0c;而是从当天扎堆出…

2026/10/20 9:46:04 阅读更多 →
C语言入门不绕路:环境搭建、指针初体验与新手高频坑

C语言入门不绕路:环境搭建、指针初体验与新手高频坑

决定学C语言那天&#xff0c;我在笔记本上只写了一行字&#xff1a;从今天开始&#xff0c;不绕路了。之前大半年我一直徘徊在各种语言教程里&#xff0c;能照着写点小工具&#xff0c;但只要别人追问一句"那个内存错误到底怎么回事""为什么这段代码快那段代码慢…

2026/10/20 9:46:04 阅读更多 →
Transformer长上下文之困:SSM与混合架构如何破局?

Transformer长上下文之困:SSM与混合架构如何破局?

1. 为什么Transformer的“上下文墙”不只是算力问题&#xff0c;而是记忆结构问题我一开始接触长文本任务的时候&#xff0c;也以为只要把显卡堆上去、把序列长度拉到128K、甚至1M&#xff0c;Transformer就能从容应对。直到有一次跑一个金融公告的风险因子抽取任务&#xff0c…

2026/10/20 9:46:04 阅读更多 →
AI课程矩阵实战路线:从提示词工程到API调用与批量部署

AI课程矩阵实战路线:从提示词工程到API调用与批量部署

AI 课程现在不难找&#xff0c;难的是判断一门课在你的成长路径里到底处于哪个位置。市面上大量课程要么只教提示词&#xff0c;要么直接堆 ComfyUI 节点&#xff0c;学完能复现案例&#xff0c;但换个场景就不会用了。“河南元力共生 AI 课程矩阵”这个提法&#xff0c;核心是…

2026/10/20 9:45:00 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →