免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

华为OD机试C卷明日之星选举:投票统计与多关键字排序C++实现

华为OD机试C卷明日之星选举:投票统计与多关键字排序C++实现 提起2026年的华为OD机试C卷有一道题我印象特别深——《明日之星选举》。名字听着挺唬人像是什么复杂业务系统实际上机考碰到它的时候不少人都折在了“看着简单、一写就错”的坎上。这道题的核心逻辑就是投票统计加多关键字排序放在C卷里属于典型的“基础题中的分水岭”代码量不大但边界条件密排序规则藏得深稍不留神就会在自测时全绿、提交后爆红。这篇文章我就用C把这道题完整拆开讲一遍。从题目规则还原、数据结构选型、完整实现到我在调试中踩过的坑和最后总结出的避坑清单全部按实际做这道题的思考顺序来写。不管是准备华为OD机试、大厂笔试还是蓝桥杯、CSP这类算法竞赛只要你需要练“字符串哈希统计 自定义排序”这个组合这篇都值得认真看一遍。1. 题目“明日之星选举”核心规则与考点拆解先说这道题为什么值得单独写一篇。因为“明日之星选举”在C卷里代表的是一大类考题规则看着像业务逻辑实际考的是最基础的工程编码能力。这种题不会像动态规划那样考你思维深度它考的是你能否把一句话的需求准确转换成无歧义的代码能不能把所有边界情况都照顾到。1.1 题目规则还原按C卷常见描述根据C卷考生回忆和历年出题规律这道题的核心规则可以还原成下面这样某公司要举办“明日之星”评选活动共有N名候选人参与评选M名员工参与投票评选结束后需要从高到低选出得票数最高的K名候选人。输入格式如下第一行包含三个整数 N、M、K分别表示候选人数、投票人数、晋级人数。接下来N行每行包含一个字符串表示候选人姓名。候选人姓名由大小写英文字母组成不包含空格长度不超过20。接下来M行每行包含一个字符串表示该员工投给的候选人姓名。计票规则每张有效票计1分。如果投票姓名不在候选人名单中视为废票不计入任何候选人。最终按总得票数从高到低排序得票相同的按姓名字典序升序排列。输出得票数最高的K名候选人每行输出“姓名 得票数”。如果得票数大于0的候选人数不足K人则按实际人数输出。数据范围1 ≤ N ≤ 10^51 ≤ M ≤ 10^61 ≤ K ≤ N这个还原版本基本保留了原题的考察点。我自己写代码时习惯先把规则在纸上拆成“输入处理、统计存储、排序、输出”四段再动手这道题尤其需要这么做因为它的规则细节实在太多直接上手写循环很容易漏掉废票判断或排序规则。1.2 这道题到底在考什么拆开看这道题一共覆盖了四个核心考点字符串存储与比较。候选人姓名是英文字符串最长20个字符。C里string可以直接用进行字典序比较这个特性后面排序时会用到。哈希映射统计。需要建立“姓名 → 票数”的映射关系。常用的做法是用mapstring, int或unordered_mapstring, int。这里就涉及一个选择问题到底用哪个我在第2章详细讲。多关键字排序。排序的第一关键字是票数降序第二关键字是姓名升序。这在C里用sort加lambda表达式几行就能搞定但很多人第一次写会忽略第二关键字导致相同票数的候选人输出顺序不稳定。边界条件处理。废票怎么判、K大于有票人数怎么办、候选人得票为0要不要输出这些细节直接决定你能否拿到满分。顺便说一个很多备考的人容易忽略的点华为OD机试C卷的题正确率比速度重要。一道题拿到满分和拿到80分差距往往不在主逻辑上就在于这些边界细节。2. 数据结构和排序方案为什么这么选这道题刚拿到手的时候我脑子里第一个蹦出来的想法是用一个mapstring, int存票数最后转成vector再排序。但后来我仔细想了想发现这里其实有几个可以斟酌的设计点。2.1 数据结构的选型分析先看统计阶段的核心需求给定一个投票姓名快速判断它是否在候选人名单中如果在票数加1。这个需求有三个候选方案vectorstring存放所有候选人姓名每次投票时find查找。每投一票就要遍历一次候选人列表时间复杂度O(N)M张票就是O(N×M)在N10^5、M10^6的极端数据下会直接超时。unordered_mapstring, int哈希表查找平均O(1)复杂度。优点是快缺点是遍历时元素无序后面排序前需要手动把键值对转成vector。mapstring, int红黑树查找O(logN)复杂度。虽然没有哈希表快但logN在10^5量级下也只有17次比较实际差距可以忽略。我最终选择了map。原因有两条第一代码简洁性。map的count(key)可以直接判断键是否存在存在则通过map[key]累加不需要额外的标记数组或查找逻辑。第二天然有序性。map按键即姓名有序排列虽然最后还是要转vector排序但调试时打印中间结果会比较直观排查问题方便。这里提醒一句如果你的目标是极致性能用unordered_map本身没问题但要注意它遍历时无序而且如果题目要求按“输入顺序”输出同票候选人unordered_map会给你带来麻烦。就本题而言map是性价比最高的选择。2.2 排序规则的C实现思路统计完票数后需要把所有候选人放进一个可排序的容器里。因为map是键值对我定义了一个简单的结构体struct Candidate { string name; int votes; };然后把map里的数据逐个转移到vectorCandidate中。这里有一个小坑很多人会在转移过程中忘记处理得票为0的候选人。按题目规则0票的人不会被输出所以在最终输出时可以加一层过滤不用提前删除。排序部分的lambda表达式如下sort(candidates.begin(), candidates.end(), [](const Candidate a, const Candidate b) { if (a.votes ! b.votes) return a.votes b.votes; // 票数降序 return a.name b.name; // 票数相同按姓名升序 });这个比较器用了C11的lambda表达式是华为OD机试里非常高频的写法。需要特别注意的是比较器的严格弱序要求两个比较项必须能给出确定的大小关系不能出现a b和b a同时为假但两者又不等价的情况。我上面这个写法满足要求。2.3 复杂度估算统计阶段map查找的复杂度是O(logN)共M次总计O(MlogN)。排序阶段有N个候选人参与排序复杂度O(NlogN)。在N10^5、M10^6的范围内总耗时通常在几百毫秒级别完全可以通过机考的时间限制。空间上map存储N个键值对vector存储N个结构体总空间O(N)也没有任何压力。3. C完整实现与代码逐段解读这一章直接上完整代码。代码基于C11标准编写输入输出使用cin和cout本地用VSCode配置C/C环境即可直接编译运行。3.1 完整可运行代码#include iostream #include string #include map #include vector #include algorithm using namespace std; struct Candidate { string name; int votes; }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N, M, K; cin N M K; mapstring, int voteCount; // 读入候选人名单 string name; for (int i 0; i N; i) { cin name; voteCount[name] 0; } // 读入M张投票并统计 string vote; for (int i 0; i M; i) { cin vote; if (voteCount.count(vote)) { voteCount[vote]; } // 不在候选人名单中废票不处理 } // 转为vector以便排序 vectorCandidate candidates; for (auto it voteCount.begin(); it ! voteCount.end(); it) { candidates.push_back({it-first, it-second}); } // 按票数降序票数相同按姓名升序 sort(candidates.begin(), candidates.end(), [](const Candidate a, const Candidate b) { if (a.votes ! b.votes) return a.votes b.votes; return a.name b.name; }); // 输出前K名只输出得票数大于0的候选人 int printed 0; for (const auto c : candidates) { if (c.votes 0 || printed K) break; cout c.name c.votes \n; printed; } return 0; }这段代码的输出逻辑要注意如果某个候选人的票数为0说明后面所有候选人的票数也均为0因为已经按票数降序排列直接break跳出即可不需要继续向后遍历。3.2 逐段解读关键代码先看ios::sync_with_stdio(false); cin.tie(nullptr);这两行。这是C输入输出优化的固定搭配。华为OD机试的输入量可能达到百万行如果不关闭C iostream与C标准IO的同步cin的性能会明显下降。我实测过在M10^6的测试数据下加不加这两行运行时间能差出好几倍。再看读投票的逻辑。voteCount.count(vote)返回的是键vote在map中出现的次数只能是0或1。这种写法比先find再比较迭代器更简洁也比直接用operator[]更安全——因为operator[]在键不存在时会自动插入一个默认值虽然本题中插入后也不会影响最终结果但会轻微浪费空间更重要的是代码意图不够清晰。排序用lambda表达式这里再强调一次比较器里的两个return缺一不可。只写票数降序虽然输出时大部分情况没问题但在票数相同的场景下标准排序算法不保证稳定输出结果可能和预期不一致。加了第二关键字的比较规则才是真正的“无歧义排序”。输出阶段用printed变量计数同时用c.votes 0作为终止条件。这个设计隐含了一个结论得票为0的人在排序后一定排在最后面所以一旦遇到票数为0的候选人后面的都不用看了。3.3 一个自测样例为了验证代码正确性我用下面这组数据做过自测5 8 3 Alice Bob Carol David Eve Alice Bob Alice Carol Eve Alice Frank Bob注意这里有一张投给Frank的票但Frank不在候选人名单中属于废票。程序运行后输出Alice 3 Bob 2 Carol 1这个结果符合预期Alice三票排第一Bob两票排第二Carol和Eve各一票按字典序Carol在Eve前面所以Carol拿到最后一个晋级名额。4. 我实测踩过的坑边界条件才是失分重灾区说实话这道题我第一次写代码花了不到10分钟但后面调Bug花了将近半小时。问题不出在主逻辑全是边界条件。这一章把我在实际调试中遇到的几个典型问题完整梳理一遍每一个都是真实翻车现场。4.1 坑一排序规则理解偏差有个特别容易踩的坑题目说“得票相同按姓名字典序升序”但很多人的第一反应是“按输入顺序”。表面上看起来没区别但一旦候选人名字是乱序输入的立刻就出问题。举个例子。候选人输入顺序是Eve、Alice、Bob如果某人把排序规则写成了“票数相同按输入顺序”那么在有并列票数时输出顺序就会和正确答案不一致。这种Bug很难通过小数据发现因为小数据往往样本少、并列情况不明显。我的经验是凡是题目说了排序规则就以排序规则为准不要依赖任何外部顺序。候选人输入顺序、map内部的键顺序都不应该作为最终输出的依据。4.2 坑二cin和cout的输入输出性能问题第二个坑是性能问题。机考环境的数据量经常比我预想的大得多M10^6的投票记录如果不用优化光cin读入就可能超时。我第一次提交时没有加ios::sync_with_stdio(false)本地测试没发现异常结果在线上数据量上来之后直接超时。后来我养成了一个习惯所有用到cin/cout的题代码第一行就写优化语句不管题目数据量看起来大不大。这个习惯让我后来在各种机考中省了不少麻烦。顺带说一句如果只是输出一行一行的简单数据用\n换行而不使用endl也是性能优化的一个细节。因为endl除了换行还会强制刷新输出缓冲区在大量输出时会拖慢速度。4.3 坑三0票候选人的输出问题第三个坑是输出规则。题目说“输出得票数最高的K名候选人”但没说0票的人算不算“候选人”。我在写第一版代码时直接输出了排序后的前K个人结果发现只要前K名里混入0票的人输出就和答案对不上。后来我仔细想了一下实际场景“明日之星”晋级0票的人肯定不可能晋级所以规则应该是“只输出有票的人最多输出K个”。我在代码里对应的处理是if (c.votes 0 || printed K) break;这个写法比“先筛掉0票再输出前K”更高效因为排序已经保证了有序性遇到0票直接终止循环即可。4.4 边界用例测试清单给大家一份我自测时用得比较顺手的用例清单建议拿到代码后先跑一遍测试场景输入要点预期结果常规场景5候选人、8票、K3输出票数前三名废票场景投票给不存在的人废票不计入结果K大于有票人数K10实际只有3人有票只输出3人全部0票所有票都是废票无输出票数全部并列所有人都是1票按字典序输出单候选人N1、M1、K1输出唯一候选人及票数最后一行的单候选人场景经常被忽略但它能最快验证代码的基本逻辑是否正确。5. 题目变形与备考思路从一道C卷题看一类题型华为OD机试的C卷题有时候会做一些规则上的微调这道“明日之星选举”就是典型的可变形题目。如果能准确识别出它的核心考点无论题目怎么包装都能快速找到解题方向。这一章聊几个常见的变形方向和我总结的备考思路。5.1 常见变形方向一名字变成“工号姓名”的组合候选人姓名可能附带工号比如输入10001 Alice投票时投10001或Alice都要能识别。这种情况就需要定义一个结构体来存储姓名与工号的映射关系并修改map的键类型。处理这类变形的核心思路是先明确唯一标识是什么。如果工号唯一就以工号为键如果姓名唯一就以姓名为键如果两者都可能是输入形式就建立两个映射互相转换或者统一在输入阶段把“工号 姓名”转为同一个内部表示。5.2 常见变形方向二增加评委有效性校验原题只判断投票对象是否在候选人名单中变形题可能还会判断投票人是否有效。比如每个评委只能投一张票重复投票则该评委的所有票作废。这就需要在统计逻辑之外额外引入评委维度的信息用mapstring, vectorstring先存“评委 → 投票列表”最后统一校验。代码结构上可以先完整读取所有投票信息做两轮处理第一轮判断每个评委的票是否有效第二轮才真正计票。这样主逻辑依然清晰只是输入处理阶段多了一步缓冲。5.3 常见变形方向三晋级规则改为“得票过半”有时候题目会把输出规则从“前K名”改成“得票数超过总有效票数一半的候选人全部输出”。这种变形更贴近真实选举场景考察的核心就变成了“总有效票数”的统计——注意是有效票废票不能计入分母。实现上只需要在统计阶段多维护一个有效票总数的变量输出阶段从排序好的向量中逐个判断即可。这个变形思路在往年题目里出现过值得提前准备。5.4 对备考华为OD机试的整体建议结合“明日之星选举”这道题我总结了几条对华为OD机试C卷备考比较实用的经验优先把基础题型吃透。统计、排序、字符串处理、模拟题是C卷的高频考点先把这类题做到“闭着眼睛都能写对”再谈动态规划和DFS。每天保持手写代码的感觉。机考和写业务代码不太一样它更强调在限时、有压力的情况下快速写出正确代码。我备考时每天至少完整写三道题不只看题解而是自己从头敲一遍。重视自测习惯。写完代码后不要急着提交先跑几组自己构造的边界数据。像我上面列的那张测试清单就是长期训练形成的习惯。这种习惯在正式机考中能帮你避免大量无谓的罚时。不要轻信押题和题库。网上流传的各种“华为OD机试题库”品质良莠不齐题目有没有价值前提是它足够贴近真实考点。与其花时间找题不如把历年真题按专题刷透研究题目背后的考察逻辑。如果你用的是VSCode写C记得提前调试好环境包括编译器的安装路径、task.json的配置、调试器的使用。机考是双机位监考考试过程中不能随意切屏查资料环境问题最好在正式考试前就全部解决掉。最后再分享一个我备考时的小习惯每做完一道题我会把它的核心考点用两三句话记录在文档里比如“明日之星选举map统计 vector排序 lambda比较器”。遇到类似题型时先在脑子里匹配之前总结的考点再动手写代码。这个方法帮我保持住了刷题的节奏也让我在实战中少走了很多弯路。
返回列表