很多同学问我东华大学机试怎么准备,我通常都会先让他们把手头那套“2023东华大学机试OJ进阶版1-81题”从头到尾过一遍。这套题不是入门题库,而是很多保研生和考研复试党刷了都说“很有分量”的进阶题单,覆盖的知识点很广,难度也从基础语法一路延伸到了动态规划和图论。你能不能在机试里稳住心态、稳定输出,很大程度上取决于这套题你刷得够不够透。这篇文章我就把这81道题当成一个整体来拆:考什么、怎么刷、哪些题值得反复做、提交报错到底怎么查,一条龙讲清楚。不管你是第一次碰OJ的萌新,还是刷到一半卡在瓶颈期的选手,都能在下面找到适合自己的阶段建议和实操方法。
1. 题库背景与题型分布分析
1.1 东华机试OJ进阶版1-81题到底是什么
先解释一下“OJ”这个词。OJ全称是Online Judge,在线评测系统,简单说就是你把代码提交到网站上,系统自动用一大堆测试数据跑你的程序,然后告诉你“Accepted(通过)”或者“Wrong Answer(答案错误)”。东华大学的机试就是在这样的系统上完成的,所以平时刷OJ的过程,本质上就是在模拟真实考场。
“2023东华大学机试OJ进阶版1-81题”是一套由浅入深的题目集,题目编排很有逻辑。1到20题偏基础,但比单纯练语法的入门题要难,会考你字符串处理、简单模拟和基础数学;21到50题开始进入算法核心区,排序、二分、DFS、BFS、背包这类经典问题都会出现;51到81题则是综合题,经常把多个算法揉在一起,是真正拉分的地方。很多同学刷到一半会觉得“怎么突然变难了”,这其实不是你的问题,而是题单设计上就有意设置了梯度:前30题帮你找手感,中间30题逼你掌握算法模板,最后21题考验你灵活组合的能力。
从实际机试结果来看,能把这套题稳定做到60题以上的同学,考场上基本不会出现“题目看得懂、代码写不出”的窘境。所以不管你目标是多少分,这套题都应该作为核心训练材料。
1.2 高频知识点与难度梯度速览
根据我对这套题和同类机试题目的观察,知识点分布大概可以归纳成下面这张表。注意这个比例是经验估算,不是官方数据,但用来指导刷题分配时间已经够用。
| 知识板块 | 预估题目数 | 常见题型 |
|---|---|---|
| 简单模拟与数学 | 16题左右 | 日期计算、进制转换、最大公约数、质数判断 |
| 字符串处理 | 12题左右 | 单词翻转、子串统计、高精度运算 |
| 排序与查找 | 10题左右 | 结构体排序、二分查找、第K大元素 |
| 数据结构 | 12题左右 | 栈模拟、队列应用、链表反转、二叉树遍历 |
| 搜索算法 | 10题左右 | 迷宫最短路径、连通块计数、全排列生成 |
| 动态规划与贪心 | 15题左右 | 背包问题、最长上升子序列、区间调度 |
| 图论基础 | 6题左右 | 最短路径、最小生成树、并查集 |
从难度梯度上看,1到20题是“给你一个明确的步骤,你照着模拟就行”,但要注意边界条件,比如数组越界、除零、字符串末尾换行等;21到50题需要你看出题目背后的算法模型,比如“这题本质上是背包问题”;51到81题则更看重综合设计,可能一道题里既要排序又要二分还得加贪心。刷题的时候一定要清楚自己处在哪个阶段,不要老拿后面的难题打击自己,也不要一直在舒适区里转圈。
2. 刷题路线与高效策略
2.1 刷题前的准备:语言、环境和常用头文件
工欲善其事,必先利其器。我建议机试语言优先选C++,原因很简单:STL能帮你节省大量时间。vector、string、stack、queue、map这些容器在考试时直接调用,比用C语言手写链表和哈希表快得多。如果你只会C语言,也不是不行,但至少要把qsort、字符串处理函数和手动模拟栈搞熟,否则最后几道题会写得很痛苦。
本地开发环境建议装一个轻量IDE,比如Code::Blocks、Dev-C++或者VS Code都行。关键是调试方便,能打断点、看变量。不过要记住,本地编译通过不代表OJ能过,因为OJ的编译器版本、警告级别、运行环境都可能和你本地不一样,所以平时就要养成“用标准C++11语法”的习惯,别用编译器特有的扩展语法。
每次刷题前,把下面这些头文件背到肌肉记忆里:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <vector> #include <string> #include <queue> #include <stack> #include <map> using namespace std;这是最常用的一套组合拳,覆盖了绝大多数题目的需求。另外要熟练掌握两种输入方式:一是固定组数输入,比如先读一个n,然后循环n次处理;二是“读到文件结束为止”,也就是while(cin >> x)或while(scanf("%d", &x) != EOF)。东华机试很多题目是多组测试数据,如果你只写了一组数据的逻辑,那几乎必WA。
2.2 三阶段刷题法:从专项到混合再到全真模拟
我把81道题分成三个刷题阶段,每个阶段目标不同,方法也不同。
第一阶段是“专项突破”,对应1到30题。这一阶段按知识点分组刷,比如今天专门做模拟题,明天专门做字符串题。每做完一道题,不要急着看题解,先自己调,实在卡了1小时再参考别人的代码。目标是建立条件反射:看到“翻转字符串”能马上想到用双指针,看到“日期相差多少天”能马上想到从公元1年开始累加的天数。
第二阶段是“混合训练”,对应31到60题。这时候不再按知识点分类,而是随机抽题,就像考试一样。拿到题目先不急着写代码,花5分钟判断:这题考的是什么?有没有做过类似的题?用什么复杂度可以过?如果能在5分钟内定出思路,这道题就成功了一半。这一阶段会强迫你把知识网络打通,因为你没法靠“上一题是BFS”这种提示来作弊了。
第三阶段是“全真模拟”,对应61到81题。每套模拟题组控制在90到120分钟,严格按考场规则来:不能暂停,不能翻笔记,写完就交,然后用剩余时间检查。这一步不是为了做对多少题,而是训练考试节奏和心态。你会发现,有些题不是不会做,而是时间分配不对,导致会写的题都没时间了。
2.3 错题本到底应该记什么
很多同学刷题就是“AC了就下一题,WA了就改到AC为止”,然后什么也没留下。这种刷法效率很低。我建议每道题都留个简单记录,至少包含四个部分:题目编号和一句话题意;你的初始思路;实际通过时用到的解法;犯错原因。举个例子:
第37题:给一个序列,找最长连续上升子序列长度。 初始思路:排序后比较相邻元素,结果想复杂了。 正确解法:一次遍历,记录当前递增长度和最大长度。 错误原因:没看清“连续”二字,以为是求最长上升子序列。
复习的时候只看错题本,等于把最值钱的错题重新做了一遍。比从头再刷一遍所有题目的性价比高太多。
3. 核心题型拆解与解题模板
3.1 模拟与字符串:高精度加法必须会
模拟题是OJ的“送分题”吗?不一定。模拟题的难点在于把题意翻译成代码,翻译错了就直接WA。比如日期计算,要处理闰年、月大月小,稍微不细心就出错。我建议大家把常用的日期函数和进制转换函数提前封装好,考场上直接调用。
另一种几乎每年都考的字符串重点题是高精度加法。所谓高精度,就是数字太大,连long long都存不下,只能用数组表示。核心思路是把数字的每一位拆开倒序放进整型数组,然后按位相加、处理进位。下面这个模板非常实用:
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 1005; int a[MAXN], b[MAXN], c[MAXN]; char s1[MAXN], s2[MAXN]; int main() { while (scanf("%s%s", s1, s2) != EOF) { int len1 = strlen(s1), len2 = strlen(s2); int n = max(len1, len2); memset(a, 0, sizeof(a)); memset(b, 0, sizeof(b)); memset(c, 0, sizeof(c)); for (int i = 0; i < len1; i++) a[i] = s1[len1 - 1 - i] - '0'; for (int i = 0; i < len2; i++) b[i] = s2[len2 - 1 - i] - '0'; int carry = 0; for (int i = 0; i < n; i++) { c[i] = a[i] + b[i] + carry; carry = c[i] / 10; c[i] %= 10; } if (carry) { c[n] = carry; n++; } for (int i = n - 1; i >= 0; i--) printf("%d", c[i]); printf("\n"); } return 0; }注意三点:数组倒序存储,方便从低位开始加;每轮处理进位,carry要么是0要么是1;最终输出前判断最高位有没有进位。这个模板在大多数OJ上都能直接过。
字符串处理还有一个高频坑:读入时把空白字符也读进去了。用scanf读字符串会自动跳过空格和换行,但读单个字符或读带空格的一行时就要小心。建议处理带空格的行用gets(老OJ)或cin.getline,然后手动遍历。
3.2 搜索题:BFS模板要刻进DNA
搜索题在东华机试里比重不小,尤其是BFS,用来求最短步数、最少操作次数特别方便。BFS的标准套路是:初始状态入队,访问标记,然后循环从队首取出状态,尝试所有可能的下一步,把合法的、没访问过的状态入队。很多同学写BFS容易漏掉“访问标记应该入队时就打上”这个细节,结果同一个点被反复入队,导致超时或死循环。
给你一个可用的BFS模板,以迷宫最短路径为例:
#include <cstdio> #include <cstring> #include <queue> using namespace std; const int MAXN = 105; char maze[MAXN][MAXN]; int vis[MAXN][MAXN]; int dist[MAXN][MAXN]; // 距离/步数 int dir[4][2] = {{1,0},{-1,0},{0,1},{0,-1}}; int n, m; int bfs(int sx, int sy, int ex, int ey) { memset(vis, 0, sizeof(vis)); memset(dist, 0, sizeof(dist)); queue<pair<int,int> > q; q.push(make_pair(sx, sy)); vis[sx][sy] = 1; dist[sx][sy] = 0; while (!q.empty()) { int x = q.front().first; int y = q.front().second; q.pop(); if (x == ex && y == ey) return dist[x][y]; for (int i = 0; i < 4; i++) { int nx = x + dir[i][0]; int ny = y + dir[i][1]; if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; if (maze[nx][ny] == '#') continue; if (vis[nx][ny]) continue; vis[nx][ny] = 1; dist[nx][ny] = dist[x][y] + 1; q.push(make_pair(nx, ny)); } } return -1; // 无法到达 }重点留意:方向数组dir定义成全局;vis和dist可以分开也可以合并成一个二维数组记录从起点到每个点的最短长度,初始化为-1即可。如果题目要求输出路径,还要额外开一个pre数组记录前驱节点,最后递归倒序输出。
DFS的使用场景则更偏向于“求可行方案总数”“全排列”“连通块染色”。DFS递归时要特别注意递归出口和状态恢复,也就是“回溯”。比如生成全排列,每尝试一个数字后要恢复标记,否则后面的分支会受影响。
3.3 动态规划:最长上升子序列和多阶段决策
动态规划是进阶题的重头戏,不懂DP,后面20多题基本没法做。很多同学一听DP就头大,其实可以把DP理解成“填表游戏”:定义一个数组,每个元素表示到当前位置为止的最优解,然后通过状态转移方程一步步推下去。
以最长上升子序列为例,朴素做法是O(n^2):
for (int i = 0; i < n; i++) { dp[i] = 1; for (int j = 0; j < i; j++) { if (a[j] < a[i] && dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; } } }但东华机试的题目n可能到达10万,要求你用O(n log n)的贪心加二分优化。核心是维护一个数组d,d[i]表示长度为i+1的上升子序列的最小末尾元素。每次读入一个新数x,就在d里二分查找第一个大于等于x的位置,替换掉它。如果x比d中所有元素都大,就追加到末尾。最终d的长度就是LIS长度。这个技巧在进阶题里出现频率很高,建议自己手推一遍。
除了LIS,背包问题也是出题人最爱。01背包的经典转移是:
for (int i = 1; i <= n; i++) { for (int j = V; j >= w[i]; j--) { dp[j] = max(dp[j], dp[j - w[i]] + v[i]); } }内层循环必须倒序,才能保证每个物品只用一次。如果换成完全背包,内层改为正序即可。我在第50题左右见过一道“多重背包”的题目,其实只要把每种物品的数量按照二进制拆分成若干组,再套01背包模板就行。这些东西你不提前练,考场上很难在半小时内写对。
3.4 图论:并查集与最短路径不能丢
图论题在81题里数量不算多,但几乎每年都会有一两道,而且往往不是最难的题,却是最容易因为没模板而丢分的题。比如并查集,代码量不大,但思路很巧妙。模板记熟:
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } void unionSet(int x, int y) { int fx = find(x), fy = find(y); if (fx != fy) fa[fx] = fy; }使用前要把fa数组初始化成自己:for (int i = 1; i <= n; i++) fa[i] = i;。并查集常用来判断两个节点是否连通,或者统计连通分量个数。如果题目要求维护集合大小,再开一个size数组,只在合并时更新根节点的大小即可。
最短路径更不用说了,Dijkstra是单源正权最短路的标准解。我用的是优先队列优化版本,这里给大家一个精简模板:
#include <queue> #include <vector> #include <cstring> using namespace std; const int INF = 0x3f3f3f3f; vector<pair<int,int> > g[MAXN]; int dist[MAXN]; void dijkstra(int s) { memset(dist, 0x3f, sizeof(dist)); dist[s] = 0; priority_queue<pair<int,int>, vector<pair<int,int> >, greater<pair<int,int> > > pq; pq.push(make_pair(0, s)); while (!pq.empty()) { int d = pq.top().first, u = pq.top().second; pq.pop(); if (d > dist[u]) continue; for (int i = 0; i < g[u].size(); i++) { int v = g[u][i].first; int w = g[u][i].second; if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; pq.push(make_pair(dist[v], v)); } } } }注意priority_queue默认是大根堆,要加上greater得到小根堆;pair排序先比较first,所以first放距离,second放节点。如果起点有多个,可以加一个超级源点,与所有起点连一条权值为0的边,跑一次Dijkstra即可。
4. 常见问题与排查技巧实录
4.1 OJ提交常见错误对照表
刷题过程中最让人烦躁的就是“明明本地跑得好好的,提交就错”。我整理了东华OJ上最常见的几类反馈,以及对应的排查方向,希望对大家有帮助。
| 提交反馈 | 可能原因 | 解决思路 |
|---|---|---|
| Compile Error | 少头文件、变量名冲突、语法错误 | 看编译器给出的错误行号,优先检查数组定义、语句结尾分号 |
| Runtime Error | 数组越界、除以零、栈溢出、空指针 | 检查所有数组大小是否够、循环边界是否±1、递归深度是否过大 |
| Wrong Answer | 逻辑错误、边界条件漏判、精度问题 | 造极端数据自测,比如n=1、最大值、全相同的数据 |
| Time Limit Exceeded | 算法复杂度过高、死循环 | 估算复杂度,考虑用二分、哈希或动态规划优化 |
| Memory Limit Exceeded | 数组开太大、递归栈太深、使用过多STL容器 | 把全局数组改成动态分配,减少无用容器 |
这里重点说一下Runtime Error里的数组越界。很多同学定义数组大小是100,但题目范围写到1000,结果访问越界,OJ直接报运行时错误而不是答案错误。我建议开数组时统一比题目上限多5到10个,比如题目说n不超过1000,就开1010。另外,所有全局数组一定要初始化,memset或循环赋值都行,否则里面是随机值,会引发各种诡异bug。
4.2 输入输出陷阱:样例过了却WA的迷之操作
“样例过了但WA”是OJ新手最崩溃的场景。这里几乎都是输入输出或边界条件的问题。第一,多组数据要循环处理,直到EOF,不要只跑一次。第二,行末不能有多余空格。比如输出一行数组时,数字用空格分隔,算法是“前n-1个元素后面跟空格,最后一个元素后面直接换行”。第三,有的题目要求输出后不带任何多余空行,但有些题目允许两个case之间有空行,一定要看清题目描述。
另外一个经典陷阱是读取字符时把换行符读进去了。比如你用scanf("%d", &n)读入整数后,再用scanf("%c", &ch)读字符,这时候ch很可能拿到的是缓冲区里的换行符。解决办法是在%c前加一个空格,写成scanf(" %c", &ch),或者用getchar()把换行吃掉。这个小细节我见过太多人栽坑了。
4.3 本地调试技巧:造数据能力和打印大法
调试OJ题和调试普通业务代码不太一样,你不能打断点看堆栈,更多时候只能靠“推理加打印”。我自己的调试习惯是:先把题目给的所有样例都过了,然后自己编几组“刁钻数据”测试。比如排序题,我会测n=0或n=1的情况;数字题,我会测最大值int=2147483647附近;字符串题,我会测含有空格、连续空格的输入。如果这些数据都能过,基本就稳了。
如果还是WA,就在关键计算前后打印中间变量的值:进入循环前打印数据,更新答案后打印结果,递归前后打印当前状态。但记得提交前要删掉这些调试输出,否则会干扰评测,导致WA或PE。也可以使用assert在代码里判断条件,条件不成立时程序会直接崩溃,但OJ上可能显示RE,所以本地用assert,提交前删掉。
5. 考前冲刺与考场实战策略
5.1 考前一周:回归模板与错题本
考前几天不建议再做大量新题。你可能会想“我是不是还有好多题没刷完”,但我告诉你,这时候刷新题容易增加焦虑,而且短期也消化不了。正确做法是把之前整理过的错题本翻一遍,把自己封装好的模板重新抄写一遍,包括高精度、BFS、Dijkstra、并查集、01背包、LIS等。抄写不是浪费时间,这能帮你把模板里的细节刻进记忆里。
另外,考前要熟悉OJ系统的提交方式。东华机试一般支持C、C++、Java,有的还支持Python。但同一个算法用不同语言,运行效率差别很大。如果你主用C++,就一定要确认编译选项是否正确,比如是否要求C++11,是否禁用gets函数等。考前一天可以在OJ上重新交一道简单题,确认账号、密码、代码模板都没有问题,这叫“轻装上阵”。
5.2 考场上的时间分配与心态调整
真正的机试一般2到3个小时,题目数量差不多5到10道,难度有梯度。我的策略是先把所有题都看一遍,花5分钟评估每道题的难度,然后从最简单的开始写。千万不要卡在第二题上死活不出来,导致后面三题明明会做却没时间写。一般建议每道简单题控制在20分钟内,中等题40分钟,最后20分钟留出来检查。
心态上,如果遇到“完全没有思路”的题,就把它放到最后,先做其他题。做完所有能做的题之后,再回来啃硬骨头。哪怕只能写个暴力解法,拿到部分分数,也比交白卷强。我记得有一次机试,最后一题是动态规划,我没想出最优转移方程,但我用DFS枚举了一部分状态,最后也拿到了一些分。机试不看过程,但看的是你手头代码能跑出多少测试点,所以暴力法在关键时候能救命。
还有一点,务必注意审题。东华机试的很多题目会带一些小限制,比如“所有数都不重复”“结果对1000000007取模”“多组数据,每组以一个0结束”。这些细节直接决定代码怎么写。我见过好几次,整道题的思路完全正确,就是因为漏掉了“取模”导致答案溢出,最后全部WA。
5.3 长期坚持与复盘价值:把81题变成你的底气
最后说点个人的真实体会。我当年刷这81题的时候,其实一度刷到想吐,尤其是50题之后,几乎每题都要花一整个下午。但坚持到后期,我发现自己的思维速度明显变快了,很多题目刚读完题就能想到大致解法,不是因为我聪明,而是因为见过的套路足够多,脑子里已经建立了“模式库”。
比如看到“求满足条件的最短区间”,你会想到滑动窗口;看到“在一个有序数组里查找”,你会想到二分;看到“多源汇最短路”,你会想到超级源点和反向建图。这些模式全靠刷题积累。等到真正上考场的那一刻,你会感谢那个在OJ前反复修改、不断提交的自己。希望你也能把这81题刷明白,让它们成为你机试的底气,而不是压力。