2025年3月底,海大计算机考研复试的机试考完,我出考场第一件事就是找地方把题目记下来。今年这套题整体难度比往年稍微温和一些,没有出现那种需要半小时才能想出来的偏题怪题,但“看起来简单”恰恰是最容易丢分的情况:边界条件、输入格式、内存限制,每一处都能卡掉一批人。这篇帖子把四道真题按考场顺序整理出来,每道题都会拆解解题思路,给出可直接跑通的 AC 代码,并标注我当时为什么选择这个做法。如果你正在备考海大计算机的复试机试,或者想找一套难度接近的模拟题练手,这篇文章可以直接拿来用。已经有一定刷题量的朋友,重点看边界处理和复杂度估算就行,我在这两块踩过不少坑。
1. 考题总览:考点分布与复习优先级
1.1 2025年机试四道题的核心考点
今年的机试一共四道题,两个小时,学校自己的OJ提交,按通过样例的比例给分,不是一题定生死。这个机制很重要,后面我会专门说。
先把四道题的核心信息整理成一张表:
| 题号 | 题目主题 | 核心考点 | 建议用时 | 难度评估 |
|---|---|---|---|---|
| 1 | 日期计算 | 模拟、闰年判断、数组预处理 | 10-15分钟 | 简单 |
| 2 | 矩阵最大连通区域 | DFS/BFS、连通块、坐标边界 | 20-25分钟 | 中等 |
| 3 | 物资搬运最大价值 | 0/1 背包、一维 DP 优化 | 20-30分钟 | 中等 |
| 4 | 有序数组区间查询 | 二分查找、边界设计、排序 | 25-35分钟 | 中等偏上 |
从这个分布能明显看出,海大的机试没有追求高深算法,考的是“把事做对”的基本功。日期模拟是计算机专业学生大一就会的题目,为什么复试还会出?因为它能把“我会写代码”和“我能把代码写对”这两类人区分开。矩阵连通性则是数据结构里图的遍历最直接的落地场景,考的是 DFS 和 BFS 的熟练度。第三题是动态规划里最经典的入门模型,第四题是二分查找的边界细节,这两类题目约等于机试的必考题。
所以准备海大复试机试,优先级很明确:模拟题、搜索、动态规划、二分查找,这四个方向吃透,基本就能覆盖一大半题目。字符串处理、排序、简单的数据结构题(栈、队列、优先队列)可以排在这之后。数学类的高难题目比如数论、组合数学,复试出现的概率不高,备考时间紧张的话可以先放一放。
1.2 时间分配策略
两个小时四道题,说起来平均每道题半小时,但实际按难度分配更合理。我自己的策略是“先易后难、保障保底分”:首先写第一题日期计算,即便很简单,也立刻拿下一道题的分,给自己建立信心。然后做第二题矩阵连通,因为搜索题只要想清楚递归边界就稳了。第三题背包和第四题二分,分配给它们的时间比例最大。
这里有一个容易被忽视的心理因素:机试和笔试不一样,看到倒计时在走,人会不自觉地紧张。如果一上来就啃难题,卡住几十分钟后心态容易崩,后面简单的题也拿不稳。先做简单题,至少在心理上先稳住节奏。如果某道题超过了预设时间还没思路,我建议先跳过,把所有题目的暴力写法都过一遍,拿到部分分之后再去优化。因为海大OJ按测试点给分,暴力解能过小数据就算赢了,总比一道题死磕到超时要好。
1.3 环境与提交平台注意点
海大的机试环境用的是自己学校的OJ,提交界面楚,允许的语言是 C/C++、Java 和 Python。我的个人建议是,除非你平时对 Java 或 Python 非常熟悉,否则优先选 C++。原因很简单:复试机试的测评规则通常按标准输入输出进行,C++ 的语法在竞赛场景里兼容性最好,STL 能帮你省下大量手写数据结构的时间,而且网上能找到的题解代码绝大多数也都是 C++ 写的,后面复习对照也方便。
另外一个很多人到考场上才发现的细节是编译标准。部分OJ默认的编译器参数是 C++14 甚至 C++11,如果你在本地用了 C++17 的特性,比如结构化绑定或者某些新的 STL 接口,提交之后可能直接编译失败。稳妥的做法是:不用太高版本的语法,尽量用 C++11 也能跑的写法。代码里不需要用到什么高深的模板技巧,老老实实写,反而最安全。
2. 四道真题的解题思路与 AC 代码
这一部分我会按照考场上的顺序,把每道题尽可能完整地复述出来。题目描述是我考后根据自己的记忆整理的,具体表述可能存在出入,但核心考点和数据范围基本是一致的。
2.1 日期计算:最容易在闰年上丢分
第一题题目大致是这样:输入一个日期,格式为“年 月 日”,输出这个日期是当年的第几天。数据范围是年份在一千到三千年之间。
看题意觉得简单,但很多人在这个地方丢了分。丢分的原因不是不会算,而是没考虑到两个细节:第一,闰年的判断条件是“能被400整除,或者能被4整除但不能被100整除”,这个条件不能写反。第二,判断是否多一天时,必须只在已经过去的二月份之后才生效,也就是只有月份大于二的时候才能加一天。
我当时采用了预处理天数数组的方式:
#include <bits/stdc++.h> using namespace std; bool isLeap(int y) { return (y % 400 == 0) || (y % 4 == 0 && y % 100 != 0); } int main() { int y, m, d; while (cin >> y >> m >> d) { int monthDays[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int sum = 0; for (int i = 1; i < m; i++) { sum += monthDays[i]; } if (m > 2 && isLeap(y)) { sum++; } sum += d; cout << sum << "\n"; } return 0; }这道题用 while 循环持续读入是因为考试中的测评数据通常是多个测试点一次性输入,用这种写法能适应后续所有测试点。我把月份天数数组初始化为下标 1 到 12,下标 0 空闲不用,这样从 1 月加到 m-1 月时逻辑更直观,不需要再处理“数组下标和月份错一位”的问题。这个小习惯在写代码比较着急的时候特别管用,能省掉很多脑子里的换算。
过了闰年这个坎还不够,还要注意数据范围。年份上限是3000年,int完全存得下,不存在溢出问题。但是如果你习惯用字符读入“2025-03-15”这种带横杠的格式,一定要回忆一下OJ到底输入的是空格还是符号。看到题目描述里明确写了“年 月 日”,就用最简单的 cin 读三个整数,别给自己额外增加解析负担。
2.2 矩阵最大连通区域:DFS 还是 BFS?怎么选才稳
第二题是个矩阵搜索问题:输入一个 n 行 m 列的 01 矩阵,1 表示陆地,0 表示水域,只能上下左右四个方向走,不能斜着走,要求输出最大的陆地连通块面积。n 和 m 都不超过 100。
这种题在力扣上叫“岛屿最大面积”,在海大的机试题库里属于“高频题”。考场上用 DFS 的人很多,但很多人在递归边界上出了问题。我用的还是最经典的写法:
#include <bits/stdc++.h> using namespace std; const int MAXN = 105; int n, m; int grid[MAXN][MAXN]; bool vis[MAXN][MAXN]; int dx[4] = {-1, 1, 0, 0}; int dy[4] = {0, 0, -1, 1}; int dfs(int x, int y) { vis[x][y] = true; int area = 1; for (int k = 0; k < 4; k++) { int nx = x + dx[k]; int ny = y + dy[k]; if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; if (vis[nx][ny] || grid[nx][ny] == 0) continue; area += dfs(nx, ny); } return area; } int main() { cin >> n >> m; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { cin >> grid[i][j]; } } int ans = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (!vis[i][j] && grid[i][j] == 1) { ans = max(ans, dfs(i, j)); } } } cout << ans << "\n"; return 0; }这道题最关键的地方就是越界检查和访问标记。递归进去之前必须先判断新坐标是否在矩阵范围内,很多新手会把判断顺序写反,先访问数组再判断边界,等于数组越界了还在继续跑,结果要么得到错答案要么直接 Runtime Error。我见过有同学在本地跑得好好的,提交上去崩掉,就是因为编译器对越界的处理方式不同。
DFS 和 BFS 到底选哪个?我的看法是:矩阵规模小的时候两者都能过,但如果你对递归没把握,或者矩阵深度可能很大,就用 BFS。比如矩阵是 100×100,DFS 递归深度在极端情况下会非常深,虽然一般不会爆栈,但如果题目把矩阵扩到 1000×1000,递归就不稳妥了。今年这题 n、m 只有 100,DFS 完全没问题。考场上选自己最熟悉的那个,不要临时换写法,一个好的 BFS 正确率往往比不熟练的 DFS 要高得多。除了搜索本身,这道题还在考验“对每个未访问的陆地都发起一次搜索”这个外层循环,遍历的时候同时更新最大值,别把全局变量弄混。
2.3 物资搬运:0/1 背包的换皮题
第三题是一道典型的动态规划题。题目背景改成了运输物资:有一个载重上限为 V 的交通工具,面前有 n 种物资,每种物资只有一个,都有自己的重量 w[i] 和价值 v[i],要求在不超过载重上限的前提下,拿走尽可能大的总价值。数据范围上,V 不超过 10000,n 不超过 100。
这道题的“裸题版本”就是 0/1 背包。在复试机试里出 0/1 背包的频率非常高,因为它是理解动态规划的基础模型,而且一维滚动数组的优化恰好是很多学生没写熟练的地方。
我的 AC 代码如下:
#include <bits/stdc++.h> using namespace std; int main() { int V, n; cin >> V >> n; vector<int> weight(n), value(n); for (int i = 0; i < n; i++) { cin >> weight[i] >> value[i]; } vector<int> dp(V + 1, 0); for (int i = 0; i < n; i++) { for (int j = V; j >= weight[i]; j--) { dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } cout << dp[V] << "\n"; return 0; }这里有个很关键的细节:内层循环必须从后往前遍历,也就是从 V 到 weight[i]。如果用正向遍历,同一个物品会被重复使用多次,那就成了完全背包,答案完全不对。这个点是我当年学背包时最容易犯的错,后来养成了习惯:看到“每个物品只有一个”马上写倒序循环,不需要再思考。
还有一种做法是开二维数组 dp[i][j],表示前 i 个物品在容量 j 下的最大值。二维写法虽然空间大一些,但逻辑上更好理解,初次学动态规划的人用二维更容易避开 bug。但问题是 V 到了一万甚至十万量级,二维数组可能开不下。这次题目 V 只有一万,二维也勉强能过,但我还是直接写了滚动数组,因为时间有限,而且滚动数组的写法在复试这种时间紧张的环境里更省空间,出问题的概率更低。至于说是“每个物品只有一件”,要仔细读题,如果题目改成“每种物品数量不限”,那就直接把循环方向改成从 weight[i] 到 V即可,模型依然不变。你能根据题目描述切换这两个方向,机器测试的时候就不会因为模型判断失误丢分。
2.4 有序数组区间查询:手写二分才是得分保障
第四题看起来是最基本的二分查找,但写完整还是有难度。题目大意是:输入一个长度为 n 的有序整数数组(可能会有重复元素),然后有 q 次查询,每次给一个目标值 x,要求输出 x 在这个数组中出现的起始位置和结束位置,如果没出现过就输出提示信息。位置从 0 开始计数。n 和 q 都在十万级别。
这道题考察的本质是 lower_bound 和 upper_bound 手写能力。在 C++ 里直接调用 STL 的 lower_bound 和 upper_bound 是可以过题目的,但我建议考场上还是自己手写一遍。为什么?因为直接调用会有两个问题:第一,STL 返回的是迭代器,你需要小心处理迭代器减数组下标这样的细节;第二,万一考场上的编译器版本较旧或者环境配置有些特殊,你心里必须清楚底层逻辑,才能快速定位到问题。
代码这样写:
#include <bits/stdc++.h> using namespace std; int lowerBound(vector<int>& a, int target) { int l = 0, r = (int)a.size(); while (l < r) { int mid = (l + r) / 2; if (a[mid] < target) l = mid + 1; else r = mid; } return l; } int upperBound(vector<int>& a, int target) { int l = 0, r = (int)a.size(); while (l < r) { int mid = (l + r) / 2; if (a[mid] <= target) l = mid + 1; else r = mid; } return l; } int main() { int n, q; cin >> n >> q; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; sort(a.begin(), a.end()); while (q--) { int x; cin >> x; int L = lowerBound(a, x); int R = upperBound(a, x); if (L == R) { cout << "NOT FOUND\n"; } else { cout << L << " " << R - 1 << "\n"; } } return 0; }这套二分模板我建议背到形成肌肉记忆,因为边界情况实在太容易错了。lower_bound 找的是第一个不小于 target 的位置,所以在 a[mid] < target 时移动左边界,其余情况移动右边界。upper_bound 找的是第一个大于 target 的位置,所以在 a[mid] <= target 时移动左边界。两个模板的区别就那一行判断符号,写混了结果全乱。
再强调一下初始区间。左边界是 0,右边界是 n,不是 n-1。采用左闭右开区间的好处是:当 target 比数组中所有元素都大时,最终 L 会停在 n 的位置,这个位置虽然越界于数组下标,但刚好表示“数组中存在数的位置”,配合后面的 R-1 也能处理。这个写法比“左闭右闭”的模板更不容易出现死循环,推荐记这一套。最后输出时是输出起始位置和结束位置,起始位置就是 L,结束位置是 R-1,不是 R。这就是最容易看错的地方,你千辛万苦求出了 upper_bound,结果输出时把 R 直接打出来,样例一过感觉对了,后面的隐藏点全挂,非常可惜。
3. 解题思路背后的通用方法论
看完四道题的具体写法后,我想跳出题目本身,聊一聊我在准备复试和考场实战中总结出来的方法论。这部分不局限于某个具体题目,而是适用于大多数复试机试。
3.1 拿到题目后先做这三步
我给自己定了一个规则:不管题目多简单,前五分钟不写代码,先把三件事做完。第一,画数据范围,确认时间复杂度可以接受。第二,找边界情况,比如空数组、最大值、最小值、负数和零。第三,确定输入输出格式,尤其是有没有多组输入,是不是每个测试点之间要空行。这几年机试丢分,绝大多数不是算法不会,而是在这三步上出了问题。
拿第一题举例,看到年份范围是1000到3000,你至少应该立刻意识到要处理闰年。看到矩阵边长不超过100,你才知道 DFS 不会爆栈。看到 n 和 q 是十万,你才知道必须用 O(n) 或 O(logn) 的解法,而不是暴力遍历一遍十万乘十万一千万的复杂度虽然不算离谱,但如果你再叠加查询次数就危险了。数据范围是出题人埋下的线索,它直接告诉你应该用哪种复杂度的算法。
还有输出格式,很多人不重视。题目要求每组输出占一行,就始终输出一行,不要说什么“为了友好在末尾多加一个换行”,在OJ眼里多一个空行就是格式错误或 Presentation Error。虽然是按测试点给分的机制,格式错误也会导致整组测试点计零,等于白写。
3.2 复杂度与时限:2秒到底能跑多少
很多考生对复杂度没有直观感受,只知道“大O越小越好”。这里我给一个经验值:在一般OJ上,2秒时限内,C++大概能跑 10 的 8 次方次简单运算。也就是说,如果数据范围是十万,O(n^2) 就是十的十次方,一定会超时,必须想办法优化到 O(n) 或 O(nlogn)。如果数据范围是一百,O(n^3) 也就是一百万,完全没问题。根据数据范围倒推算法,是机试里最实用的一招。
就今年四道题而言,第一题直接 O(n) 模拟,一个月份数组加一次判断结束。第二题每个格子最多访问一次,整体 O(nm),不超过一万的规模,跑起来飞快。第三题是 O(nV),也就是 100 乘 10000,一百万的运算量,随便跑。第四题排序 O(nlogn),每次查询 O(log n),十万级别数据也就是百万量级的总操作,同样没有压力。
复杂度的另一个隐藏作用是帮你判断要不要加预处理。比如第三题如果 V 扩大到十万,那么 O(n*V) 就是一千万,仍然可以接受,不需要额外优化。但如果 V 扩大到一千万,就必须考虑单调队列优化或者改成其他模型了。在考场上你不需要把算法优化到极限,只需要保证它能在时限内跑到最终答案。
3.3 输入输出细节就是隐形分
我刷题时有一条准则:主函数里不要夹杂业务逻辑以外的复杂代码,能只写一个循环就读完所有输入,绝不搞阉割。复试机试不是工程项目,不需要封装多么完整,但输入输出部分往往是最容易出低级错误的重灾区。
第一,尽量用 cin 加 endl 吗?不对。endl 会强制刷新缓冲区,在输出量大的时候明显拖慢速度。程序结束前只要有一行输出,用 “\n” 就够,别用什么 std::endl。第二,多组输入时用 while(cin >> ...) 处理,直到 EOF 自动结束,这比手动计数要稳。第三,如果遇到字符串中含空格的情况,用 getline但记住之前要配合 cin.ignore 清掉缓冲区里的换行,不然第一行读出来是空字符串。今年没有特别复杂的字符串题,但这个细节复试里每年都有可能出现。
另外有个机试特有的神坑:输入矩阵时如果中间没有空格,读入的是 “10101” 这样的字符串。很多人直接 for 循环 cin >> grid[i][j],导致第一个字符只读取了一行开头,后面的数据全错位。遇到这种情况,先把每一行当作字符串读进来,再用 s[j] - '0' 赋值给 grid[i][j]。我见过太多同学在矩阵题上栽在这里了,所以单独拿出来说。
4. 机试现场常见的坑与排查技巧
这节是我自己考场上和平时训练里踩出来的经验,每条都对应一个非常具体的丢分场景。
4.1 我踩过的三个坑
| 坑点 | 表现 | 解决办法 |
|---|---|---|
| 闰年判断写反 | 1900年被当成闰年,多算一天 | 用默写方式记住判断公式,不要现场推 |
| DFS 忘记越界判断 | 本地随机数据正常,提交后部分测试点崩 | 先判坐标范围再访问数组,顺序不能反 |
| 二分右边界写成 n-1 | 查询最大边界值时死循环或返回错误 | 坚持左闭右开区间,右边界初始化为 n |
闰年这个坑我说了很多遍,但还是有人前赴后继地掉进去。一个原因是平时本地测试的数据都是常见的 2024、2025 这种年份,碰不到 1900 这种世纪年。另一个原因是人一到考场就紧张,越熟练的东西越容易手滑。我的解决办法是考前把这些高频判断条件整理成一页纸,在进考场前反复看几遍,形成无意识记忆。考场里题目一变,条件反射就出来了。
DFS 越界的顺序问题也值得再说一遍。标准写法是先判断新坐标是否小于0或大于等于边界,再用这个坐标去访问数组。如果你先访问了再判断,C++里虽然不一定会立刻崩溃,但读到的可能是内存里的临界值,导致多统计或少统计连通块,答案时对时错。这种错误最难受,因为它不是必现的,debug 无从下手。第二题的代码里我特意把 continue 放在数组访问之前,就是这个原因。
4.2 本地通过但提交报错的排查顺序
如果你碰到“我在本地跑得好好的,提交上去全错”,别急着怀疑OJ有毛病,老老实实按顺序排查。首先查格式:是不是多了空格、多了空行。然后查输入:是不是用 scanf 读字符串遇到了中文全角空格,或者数据有多组而你只处理了一组。再查变量初始化和数组大小:用的是变长数组还是固定数组,数组上限是否恰好是题目给的最大值。最后查算法复杂度:是否在某个隐藏大数据的测试点上超时。
我考场上调试的时候有个习惯,会把所有输出都临时打印到文件或者备注起来,通过测试后一点一点恢复。有一次我发现结果整体差了一天,最后定位到是第一题因为误把闰年加一放在了所有二月的处理之前。这种问题看一眼正确代码和人脑推断就知道,但如果用拆分的调试输出,一次次对着样例输出,几分钟就能定位。
另外,OJ报错类型也有指示意义。Compile Error 一般就是语法版本或缺少头文件。Runtime Error 多半是数组越界或除零。Time Limit Exceeded 说明算法复杂度太高或者死循环。Memory Limit Exceeded 说明数组开的过大或者递归层数太深。把这些错误类型和对应原因记牢,看到报错就能直接定位方向,不用瞎猜。
4.3 给下一届考生的三条实战建议
如果让我给明年备考的同学说点实在话,第一条是机试前必须做至少两套完整的模拟题,卡时间,用和考场一样的输入输出方式。很多人平时刷题是一道一道刷,每题不限时,这样练不出考场的时间控制感。模拟三到五次,你就会对自己两小时能写完几道题有准确预期。
第二条是背熟一套模板库。不用多,但要把日期、搜索、背包、二分、排序这些高频考点的代码原样默写过一遍。所谓“背熟”,不是看到题能想起思路,而是手放在键盘上能直接流畅打出来。机试和笔试最大的不同就在这里,笔试你写思路就行,机试每个字符都要自己敲。
第三条是把自己的代码留在本地多存一份。海大OJ的文件提交偶尔会有网络延迟或者误操作,考完发现题目没提交成功才是真的惨。我考场上每做完一题就把代码复制进物理机自己的U盘或者云端笔记里,哪怕提交界面出了意外,也能随时找回。机试不只是考察你写代码的能力,还包括你对基础流程的掌控力。
题目难度再高,拉开差距的往往不是某个“灵光一闪”的算法,而是这些看起来琐碎却决定成败的细节。日期题忘了闰年、背包题方向写反、二分右边界差一位,每一个都是可以提前避免的低级失误。刷真题的价值不在于记住题面,而是把这些低级失误在考前全部暴露出来,考场上才能做得干净利落。
最后再分享一个我这次考试用到的检查技巧:所有题写完之后,不要急着交,把每个代码里的边界条件手动替换成极端值,在脑子里模拟一遍。比如日期题的年份换成1900,二分查询的目标换成数组最大值减1,矩阵搜索的起点放在四个角。这比无意义地反复看代码有效得多,也是我这么多年机试下来觉得最值得养成的习惯。