很多人私信问我C++机试到底该怎么准备,特别是那些看起来像“天书”的题号,比如今天要聊的这套“26.3.14 t100-t103”。先说结论:这不是什么竞赛真题,而是一套典型的机试模拟题组,编号t100到t103,难度从签到到进阶一路爬坡。如果你是准备校招机试、华为OD机试,或者只是想系统练一练C++算法底子,这套题组的拆解思路对你有直接的参考价值。
我拿到这个题组第一反应是:出题人很懂机试的套路。四道题不是随便堆难度,而是按照“能不能写出第一题”来筛人,再用“第三题防AK、第四题拉区分度”的标准设计。换句话说,这套题组就是一个小型机试的缩影。这篇文章我不打算只贴答案,而是把每一道题从读题、抽象、选算法、写代码到调试串起来讲,重点放在“为什么这么做”和“考场上怎么想”,让你看完之后遇到同类题能举一反三。
1. 机试的核心逻辑:这不是比谁聪明,是比谁稳
很多人第一次参加机试,心态就崩在“怎么这么多题、时间怎么这么紧、编译器怎么这么难用”。其实机试的本质不是选拔天才,而是在限定时间内考察编码熟练度、算法基础扎实度和代码的稳定性。t100到t103这套题组,恰好把这几项能力分层测了一遍。
1.1 t100-t103的难度阶梯设计
四道题官方没有公布过具体来源,但从题号规律和常见机试题库的分布来看,这个编号通常是按难度递增排列的。t100是签到题,目的就是让大部分人拿到分,建立信心;t101是基础算法题,通常考前缀和、差分、双指针这类套路化内容;t102开始上强度,常见的是单调栈、贪心或者简单的动态规划;t103则是压轴题,要么是数学推导、要么是复合数据结构,用来筛出真正有实力的选手。
这个阶梯设计对面是一个很直接的信号:你不需要四道题全会,但你必须保证前两题不丢分。很多人在t103上死磕一个小时,结果连t101都没写完,这就是典型的策略失误。我的建议很朴素:按顺序做题,遇卡超过20分钟果断跳过,先把能拿的分全部装进口袋。
1.2 机试评分里最容易被忽视的“隐形扣分点”
除了算法的正确性,机试系统还会考察程序对边界条件的处理。数组越界、整数溢出、死循环、输入读取不完整,这些不是“报错”而是“答案错误”。更狠的是,很多OJ平台不会告诉你错在哪个样例,只给你一个红色的WA。
所以我在练习时一直强调一个习惯:写完代码先别急着交,花两分钟检查三件事——数组开得够不够大、循环退出条件覆盖不覆盖边界、变量类型会不会爆范围。这套检查习惯,在t100-t103这类题组上尤其重要,因为前几题往往不是难在算法,而是难在“粗心”。
2. 环境与代码模板:考场上拼的就是肌肉记忆
机试跟平时写项目完全是两回事。你在IDE里可以慢慢调试、打断点、看变量,但机试系统往往只给你一个基础的编辑器,最多带个简单的命令行调试。这时候,一个趁手的代码模板能帮你省下大量时间。
2.1 VS Code下的C++环境配置建议
热词里有“vscode配置c/c++环境”,这确实是很多新手的第一道坎。我自己常年用VS Code做机试练习,配置上有一个原则:能自动化的就别手动。用微软官方的C/C++扩展套装,配合Code Runner插件,基本能做到F5编译、Ctrl+Alt+N直接运行。不过要注意,机试系统多半是Linux环境,编译器是g++,做本地练习时最好也统一用g++,别用MSVC的语法特性。
具体配置上,.vscode/tasks.json里设置好编译命令,我建议直接写死标准:
{ "version": "2.0.0", "tasks": [ { "label": "C++ Compile", "command": "g++", "args": [ "-std=c++17", "-O2", "-o", "${fileDirname}/${fileBasenameNoExtension}", "${file}" ], "group": "build" } ] }这里有个很关键的细节:加-O2编译选项。机试系统的评测机开O2几乎是标配,有些代码本地跑得好好的,一到OJ上就变快或变慢,多半就是优化级别不一致导致的。提前在本地就用O2,能把这种意外降到最低。
2.2 属于你自己的“机试秒开模板”
我每次参加机试前,都会把一段代码模板拷到手边,不依赖任何专业库,纯手写。这段模板不长,但能保证我在写题时不用重复造轮子:
#include <bits/stdc++.h> using namespace std; const int N = 100010; // 数组大小按题目上限开,留出余量 int a[N], pre[N]; // 常用数组,全局变量自动零初始化 int main() { ios::sync_with_stdio(false); // 关闭C与C++流同步 cin.tie(nullptr); // 解除cin与cout的绑定 int n; cin >> n; // 主逻辑 return 0; }bits/stdc++.h这个头文件在g++环境里能用,但在MSVC里不行。如果你不确定机试环境,退一步老老实实#include <iostream>、#include <vector>、#include <algorithm>,也就多敲三行字,换来的是不怕环境出幺蛾子。至于取消同步那两行,这是C++机试的“点火开关”,能让你在大量输入时快一个量级,一定要背下来。
3. 四道题的逐个拆解:从读题到AC的完整心路
明确了环境和模板之后,接下来是重头戏:把t100到t103四道题按机试实战标准过一遍。因为题目原题没公开,我根据题号和常见题库的特征,还原了四道最具代表性的模拟题。你可以把它当成我在考场上的“脑内直播”,每一步为什么这么想、代码为什么这么写,都有完整解释。
3.1 t100——签到题:质数判断的考场最优解
签到题通常不难,但有个隐藏陷阱:它会把时间复杂度卡在暴力的临界点上。常见的t100题目形态是“输入一个正整数n,判断它是不是质数,如果是输出Yes,否则输出No”。看起来简单,直接枚举2到n-1不就行了?如果n很大,这种暴力做法会直接超时。
考场标准解法是枚举到√n:
#include <bits/stdc++.h> using namespace std; bool isPrime(int x) { if (x < 2) return false; if (x == 2) return true; if (x % 2 == 0) return false; for (int i = 3; 1LL * i * i <= x; i += 2) { if (x % i == 0) return false; } return true; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; while (cin >> n) { cout << (isPrime(n) ? "Yes" : "No") << '\n'; } return 0; }这里几个细节:为什么判断到√n就行?因为如果n有一个大于√n的因子,那它必然伴随一个小于√n的因子。为什么先判偶数?因为偶数因子能直接排除一半的循环次数。为什么用1LL * i * i?因为i * i在int类型下可能溢出,变成负数导致死循环,这种坑我踩过一次,再也不敢裸写i*i。
顺带说一句,while (cin >> n)这个写法在机试里非常实用。很多题不告诉你输入有几组数据,你就得一直读到EOF。不要用while(true)加判断,那个容易炸,用流对象的布尔转换最安全。
3.2 t101——区间查询:前缀和的经典应用
第二题大概率是序列处理。“给定长度为n的数组,m次询问,每次给出L和R,求区间[L,R]的和”,这种题就是前缀和的教科书现场。暴力的做法是每次询问从L加到R,复杂度O(mn),当n和m都达到10^5时,百万级别的操作在OJ上直接超时。
前缀和的核心思想是把“区间和”转换成“两个前缀和的差”。先预处理出一个数组pre[i],表示前i个元素的和,那么[L,R]区间和就是pre[R] - pre[L-1]。一次预处理O(n),每次查询O(1),总复杂度直接降到O(n + m)。
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<long long> a(n + 1), pre(n + 1, 0); for (int i = 1; i <= n; i++) { cin >> a[i]; pre[i] = pre[i - 1] + a[i]; } while (m--) { int L, R; cin >> L >> R; cout << pre[R] - pre[L - 1] << '\n'; } return 0; }注意我把pre和a都声明成了long long。这是机试最常见的一个大坑:当n=10^5、每个a[i]=10^9时,区间和最大能到10^14,int类型根本装不下。很多人的代码逻辑写得完全正确,就因为int溢出错了几组样例,特别冤。
还有一个小细节:数组下标从1开始,而不是从0开始。为什么?因为前缀和公式需要用到pre[L-1],如果下标从0开始,你得单独处理L=0的边界情况。从1开始的话,pre[0] = 0天然就是对的,省掉一堆if判断。这是写前缀和、差分的通行写法,记下来能少很多焦虑。
3.3 t102——单调栈:下一个更大元素与经典变式
从这题开始,题组进入进阶区间。t102的常见形态是“给定一个整数数组,输出每个元素右边第一个比它大的元素,没有则输出-1”。朴素的做法是双重循环,对每个元素向右扫描,复杂度O(n²)。当n=10^5时,这是绝对不能接受的。
单调栈的思路非常巧妙:维护一个栈,栈里的元素从栈底到栈顶保持单调递减(对“下一个更大元素”而言)。从右往左遍历数组时,栈顶元素就是在当前元素右侧最近的候选答案。如果栈顶比当前元素小,说明它永远不可能是答案了,直接弹出。最后栈顶就是答案,再把当前元素入栈。
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<int> a(n), ans(n, -1); stack<int> st; // 栈里存下标 for (int i = n - 1; i >= 0; i--) { while (!st.empty() && a[st.top()] <= a[i]) { st.pop(); } if (!st.empty()) ans[i] = a[st.top()]; st.push(i); } for (int i = 0; i < n; i++) { cout << ans[i] << (i == n - 1 ? '\n' : ' '); } return 0; }为什么是“从右往左”而不是“从左往右”?因为“右边第一个更大的元素”这个语义天然适合从右往左维护单调性。当你在位置i时,右边的情况已经全部处理完,栈里留下的都是当前元素的“候选答案”,弹出的元素都是“不会再被用到”的次品。
输出格式也是一个易错点。很多OJ要求每个输出后用空格分开,但行末不能有多余空格,否则会判PE(Presentation Error)。我上面用三目运算符在最后一个元素时换行,这在机试里是标准姿势。
3.4 t103——数学题:快速幂的分治之美
压轴题通常是数学或者更复杂的数据结构题。t103我选一个最经典的数学考点来拆解:计算a的b次方模p,其中a、b、p都可能达到10^9甚至更大。直接循环乘b次显然不可行,这是快速幂应用的经典场景。
快速幂的核心思想是基于指数的二进制分解。比如计算3^10,先不直接乘10次,而是算3^2=9、再算3^4=9²=81、再算3^8=81²=6561,然后10的二进制是1010,所以3^10 = 3^8 * 3^2。这样乘法次数从10次降到了4次,复杂度从O(b)变成O(log b)。
#include <bits/stdc++.h> using namespace std; long long fastPow(long long a, long long b, long long p) { long long res = 1 % p; while (b) { if (b & 1) { res = res * a % p; } a = a * a % p; b >>= 1; } return res; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); long long a, b, p; while (cin >> a >> b >> p) { cout << fastPow(a, b, p) << '\n'; } return 0; }这段代码值得背下来的不只是循环逻辑,还有两个细节。第一个是res = 1 % p的初始化,这是为了处理p=1的特殊边界。当模数是1时,任何数的结果都是0,1 % p直接得到0,不会出错。第二个是每一步乘法后立即取模,防止中间结果溢出long long。如果你先乘完再取模,a和b都达到10^9的规模,乘积10^18已经顶到long long的上限,差一点就炸了。
从实战角度讲,t103这种题如果你没思路,我建议你千万别空着。快速幂模板背下来后,很多变式题(如矩阵快速幂、斐波那契数列的O(log n)解法)都能套,一道题可能就是10分到20分的差距。
4. 高频报错与调试实录:那些年我们踩过的坑
机试最痛苦的不是不会做,而是“我觉得我写得对,OJ非要WA”。热词里那串c#调用c++出现access violation c0000005和vscode c++所有函数变量都没办法跳转,背后其实是同一类问题:环境或内存出了问题,但不一定是算法的问题。这一章我把自己见过的、踩过的坑集中整理一遍,当成一份速查表给你。
4.1 段错误与c0000005访问冲突的排查清单
在Windows上跑C++程序,遇到access violation c0000005,翻译成Linux下的说法就是段错误。说白了就是程序访问了不属于自己的内存。常见的四大诱因:
- 数组越界:创建了
int a[100],结果循环访问到了a[100]甚至更远,这在循环边界写错时极易发生。 - 字符串操作越界:
char s[10]硬要存11个字符,最常见就是strcpy溢出。 - 空指针/野指针解引用:指针指向了已释放的内存,或者压根没初始化就拿来用。
- 递归栈溢出:递归层数太深,把程序栈撑爆了。这在某些暴力dfs里特别容易出现。
排查方法也很有序:先看报错行号,定位到具体访问哪一块内存,然后往上追这个变量在哪分配、在哪写入。如果崩在陌生地点,十有八九是前面某处数组越界改坏了内存。调试老手有个土办法,在关键循环里加assert,比如assert(i >= 0 && i < n),跑一遍立刻能定位。
4.2 为什么本地能过、OJ上就是WA?
这是机试新人最崩溃的时刻。我在本地测试样例全对,交上去上来就WA,连个提示都没有。这里我总结过几类“本地与OJ不一致”的原因:
第一类是编译器版本差异。本地用MSVC,OJ用g++,bits/stdc++.h在MSVC上不存在,编译直接挂。还有一些代码写成for (int i = 0; i < n; i++),循环变量i在循环外使用,这在某些C++标准下是未定义行为,编译器可以自由发挥。
第二类是数据范围没读全。题目说n最大10^5,但有个隐藏条件是“所有整数的绝对值不超过10^9”,你偷懒全用int,等到大样例直接溢出。我记得第一次做前缀和的题就是用int,WA了五回才被朋友点醒,气得我对着屏幕翻了五分钟的白眼。
第三类是读入输出问题。如果输入有空格和换行混合,cin能自动跳过空白,但如果你混合用scanf和cin且没同步关闭,缓冲就打架了。我现在的习惯是:要么全部scanf/printf,要么全部cin/cout,绝不混着来。
第四类是答案格式错误。看起来不伤大雅,但OJ对格式是零容忍的。多余的逗号、缺一个空格、末尾多了一个空行,都可能被判PE甚至WA。所以写输出时,先把样例的格式抠得一模一样。
4.3 一个没通配的调试利器:用大数据断言纠错
如果你已经排除了上面所有问题,还是不知道哪里错了,这时候有个高级技巧:构造一个可以暴力验证的小规模数据,然后跑一遍你的高效算法,再跑一遍暴力算法,对比结果。
// 临时写一个暴力算法验证答案 #include <bits/stdc++.h> using namespace std; long long brute(vector<int>& a, int L, int R) { long long s = 0; for (int i = L; i <= R; i++) s += a[i]; return s; } // 然后随机造数据,跑两个函数比对这个“对拍”思路是竞赛选手的基本功,但在机试准备中很少人用。性价比极高:它能让你在完全不知道错在哪的情况下,通过二分定位逻辑错误的代码段。做法很简单,用rand()生成随机数组,分别调用暴力算法和高效算法,一旦结果不一致,把输入数据缩小,再逐步定位到具体哪一步算错。
5. 机试冲刺的选题策略与心态管理
聊完具体的题目和调试,最后说说这四道题背后的备考策略。t100到t103这套题组给了我们一个很清晰的复习大纲:签到题考基础语法、第二题考常用套路、第三题考数据结构和思维、第四题考数学与综合能力。对应到你的复习计划,应该按照这个优先级去铺开。
5.1 如何用“三遍刷题法”消化一套题组
我不推崇“题海战术”,尤其是机试前两周,盲目刷题只会让你在考场上一看到变式题就发怵。我自己的刷题节奏是“三遍法”:
第一遍,拿到一套题按真实考试计时,做完对答案,记录哪些题卡壳、花了多久。第二遍,把每一道题从头到尾重新推导一遍,不看答案,纯粹自己写,然后用不同的实现方式写一遍(比如前缀和用循环写、用STL的partial_sum写)。第三遍,把所有题归类总结,写进自己的“题型→算法模板”对照表里。
这个对照表长什么样?随便举个例子:看到“区间和”想到前缀和;看到“右边第一个更大的数”想到单调栈;看到“a的b次方模p”想到快速幂;看到“图中最短路径”想到Dijkstra和SPFA。机试本质上就是“题型识别 → 模板套用 → 细节校正”三步走,第三遍刷题的作用就是强化第一步。
5.2 机试最后三天的热身与心态调整
机试前三天不建议再做新题了。我当时做了一件我认为非常有用的事:把常用模板(快读快写、前缀和、差分、单调栈、并排快速幂、DFS/BFS模板)全部手写一遍,然后存成一个文档,反复看。不是为了背代码,而是为了让自己在考场上就像条件反射一样,看到题就知道该调出哪一段逻辑。
还有一个很多人忽略的事项:提前进考场系统,熟悉编译环境,建一个空白项目,编译一个“hello world”,确保自己的账号和编译器配置完全正常。这些看似不起眼的动作,能直接决定你前半小时的心态。机试考的不只是算法,更是你能不能在最紧迫的3小时里保持节奏。
5.3 考场上的一分钟检查清单
交卷前,按下面这个清单过一遍,能拦下80%的低级失误:
- 数组开的是不是题目上限的1.2倍以上?
- 有没有用
long long的地方被我写成了int? - 循环边界是
< n还是<= n? - 输出格式里行末空格处理了吗?
- 有多组输入时,循环正确退出吗?
- 注释里有中文或特殊符号,影响编译吗?
可别小看最后一条。早期我用Windows记事本写C++,保存成了ANSI编码,里面敲了中文注释,拿到Linux上一编译,直接乱码报错。后来学乖了,要么全英文注释,要么干脆不注释,把思路写在草稿纸上。
6. 从t100-t103到正式机试:最后几点经验
这套题组练完以后,你对机试应该有一个比较立体的感知了。但我还是想多啰嗦几句,因为这几条经验是我自己拿真金白银换来的。
第一条:永远不要试图“完美通关”。t103这种压轴题,能做出来固然好,做不出来也完全不影响你通过机试。大多数机试的及格线是“做对前两题再加半道第三题”,把这个目标定清楚,你的压力会小一半。
第二条:多练手写代码,别依赖自动补全。考试环境不一定有语法高亮和智能提示,你平时在IDE里写得太舒服,一到考场上连#include <vector>都要想半天,这就很危险。我建议每周至少有两套题是完全不用任何补全功能、纯手敲完成的。
第三条:交卷前一定留出5分钟,把每道题的样例重新跑一遍。很多时候你对代码做了微调,比如把cout换成printf,结果因为格式冲突样例都过不了。这种“自杀式失误”在考场上年年都有,我不希望你是其中之一。
说实话,机试这道坎没有想象中那么难,它更像是一个“熟练度测试”——你把常见题型和模板练到肌肉记忆,考场上就不会慌。这套t100-t103的题组恰好覆盖了最核心的几类题型,照着拆解思路去练,后续遇到再花哨的变式题,你也能一眼看穿它在考什么。