尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++机试复盘:从字符串解析到单调栈的踩坑与优化策略

C++机试复盘:从字符串解析到单调栈的踩坑与优化策略 机试这种东西说到底拼的不是谁刷题多而是谁能在有限时间内把思路翻译成能跑通的代码。我最近复盘了一次C机试日期是26年3月14日题目编号从t100到t103一共四道难度递增覆盖了字符串处理、排序、数据结构和数学优化几个高频考点。这套题做下来让我意识到很多平时写业务代码不会暴露的问题一到机试就会疯狂冒头。这篇文章我就把这四道题的完整思路、代码实现、以及考场上的真实踩坑记录下来给后面要参加机试的朋友一个参考。先说结论这套题整体不算偏但每道题都有隐藏的坑。如果你能在60分钟内稳定AC前三题第四题拿部分分就已经超过了相当一部分考生。下面我从命题逻辑说起再一道一道拆解。1. 机试命题的底层逻辑四道题分别想考察什么很多人拿到题目就急着写代码这是最大的误区。机试的四道题不是随便凑的它有一套非常明显的梯度设计逻辑。我复盘了t100到t103之后发现它们的考察目标基本可以归类为以下几个层次。1.1 从能写到会优化的三级跳第一题通常考查输入解析和基础数据处理只要会用cin、getline、istringstream这一类工具配合map或vector组织数据就能拿到分数。它筛的是你能不能写代码属于门槛题。第二题考查排序和自定义规则难度略微上升要求你不仅会用sort还得理解比较器的设计原理。这里有个关键区分稳定排序和非稳定排序在某些场景下直接决定答案对错。很多人在这里翻车不是不会排序而是不知道std::sort的不稳定性会改变相同权重元素的相对顺序。第三题开始进入算法层常见的出法是单调栈、双指针、滑动窗口。它筛的是你懂不懂数据结构与算法的经典模型需要你识别题面背后的套路而不是对着题目硬模拟。第四题往往是综合题把数学优化、贪心或者动态规划里的某一类核心思想嵌入到一个场景里。它筛的是你在压力下还能不能保持思路清晰通常不会让你一次写对部分分的设置就是给准备不充分的人留的。比如说快速幂、质数判断优化这类数学底层能力会频繁出现在第四题的某个环节里。1.2 判题系统没说但你必须懂的三条规则关于判题系统有几点在题目描述里通常不会直接写但实际影响非常大。内存限制这条容易被忽略。机试常见的限制是128MB或256MB如果你第三题用了vectorvectorint存了一个10000乘10000的矩阵光这一个容器就会吃满内存程序还没跑就爆了。我的经验是预估内存时一个int按4字节算两层容器还要额外算上每层的对象开销不是简单的n乘m乘4。时间限制方面O(n^2)的算法在n为10^5量级时基本必挂。很多题目数据范围不是随便标的它直接暗示了期望复杂度——看到n小于等于1000O(n^2)可以接受看到n小于等于10^5你必须想O(n log n)的解看到n小于等于10^6基本上只有O(n)才能过。还有一个容易被忽略的点输入数据的格式往往比题面描述的更脏。比如字符串里可能混着多余空格、空行数字和字母之间可能用制表符分隔。准备一个健壮的输入解析函数比什么都重要。1.3 你该用什么标准分配做题时间我自己的分配方案是前15分钟通读四道题把每道题的数据范围、输入输出格式标记清楚。然后按从易到难的顺序做题不在第一题上反复改也不在第四题上死磕到最后一分钟。理想时间分配是t100约10分钟、t101约15分钟、t102约20分钟、t103剩余时间全力拿分。如果你在某一题上卡了超过20分钟果断跳到后面的题回头再来处理机试的得分效率永远比单题完美更重要。2. t100 解法复盘字符串解析与分组统计最基础也最容易被扣分这道题从难度上看属于送分题但送分不等于送满分。题目大意是输入一段由英文单词和数字组成的文本要求按某种规则分组统计并输出结果。听起来很简单但实际做起来有几个细节能卡掉不少人。2.1 题目场景还原与第一反应题目场景大概是这样的输入若干行文本每行由空格分隔的若干字符串组成要求统计每个单词出现的次数并按出现次数从高到低排序输出次数相同时按字典序输出。看到这个需求第一反应就是mapstring, int先统计再转成vectorpairstring, int排序。思路没问题但实现过程中的坑比你想的多。第一个坑在输入解析。文本可能包含连续多个空格、行首行尾空格、甚至空行直接用cin s逐单词读是可以的因为它天然按空白字符分隔。但如果你试图用getline读一整行再手动拆就必须处理好多余空格否则会出现空字符串进入统计容器的情况。第二个坑在大小写。题目没说要不要区分大小写但通常默认不区分。如果不先做归一化把大写转小写统计结果就会把Apple和apple当成两个单词直接丢分。转换成小写可以用std::transform(s.begin(), s.end(), s.begin(), ::tolower)一行搞定。2.2 字符串解析的健壮写法我当时的实现是这样的#include bits/stdc.h using namespace std; vectorstring splitWords(const string line) { vectorstring res; string cur; for (char c : line) { if (isalnum(c)) { cur tolower(c); } else if (!cur.empty()) { res.push_back(cur); cur.clear(); } } if (!cur.empty()) res.push_back(cur); return res; }这个函数用逐字符扫描替代istringstream好处是它能过滤掉标点符号和任意连续空白不用预先处理空格数量。tolower放在压入前调用保证单词统一小写。在这个场景里越简单的逻辑越不容易出错逐字符扫描虽然看起来土但它对输入的容忍度非常高。如果你不想逐字符处理用istringstream配合getline按空格拆分也可以但要做好空字符串过滤。两种写法本质一样关键是别在边界输入上崩。2.3 统计和排序的容器选择统计用unordered_mapstring, int在速度上有优势但排序时需要对pair排序而unordered_map没有排序能力所以最终还是得转存到vector。如果你直接用mapstring, int统计虽然插入是O(log n)但遍历时已经按键有序省去一次按字典序排序的额外负担。排序的实现很简单但比较器需要注意一个细节sort(vec.begin(), vec.end(), [](const auto a, const auto b) { if (a.second ! b.second) return a.second b.second; return a.first b.first; });这里先按次数降序再按字典序升序次序反了或者漏了一个条件输出顺序就不符合要求。很多人知道要写比较器但容易忽略第二排序键这是送分题拿不到满分的常见原因。2.4 这道题真正想考你的隐藏点复盘之后我发现这道题的核心考点其实不在排序而在输入解析的健壮性。词频统计本身没有任何难度难点在于面对脏输入时你的程序还能不能稳定工作。考场上的实际心态是越简单的题越容易掉以轻心正因为简单错了会非常影响后面做题的信心。我的建议是用getline(cin, line)逐行读还是cin word逐词读取决于题目是否要求保留行结构。如果只统计单词全程用cin word最省事如果输出要求按行保留原文顺序那必须用行读取加逐字符拆分。审清楚题再动手比写一个通用解析器再适配更高效。3. t101 解法复盘排序规则的细节设计与复杂度陷阱如果说t100是热身这道t101就开始上强度了。题目给了一个数组要求按照每个数字出现的次数升序排序次数相同的按数字本身降序排列。乍一看还是排序但实现起来比t100多了一层统计逻辑。3.1 题面之外的隐藏需求手写排序的备用方案题目本身可以用sort一把梭但机试环境里偶尔会要求你不能直接用库函数排序或者库函数的实现和预期不符。我建议至少能手写冒泡排序和插入排序不是为了在正常解法里用而是为了在规则复杂时能手动控制排序过程。比如这道题如果用稳定排序std::stable_sort配合次数升序比较器就能保证次数相同的元素保持原始相对顺序再叠加一个按数字降序或原始顺序调整。但如果你用的是std::sort库函数内部是不稳定的当你需要稳定的效果时就必须在比较器里把原始下标作为附加排序键struct Item { int val; int cnt; int idx; }; sort(items.begin(), items.end(), [](const Item a, const Item b) { if (a.cnt ! b.cnt) return a.cnt b.cnt; if (a.val ! b.val) return a.val b.val; return a.idx b.idx; });这里的第三排序键idx就是保底逻辑不管库排序稳不稳定只要有下标参与结果就是确定的。3.2 统计与排序的双阶段写法我的直接做法是分两步走。第一步用unordered_mapint, int统计频次第二步把所有不重复数字转成结构体数组再按上面的比较器排序最后按排序结果输出对应次数的数字。#include bits/stdc.h using namespace std; struct Item { int val; int cnt; int idx; }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; vectorint a(n); unordered_mapint, int freq; for (int i 0; i n; i) { cin a[i]; freq[a[i]]; } vectorItem items; for (int i 0; i n; i) { bool seen false; for (auto it : items) { if (it.val a[i]) { seen true; break; } } if (!seen) { items.push_back({a[i], freq[a[i]], i}); } } sort(items.begin(), items.end(), [](const Item x, const Item y) { if (x.cnt ! y.cnt) return x.cnt y.cnt; if (x.val ! y.val) return x.val y.val; return x.idx y.idx; }); for (auto it : items) { for (int j 0; j it.cnt; j) { cout it.val ; } } return 0; }注意我这里去重用了一个双循环n不大的时候没问题但如果n到了10^5量级这段代码就会超时。机试考场上时间宝贵我当时直接把它换成了unordered_set去重写法是遍历数组如果seen.find(a[i]) seen.end()就加入seen并push到items。复杂度从O(n^2)降到O(n)这是一个很关键的优化点。3.3 遇到排序题先想三件事复盘这道题时我总结了一个应对排序类题目的固定流程。先确认比较规则有几层把所有参与排序的条件写下来不要漏。然后确认排序稳定性是否影响结果影响就必须在比较器里补充原始下标或者改用稳定排序。最后确认数据范围对应的时间复杂度n小可以宽松处理n大必须避免重复扫描。尤其是第三点。t101看起来是简单的排序题但如果数据范围很大统计完频次之后再去逐个元素检索是否已出现就会变成O(n²)的隐性超时。我在调试的时候因为没有控制台输出一度以为自己的逻辑错了后来才发现是性能问题。机试不像平时开发不会有明确的报错告诉你运行超时它只会给你一个红色的大叉子。遇到这种情况第一反应应该是回头检查自己代码里的循环嵌套层级。3.4 现场最容易忽略的容器迭代器问题还有一个高频坑在对unordered_map进行遍历时不要试图修改它的元素。我当时为了省事想在遍历过程中直接删除已处理的键结果导致迭代器失效程序行为完全不可预测。正确做法是先把需要的信息拷贝出来再操作容器或者干脆多遍历几遍。在机试场景里牺牲一点时间换正确性是完全划算的。4. t102 解法复盘单调栈模型的识别与实现边界到了t102题目已经明确不是套模板能解决的了。这道题考的是一个经典模型给定一个数组对于每个元素找出右侧第一个比它大的元素输出距离或下标差。数据范围n是10^5级别意味着O(n²)暴力必挂。4.1 从暴力到单调栈两种思路的对比暴力思路谁都能想到两层循环外层固定位置内层向右扫描第一个更大的值遇到就记录。但n10^5时最坏情况是10^10次比较几十秒都跑不完。这时候必须引入单调栈。单调栈的理论基础其实不复杂维护一个栈栈内元素从栈底到栈顶严格递减或递增当遍历到一个新元素时不断弹出栈顶比它小的元素这些被弹出的元素遇到的第一个更大值就是当前元素。换句话说每个元素入栈一次、出栈一次整体复杂度是O(n)。那为什么这个模型的题目容易在机试里出现因为它要求你不仅知道单调栈这个概念还能在题目包装成求右边第一个比它高的柱子时把它识别出来。命题人不会直接告诉你请使用单调栈他只会给你一个看上去很像模拟的场景。4.2 核心代码与调试经验代码模板不长关键在细节。#include bits/stdc.h using namespace std; vectorint nextGreater(vectorint nums) { int n nums.size(); vectorint res(n, -1); stackint st; // 存下标 for (int i 0; i n; i) { while (!st.empty() nums[st.top()] nums[i]) { res[st.top()] i - st.top(); st.pop(); } st.push(i); } return res; }这里我一开始犯了一个错误判断条件用了。题目要求严格大于也就是说相同数值不能算作更大的元素。如果我用了相等值的元素也会被弹栈并记录结果导致答案偏差。这个细节非常容易忽略因为在样例数据里相等值往往只出现一两次不容易暴露问题。另一个调试经验是栈里存下标而不是存值。存值虽然比较方便但输出答案时需要额外映射下标而且栈里可能出现重复值无法区分位置。存下标之后访问原数组用nums[st.top()]即可弹栈后也能直接定位到结果位置一举两得。4.3 栈的方向与初始化边界这道题要求找右边第一个更大元素所以从左往右遍历时栈是天然递减的。如果你遇到的是找左边第一个更大元素的变体遍历方向反过来即可别的逻辑完全一致。初始化输出数组为-1也很重要。不是每个元素都有右边更大的值栈里剩余元素对应的结果就是-1。这个初始值如果不设置默认的0会变成合法答案再次隐蔽出错。4.4 为什么单调栈在机试里如此高频我在复盘整套题时发现t102这类单调栈题几乎是机试的高频钉子户。原因有两个第一它是一个短小精悍的算法能在一道题里有效区分考生是否受过系统训练第二它容易和贪心、二分等模型结合出变体就算同样叫右边第一个更大也能换出不少花样。所以你与其纠结要不要背模板不如把模板的推导过程理解一遍知道为什么弹栈、为什么O(n)、为什么存下标。一旦理解变体题拿到手就不会慌。5. t103 解法复盘数学优化与综合题的部分分策略t103是这套题里最考验综合能力的一道题目融合了质数判断、快速幂和贪心选择。我做完前面的题还剩二十多分钟这道题只拿了部分分但通过复盘我搞清楚了完整的正确解法。5.1 质数判断的优化从O(sqrt(n))到提前筛选题目有一个环节需要频繁判断一个数是否为质数如果对每个数都做一次从2到sqrt(n)的试除当测试量是10^5时总计算量仍然是10^5 * sqrt(10^5)约等于3*10^7勉勉强强能过。但如果测试量更大或者单次判断需要反复执行就必须做一次预处理。判断质数的优化有两条路。第一条是对单个数字做6k±1优化大于等于5的质数一定分布在6的倍数两侧所以只需要检查i从5到sqrt(n)步长为6判断n % i 0和n % (i 2) 0。这个方法可以把试除次数减少大约三分之二代码写起来也简单。第二条路是埃氏筛适合一次算好一个区间的所有质数。如果题目数据范围是1到10^6用vectorbool标记合数再加一层循环输出质数列表整体复杂度接近O(n log log n)性能远比反复试除好。5.2 快速幂笔试中的常客实现比原理重要题目里有一个环节需要计算大数的幂次并取模如果用循环累乘遇到指数是10^9的时候直接跑不动。快速幂的核心思路是指数二进制拆分把乘方操作从O(k)降到O(log k)。long long fastPow(long long base, long long exp, long long mod) { long long res 1 % mod; base % mod; while (exp 0) { if (exp 1) res res * base % mod; base base * base % mod; exp 1; } return res; }这个模板有几个容易出错的细节。首先是mod可能为1所以res初始化为1 % mod否则任何结果都会因取模变成0而与预期不符。其次是乘法过程中可能溢出long long在10^9级别相乘会到10^18接近上限建议使用__int128做中间量再转回来或者题目如果保证模数在10^9以内可以先判断再做乘法。我当时在调试时发现把base取模放在循环外固然正确但每次更新base base * base % mod不要漏掉取模否则下一轮乘法照样溢出。5.3 部分分策略拿分比满分更重要说实话t103的完整解法需要快速幂、质数筛选、贪心三者同时作对在机试十分钟内写对并调通难度很大。我的建议是遇到这种综合题先写暴力版本保证小数据全过拿稳部分分然后针对题目数据范围做优化。比如暴力循环算幂次n小时能跑过提交后拿到的分数是部分正确这已经比编译失败或者超时好太多。我还踩了一个没必要的坑写完快速幂之后没有验证边界指数为0的情况。指数为0时任何非零数的0次幂都是1但模数为1时按定义应该是0。我当时没考虑模数为1的特殊输入用res 1 % mod初始化的写法正好规避了这个坑但这属于运气好。正确习惯是拿到一道题先把边界条件列出来空数组、单个元素、最大值、最小值、模数为1等逐个验证再提交。5.4 综合题里的贪心环节把大问题拆成小块最后那部分贪心本质上是从一系列可行操作中选择最优组合。我当时的做法是先把所有候选操作按单位收益排序然后从高到低依次尝试只要能加就加。这种贪心不一定保证全局最优但在机试这种场景下配合部分分策略效果往往不错。如果题目要求更高需要用动态规划但动态规划在机试里出现频率相对低而且难度陡增。我的建议是把贪心和二分搜索这两种优化手段练熟它们能覆盖绝大多数综合题的优化环节。6. C机试的隐性扣分点与考前自检清单四道题复盘完了但我发现最影响成绩的往往不是算法本身而是一些平时不写机试根本注意不到的细节。这些东西不会出现在题面里却会在判分时悄悄地扣掉你的分数。我把它们单独拎出来说。6.1 输入输出性能cin和cout的灾难现场很多C选手习惯直接用cin和cout一旦数据量达到10^5级别性能就会骤降现场超时。这不是cin本身慢而是它默认和C标准输入输出同步多了一层缓冲区同步开销。解决办法是在main()开头加上这两行ios::sync_with_stdio(false); cin.tie(nullptr);第一行取消cin与scanf的同步第二行取消cin与cout的绑定。加完之后cin和cout的速度基本能跟上scanf和printf。但要记住这两行一旦加上就不能混用cin和scanf否则输入顺序无法保证。我当时在t101里就是混合用了结果数据读取顺序错乱排查了好一阵子才想起来是这个原因。如果你的机试环境允许直接全部用scanf和printf读写也是个稳妥方案但这要求你习惯C风格字符串处理string时得多一步转换。两种方式没有绝对优劣关键在于到底用哪种就要用到底别混着来。6.2 编译器差异与安全报错fopen与CRT函数热搜词里有个很典型的坑c 64位 fopen报安全错误。这是因为新版本的Visual C编译器默认要求使用带安全后缀的函数如fopen_s直接调用fopen会出现C4996警告或错误。机试环境如果是老版本GCC通常没有这个问题但如果你在本机用VS调试顺手就会踩上。解决办法是在代码最前面加上#define _CRT_SECURE_NO_WARNINGS或者把fopen改成freopen重定向输入输出文件。不要因为这种细枝末节浪费考试时间。还有一种更省心的方式在项目设置里把SDL检查关掉或在预处理器定义里加上_CRT_SECURE_NO_WARNINGS。机试题目如果明确说从标准输入读就别碰文件了直接用cin或scanf最省事。6.3 高频编译错误自查表我整理了一张考场上最常遇到的编译错误对照表每次提交前扫一遍能省不少时间。症状原因对策未定义标识符头文件没包含全或拼写错误用万能头#include bits/stdc.h数组越界访问for循环边界写错检查和尤其涉及0基下标栈溢出递归过深或局部大数组改用堆分配vector或改写为迭代类型不匹配int和long long混用有乘法和取模的运算统一用long long运行时崩溃空指针或迭代器失效访问容器前判空遍历时不要修改输出格式错误多空格、缺换行严格按样例格式逐字符比对编译错误并不可怕可怕的是同一个错误反复出现。我建议平时刷题时就把常用头文件和宏定义整理成一个模板考场上直接复制修改能省下很多无谓的时间。6.4 考前30分钟的动作清单我根据自己的经验总结了一套考前自检流程已经固化成了肌肉记忆。先确认编译环境用的哪个标准C11还是C17有些写法在老标准下编译不过。然后确认输入是从标准输入读还是从文件读题目描述里如果出现输入文件名为in.txt就要用freopen重定向千万不要傻等控制台输入。再确认输出格式要求比如是否需要行末空格每行结尾是否需要换行。接下来是内存和时间限制题目通常会写在最前面把它变成你选择算法复杂度的依据。最后快速浏览四道题的数据范围如果发现某道题的n特别大再回头看自己的算法复杂度是否匹配。这些检查做下来最多五分钟但能避免因为极小失误直接送命。6.5 从t100到t103提炼出的通用解法模板整套题做下来我把C机试的通用解法模板总结成了四步每道题都可以套用。第一步是读题和标注把输入格式、输出格式、数据范围、特殊边界条件用笔圈出来。第二步是选容器和数据结构根据是否需要去重、有序、快速查找、频繁增删选择vector、map、unordered_map、set还是stack。第三步是写骨架循环先把主流程跑通再回来优化复杂度。第四步是边界测试构造最小用例、最大用例、重复元素用例、空输入用例逐个跑一遍。这套模板的核心价值在于它把不确定的能写出来变成了流程化的按步骤完成。机试最大的敌人不是题目难而是你在慌乱中丢失了最基本的逻辑。有了模板至少能保证你可以稳定输出。复盘完这四道题我最大的感受是机试考的不只是算法更是你在限定时间内管理自己注意力的能力。C本身提供了vector、map、stack这些强大的工具但真正决定分数的是你能不能把工具用在正确的地方。t100提醒我注意输入解析的健壮性t101让我正视稳定排序和比较器设计t102逼我理解单调栈的本质而非背模板t103则教会我在有限时间内的取舍。如果你也在准备机试建议把这些经验变成自己的检查习惯特别是ios::sync_with_stdio(false)和边界条件测试这两件事一定要刻进DNA里。希望这篇复盘能帮你少走一些我走过的弯路。
返回列表