如果你正在准备东华大学的机试,或者对高校OJ(Online Judge,在线评测系统)的题目风格感到头疼,那么“2023东华大学OJ机试题22-70”这一批题值得你花时间好好研究。我今年实际刷完了这批题,也带过几个学弟学妹做同样的训练,今天就把我踩过的坑、总结出的套路、以及针对典型题目的完整拆解一次说清楚。这篇文章不止是题解汇总,更是一份机试备考的实操指南,适合正在准备复试、想要系统训练算法基础、或者单纯想搞懂OJ评测逻辑的同学阅读。
先说一个很多人容易忽略的点:OJ机试题和我们平时在IDE里写代码完全是两个世界。你本地跑通不算数,评测机只看你的程序能不能在规定时间内、用规定的输入输出格式,稳定地得出正确结果。东华大学这批题从22题到70题,恰好覆盖了从“入门语法”到“经典算法”的完整阶梯,我刷完之后最大的感受是:它不搞偏题怪题,但非常考验基本功的熟练度和边界情况的处理能力。
1. 内容整体设计与思路拆解
1.1 这批题的核心定位:从语法巩固到算法入门的过渡带
22-70这个题号区间很有意思。它不像1-20题那样纯考if、for、while的语法层面,也还没到后面那种动辄需要图论、网络流、线段树的高强度算法题。如果你打开东华OJ的题目列表,你会发现这个区间里大量出现的是:模拟题、字符串处理、简单排序、数论基础、以及最基础的动态规划模型。
我个人的理解是:这个区间是出题人专门设计的“分水岭”。基础好的同学做这些题可以快速找回手感,基础薄弱的同学也能通过这一批题建立“用程序解决实际问题”的思维模式。它不会让你一上来就面对一个复杂的算法模型发懵,但如果你只会背API、不理解和推导,这几道题会精准地暴露你的问题。
举个例子,第30多题附近有一道关于日期计算的模拟题,表面上是“给定年月日,求星期几”,核心考察点其实是:闰年判断的完整条件、月份天数的预处理、以及累加过程中是否会溢出或越界。这类题放到LeetCode上就是简单题,但在OJ的机试场景里,它就是用来筛掉“粗心”和“不严谨”的。
1.2 为什么说机试备考一定要刷OJ而不是只刷LeetCode
很多同学问我:我LeetCode刷了两三百题,机试是不是稳了?说实话,这完全是两个体系。
LeetCode的主流模式是“核心代码模式”,也就是你只需要补全一个函数,输入输出都被封装好了。而OJ机试绝大多数是“ACM模式”,也就是你需要自己处理标准输入输出,用scanf或cin把数据读进来,再用printf或cout把结果打出去。就这一步,能把很多只刷LeetCode的同学卡住。
更关键的是,OJ的评测机对代码的“边界行为”极其敏感。多输出一个空格、少输出一个换行、读取时没有处理多组数据(直到EOF)、数组开小了导致越界、递归层数太深导致栈溢出,这些在LeetCode上可能根本触发不了,但在OJ上就是实打实的“Runtime Error”或“Wrong Answer”。
东华大学这批22-70题,就是帮你把这些“机试专属的坑”一个个踩平。刷完这批题,你至少能建立起三个核心能力:第一,能熟练处理各种输入输出格式,包括多组输入、特定终止条件、字符串含空格;第二,能估算自己算法的最坏时间复杂度,判断在1秒或2秒的时间限制下是否可行;第三,能养成极端数据测试的习惯,比如测试0、测试最大值、测试空串。
1.3 选择C/C++作为主语言的理由
如果你去看东华大学OJ上的通过率统计,会发现使用C/C++的提交数量远远超过Java和Python。这背后有历史原因,也有实际考量。
最主要的原因是性能。机试的评测机通常不会很豪华,一台物理机上要跑大量提交,每个题目还有严格的时间限制(一般是1000ms到2000ms)。同样的算法,用C++写能过,用Python写可能就超时了。尤其是涉及双重循环的模拟题、或者10^6级别的排序题,Python的动态类型和解释执行开销会被瞬间放大。
但这不代表你不能用Python,如果你对Python的优化技巧非常熟悉(比如多用sys.stdin.readline而不是input,避免不必要的对象创建),一部分题也能过。只是从稳妥和普适的角度,我强烈建议备考机试的同学掌握C++的基础语法和STL(标准模板库)的基本用法。你不需要精通模板元编程,但像vector、string、sort、map、queue、stack这些高频容器,必须达到肌肉记忆的水平。
2. 核心细节解析与实操要点
2.1 输入输出格式的“潜规则”:比你想的更严格
不管你是刚接触OJ还是已经刷了不少题,输入输出格式永远是最容易翻车的地方。东华的这批题里,输入格式大概分成三类,我逐个说一下它们的特点和应对方式。
第一类是单组输入,这是最简单的。比如题目说“输入一个正整数n”,你直接读就行。关键是第二类:多组输入,直到文件结尾(EOF)。这类题通常会写“输入包含多组测试数据,每组第一行是一个整数n”或“处理到文件结束”。C++标准的写法是:
int n; while (scanf("%d", &n) != EOF) { // 处理逻辑 }这里有一个非常典型的坑:如果你在循环里用了continue,一定要先用scanf把本组数据读完再去continue,不然残留的输入数据会污染下一组。我就见过很多同学在“判断到某个标志就跳过本组”的题目上反复WA(Wrong Answer),一查代码,全是这个原因。
第三类是有特定终止条件的输入。比如“输入两个正整数a、b,当a和b都为0时结束”。这种题需要你小心处理“0 0”到底算不算一组有效数据。很多题明确说了“0 0表示输入结束,不作为测试数据”,那就意味着你要在循环开始先判断这两个值是否为结束标记,如果是就直接break,不能把这一组数据拿去算结果。
再来说输出。OJ对输出格式的判定是绝对严格的:多一个空格、少一个换行、整型输出成了长整型带了一串数字,统统判错。有一条实用经验是“宁可多换行,不要多空格”。比如要求“每行输出一个数”,那一行结尾的换行是必须的;如果要求“两个数之间以空格分隔”,那么最后一个数后面不要跟空格,最简单的处理方法是先输出第一个数,剩下的数用“空格+数”的格式输出,或者把结果存进容器再统一拼接。
2.2 数组和容器的边界与初始化:九成的RE根源
Runtime Error(运行时错误)是机试里最让人抓狂的反馈,而它绝大多数情况下都指向数组越界。东华这批题里有很多模拟题需要你开数组来存储状态,比如模拟一个矩阵、模拟某种序列的变化。很多同学倒在了“数组开小了”或者“数组下标越界”这种低级错误上。
核心原则是:凡是涉及数组下标,一律使用从0开始的索引,并且把数组空间开大一点。假设题目说n最大是1000,你开一个a[1005],这种余量是必要的。另外,如果你要用数组模拟“第1个到第n个”这种逻辑(比如1-based),我建议直接用a[0]占位不用,代码里用a[i]表示第i个,省去很多减一加一的头脑体操。
另一个高频问题是初始化。OJ评测机每次运行你的程序,分配的内存空间里的数据是不确定的,也就是说局部数组如果不初始化,里面是什么随机值都有可能。如果你忘了初始化但是代码逻辑恰好依赖于数组初始为0,那你的程序可能在本机能跑出正确答案,提交后却WA。用memset(a, 0, sizeof(a))或者C++的vector<int> a(n, 0)都是稳妥的选择。
2.3 时间复杂度概算:别让超时成为你的宿命
说到底,机试考的不只是“能不能写出来”,更是“能不能在规定时间内跑完”。东华OJ的题目通常会指明数据范围,比如n ≤ 10^5。看到这个数字,你心里应该立刻做一个换算:如果算法是O(n^2),那最坏要执行10^10次操作,在1秒的时间限制内基本不可能通过;如果优化到O(n log n),大约是10^5 × 17 ≈ 1.7×10^6次,这就是正常范围。
我见过不少同学在一道题上写出了完全正确的算法,但因为复杂度太高而TLE(Time Limit Exceeded)。这不是粗心,而是缺少“复杂度意识”。刷题时有意识地记录每个题目的数据范围,然后在纸上写一下自己的算法复杂度,坚持一段时间会形成本能。特别是排序题,能用sort就不要手写冒泡;能在读入时做前缀和就不要在查询时双重循环。
2.4 特殊场景与极端数据:从“能跑”到“能过”的最后一公里
题目说n的范围是1到1000,你的测试样例全是温和的两位数,这远远不够。机试评测数据里一定会包含边界值、最大值、最小值、甚至空输入。优秀的刷题者会主动给自己设计“攻击性测试用例”。
比如一个求数组最大值的题,极端情况就是数组只有一个元素,或者所有元素都是负数,或者数组长度到达上限。一个字符串处理题,极端情况就是空串、字符串只有空格、字符串长度到达上限。如果代码在这些情况下能稳定输出,才勉强算这道题过关了。我自己的习惯是,写完代码后先用题目给的示例测试,再自己构造3到5个边界用例,尤其是把“0”“1”“最大值”这些特殊值喂进去。这个习惯让我在东华这批题里少走了很多弯路。
3. 实操过程与核心环节实现
前面讲了理念,这里进入正题。我以22-70这个区间里几道有代表性的题目为例,按题型分类来做一次完整的源码级拆解。这些题目的具体文字描述我记不完全,但题型和考察点是非常清晰的,我也给每道题标注了“题号区间仅供参考”,大家实际在做的时候遇到同类题可以直接套用思路。
3.1 经典模拟题:“多组输入下的数据统计”(约22-30区间)
这类题型的标准描述是:输入有多行,每行包含若干个整数,先给出一个n表示这一组里有多少个数,然后求出这组数的和、平均值、最大值、最小值中的某几项。题目不难,但它考察的是对输入结束条件的判断、累加求和时的类型选择、以及输出格式的控制。
我给出的参考模板是这样:
#include <cstdio> int main() { int n; while (scanf("%d", &n) != EOF) { int sum = 0, maxv = -1000000000, minv = 1000000000, x; for (int i = 0; i < n; i++) { scanf("%d", &x); if (x > maxv) maxv = x; if (x < minv) minv = x; sum += x; } printf("%d %d %d\n", maxv, minv, sum); } return 0; }这里有一个值得展开的细节:为什么maxv要初始化成-1000000000而不是0?因为题目并没有说数据一定是正数,如果所有输入都是负数,而你把maxv初始化为0,那结果就错得离谱。对minv的初始化同理。这是一个极其典型的边界条件问题,也是机试判分中非常喜欢埋的雷。
还有一个常常被忽略的点:累加和的数据类型。如果每一组数很多很多(比如n=10^5),数值范围又大(比如每个数最大10^9),那么累加和完全可能超过int能表示的范围(大约21亿)。这种情况下必须使用long long(在32位系统上是64位),否则你会得到一个看起来莫名其妙的“溢出后的错误结果”。机试中涉及大数求和、乘方、组合数计算,第一反应就应该是long long,不要犹豫。
3.2 字符串处理题:“含空格的字符串处理”(约40-50区间)
字符串题是机试的常客,东华这批自然也少不了。有一道典型的题目是:输入一行字符串,可能包含空格,要求统计其中某个字符出现的次数,或者把其中某些字符过滤掉后逆序输出。
这里最大的坑是输入读取方式。如果你用了cin >> s或者scanf("%s", s),遇到空格就会停止读取,那空格后面的内容就全被截断了。要读入一整行含空格的字符串,C++里应该用cin.getline(s, len)或getline(cin, str)(后者需要包含<string>头文件),C语言则用gets(s)(虽然不够安全,但在OJ环境通常能用)或者fgets(s, len, stdin)。
针对这类“含空格字符串处理”题目,一个标准的读取和遍历框架是:
#include <iostream> #include <string> using namespace std; int main() { string line; while (getline(cin, line)) { // 读入整行,包括空格 for (int i = 0; i < (int)line.size(); i++) { // 逐个字符处理 } } return 0; }注意line.size()返回的是无符号类型,如果你在循环体里写了line.size() - 1这类代码,在size()为0时就会发生下溢,变成一个巨大的正数,导致循环异常。所以要么把size()的结果强转成int,要么在循环前用一个int len = line.size();保存长度。这个小细节,我在复查代码时经常见到。
还有一个很隐蔽的点:getline(cin, line)和前面的cin >> n混用时,中间可能会残留一个换行符。如果你先读入一个整数,再用getline读字符串,第一次getline很可能读到的是那个换行符,直接返回一个空串。解决办法是读完整数后加一句cin.ignore(),把缓冲区里的换行符吸收掉。这是OJ里“为什么我第一次读到的字符串是空的”这种问题的最常见解释。
3.3 排序应用题:“结构体排序与多关键字比较”(约50-60区间)
排序是机试的永恒主题。东华这批题里,有一道很经典的学生信息排序题:输入若干行,每行包含学号、姓名、成绩,要求按成绩从高到低排序,如果成绩相同则按学号从小到大排序。
直接用系统自带的sort函数,关键在于怎么提供“比较规则”。C++的标准做法是定义一个结构体,然后写一个自定义比较函数(或Lambda表达式):
#include <cstdio> #include <algorithm> #include <cstring> using namespace std; struct Student { char id[20]; char name[50]; int score; }; bool cmp(Student a, Student b) { if (a.score != b.score) return a.score > b.score; return strcmp(a.id, b.id) < 0; } int main() { int n; while (scanf("%d", &n) != EOF) { Student stu[105]; for (int i = 0; i < n; i++) { scanf("%s%s%d", stu[i].id, stu[i].name, &stu[i].score); } sort(stu, stu + n, cmp); for (int i = 0; i < n; i++) { printf("%s %s %d\n", stu[i].id, stu[i].name, stu[i].score); } } return 0; }这里有两个实际经验分享。
第一个是:比较函数里“成绩降序、学号升序”这种多关键字排序,千万不要写成“返回false就交换”的反向逻辑。sort的比较函数要严格遵循“严格弱序”原则:a排在b前面的条件是cmp(a,b)为真。我一般习惯把所有比较规则写成一个完整的逻辑表达式,而不是嵌套多个if-else,这样读起来更清晰,也不容易出错。
第二个是:用C风格字符串比较时一定要用strcmp,不要直接写a.id < b.id。因为结构体里的id是字符数组,不是string,直接比较数组名比较的是两个地址值,结果完全随机。如果你嫌麻烦,可以改用string id; string name;,但这样就不能用scanf读了,得先用cin读入。这里本质上是一个“C风格输入 vs C++风格输入”的取舍,我的做法是审题后统一:凡是结构体里全是数字,我就用C风格;凡是字符串多,我就全用C++的string和cin,绝不混用,最大程度避免缓冲区残留问题。
3.4 动态规划初阶:“最大连续子段和”(约60-70区间)
到了这个区间,动态规划开始出现。最经典的入门DP题就是求一个序列的最大连续子段和:给定一个整数序列,找出一个连续的子序列,使它的和最大。
这类题如果用暴力枚举起点和终点再求和,复杂度是O(n^2),在n稍大的时候会超时。正确做法是线性DP,核心状态转移是:以当前位置结尾的最大子段和,要么是自己单独成一个子段,要么是上一个位置的最大子段和加上自己。写成代码很简短:
#include <cstdio> #include <algorithm> using namespace std; int main() { int n; scanf("%d", &n); int x, cur = 0, ans = -1000000000; for (int i = 0; i < n; i++) { scanf("%d", &x); cur = max(x, cur + x); ans = max(ans, cur); } printf("%d\n", ans); return 0; }这里最关键的是ans的初始值。如果你把ans初始化为0,当所有数都是负数时,你会输出0,但正确答案应该是那个最大的负数(比如序列是-1, -2, -3,答案是-1)。所以ans必须初始化成一个足够小的负数,比如-1000000000(对应int范围内的最小值附近),或者直接用INT_MIN(要包含<climits>头文件)。
DP题在机试中的考察重点其实不是“背公式”,而是“想清楚状态代表什么”。我在带学弟学妹时经常说,如果你能用自己的话解释清楚cur和ans各自表示什么,这道题才算真正会了。cur表示“强制以当前扫描到的元素为结尾时,能得到的最优子段和”;ans表示“扫描到现在为止,已经能确定的全局最优子段和”。如果这个逻辑在写之前没有想透,写出来的代码一改就错。
3.5 数论基础:“约数与素数判断”(约60题附近)
数论题在任何OJ题库里都不会缺席。东华这批题里有一个非常常见的题型:判断一个数是否为素数,或者统计某个区间内素数的个数,或者求最大公约数(GCD)。
判断素数最基础的写法是枚举2到sqrt(n),复杂度O(√n)。但如果你需要判断多次(比如有T组测试数据,每组n最大10^6),这个复杂度可能不够用。更稳的做法是预处理素数表,也就是“埃氏筛法”,核心思路是:从2开始,把每个素数的倍数全部标记为合数。预先筛好之后,每次判断一个数是否是素数就只需要O(1)查表。
埃氏筛的参考实现:
#include <cstdio> #include <cstring> const int MAXN = 1000005; bool isPrime[MAXN]; void sieve() { memset(isPrime, true, sizeof(isPrime)); isPrime[0] = isPrime[1] = false; for (int i = 2; i * i < MAXN; i++) { if (isPrime[i]) { for (int j = i * i; j < MAXN; j += i) { isPrime[j] = false; } } } }这个写法里有一个非常经典的优化点:内层循环从i * i开始,而不是从2 * i开始。原因是,对于任何一个小于i * i的、且能被i整除的数,它一定已经被更小的素数标记过了,所以从i * i开始可以减少很多无效操作。这个细节在笔试面试中都是加分项,在机试中也能切实降低常数时间。
另外一个值得注意的点是:i * i在i比较大时可能溢出int。在MAXN不超过10^6的情况下,i最大也就1000,平方完全在int范围内,所以这里没问题;但如果你把MAXN扩到10^7级别,i * i就可能超过int上限,这时需要写成1LL * i * i,强制提升为long long再做比较。
4. 实战经验与常见问题排查
4.1 本地运行正确,提交却WA?可能是这些原因
这个问题几乎是每个OJ刷题者的“老朋友”了。程序在自己电脑上怎么跑都对,一上交就判错,最可能的三个原因是:输入输出格式问题、数据范围问题、未初始化的变量问题。
输入输出格式问题最常见,比如该输出“Case #1: xxx”但你漏了“Case #”,或者冒号后面少了个空格,或者大小写不一致。这些细枝末节一旦错了,哪怕你的算法完美,依然WA。所以我的建议是:提交前逐字检查输出格式,尤其是英文单词的拼写、大小写、空格位置、换行位置。
数据范围问题更隐蔽。你本地测试用的数据都是题目示例那种小数据,根本不会触发溢出,但评测数据里全是接近上限的数,你的int或者long long就会爆掉。所以,做题前先看题目给的数据范围,不确定时一律用long long。这年头机试里用long long绝对不会被扣分,但该用没用就一定会被扣分。
未初始化的变量问题,在上面的字符串处理和DP题里都提到过。还有一个典型场景是:你开了一个全局数组,程序里只在满足某个条件时才给它赋值,其他分支直接拿来用。这种情况下,如果评测数据恰好走了一个你没有赋值的分支,数组里的随机值就会导致错误。对策是:养成“声明变量时就初始化”的习惯,数组一律memset,局部变量一律=0。
4.2 编译错误和运行时错误的高频雷区
很多同学一看到“Compile Error”就慌,其实这反而是最好解决的错误类型,因为OJ通常会给出详细的编译报错信息。常见的原因包括:头文件没写全、C++代码用了C语言编译器提交、数组长度用了非常量(比如int n; scanf("%d",&n); int a[n];这种变长数组在某些编译器下不通过)、结构体名字和变量名冲突等。
我的建议是:先在本地使用和OJ相同或相近的编译器标准(主流的OJ一般用GNU C++ 11或17),把所有警告当作错误对待。比如-Wall -Wextra能检查出很多潜在问题,例如变量未使用、类型转换可能丢失精度等。不要觉得这些是小题大做,评测机的编译环境和你本地的IDE完全不同,提前用严格模式检查能省下大量调试时间。
运行时错误(RE)的排查思路是:先把数组空间加大一倍,看错误是否消失;然后检查是否可能存在除零、对负数开根号、空容器访问等非法操作。如果这些都没有,再考虑是否需要long long。RE这个问题,很多时候不是“你的逻辑错”,而是“程序在执行的过程中的某个阶段环境不合规”,冷静下来逐步排查,一般能定位到具体行。
4.3 调试技巧:不会打日志?那就学会“分段输出法”
机试环境下你不能用IDE的断点调试,所以一套高效的“无IDE调试”方法就很重要。我自己的方法是“分段输出法”:在怀疑有问题的代码段前后,各加一句printf("check point 1\n")或printf("debug: cur=%d\n", cur),观察这些输出在评测数据下是否如预期出现。
如果代码在循环里,就在每个关键变量的变化处输出值,提交后用实际输出和期望输出做对比。虽然这样会多消耗一点时间,但远比盲猜快得多。等定位到具体问题,再把这些调试输出删掉重新交。有一个小技巧:可以在调试输出前面加一个特殊的标记字符串,比如//DEBUG_PRINT,最后用编辑器的全局替换一次清掉,不容易漏删。
4.4 常见问题速查表
为了让你在面对“东华OJ机试题22-70”这类区间时能快速定位问题,我做了一张结合本批题型的速查表,供你参考:
| 错误现象 | 大概率原因 | 快速排查方案 |
|---|---|---|
| WA(答案错误) | 输出格式不匹配 | 逐字核对输出,尤其是空格、大小写、Case编号 |
| WA(答案错误) | 数据范围超出int | 检查数据范围,改long long |
| WA(答案错误) | 未初始化变量 | 数组memset,变量声明时赋初值 |
| WA(答案错误) | 多组数据循环中没有正确读完本组 | 检查continue是否残留输入 |
| RE(运行时错误) | 数组越界 | 扩大数组空间,检查下标计算 |
| RE(运行时错误) | 除零或取模零 | 检查所有除法/取模操作的分母 |
| TLE(超时) | 算法复杂度过高 | O(n^2)换成O(n log n)或O(n),预计算 |
| TLE(超时) | 输入输出用了cin/cout且未关同步 | 使用scanf/printf或加ios::sync_with_stdio(false) |
| CE(编译错误) | 头文件缺失或提交语言错误 | 检查头文件,确认提交G++而非GCC |
| 输出“nan”或巨大负数 | 计算过程溢出 | 改用long long并检查中间结果 |
4.5 现场作答的节奏控制:机试不只是拼智商
最后聊一个很容易被忽略但极其重要的点:机试现场的时间分配和心态控制。
以东华大学这类高校机试为例,一般时间是两个小时到两个半小时,题目数量在5到8题不等。你不可能每道题都从容地从头写到尾,所以必须分清主次。我的策略是“先易后难,先稳后快”:拿到题先全部扫一遍,把题意最短、思路最清晰的题放到最前面做,保证在考试中期就能稳住几题的分数;把那些需要较复杂推导的模拟题或DP题放在后面,留足思考时间。
还有一个细节:写每一道题之前,先在草稿纸上把核心数据结构和算法流程写出来。不要觉得这是浪费时间,它帮你避免“写到一半发现思路跑偏”的尴尬。尤其是矩阵类的模拟题,不画图直接写代码,十个里面有八个要修修补补。我在做东华这批题时养成了“先小规模模拟一组数据,推导出每一步的数组变化,再动手敲码”的习惯,实测下来正确率提升非常明显。
另外,务必预留最后10到15分钟做“全局检查”,重点检查:所有代码是否都按要求输出了最后一行换行、是否有遗漏的return 0、是否有调试输出被复制到正式提交里。这些低级失误一旦发生,白白丢分真的很亏。
总的来说,东华大学OJ机试题22-70这个区间,是一段非常适合从“会写代码”过渡到“能过机试”的训练材料。它的题目难度梯度合理,覆盖的知识面广,而且坑点集中、有代表性。如果你能把这批题刷透,不只是掌握了几道题的解法,更重要的是收获了一套属于自己的机试方法论——从审题、设计、编码、自测到提交,每一个环节都有章可循。这个方法论,才是应对任何高校OJ或者线上笔试最值钱的东西。