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的“日期×考场”统计结果做成二维表,预先算好所有行列的累计值,这样就算查询条件从“按日期查考场”扩展成“按日期范围查所有考场”,也能快速给出结果。如果你做完这题还有余力,可以按这个方向再折腾一下,对哈希表的理解会更进一步。
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的“日期×考场”统计结果做成二维表,预先算好所有行列的累计值,这样就算查询条件从“按日期查考场”扩展成“按日期范围查所有考场”,也能快速给出结果。如果你做完这题还有余力,可以按这个方向再折腾一下,对哈希表的理解会更进一步。