如果说刷OJ(Online Judge,在线判题系统)最容易让人心态崩溃的节点,我投票给第19题。前十几题基本都是"a+b"这类热身题,提交上去闭眼AC,自信心膨胀得不行。但刷到第19到21题这个区间,题目开始变着花样折腾你:多组输入、字符串边界、数学特判,稍不留神就是一个Wrong Answer。更气人的是,本地编译器跑得好好的,一交上去就错,改来改去也找不到原因。这篇文章就是围绕这个阶段写的,我想拆一拆OJ刷题从第19题到第21题这段普遍遇到的坎儿,讲讲判题机制到底怎么回事、常见报错怎么排查,顺便聊聊我试过的几个OJ平台(HDU OJ、郑州轻工业大学OJ、东方博宜OJ这类)的差异。不管你是刚开始刷题的大学生,还是准备找实习想刷点算法基础的,只要卡在这个区间,这篇文章应该能帮你少走不少弯路。
这个阶段的人有一个共同特点:已经不是完全不会写代码的新手,但又没过"会做题"的坎。说白了,代码语法都认识,但一碰到OJ的判题规则就懵。别急,下面我按实际刷题的顺序,从原理到实操一步步讲。
1. 从"19-21题"说起:入门阶段的真实处境
1.1 为什么卡在19题的人最多
我当年刷HDU OJ时,前18题几乎全是送分题,很多就是求个和、比个大小、算个阶乘,题目描述里连拐弯都不打。但到了19题往后,题目难度曲线突然陡了一下,开始出现字符串处理、多组数据输入、格式要求严格的输出。很多人在这个区间第一次遇到Presentation Error(PE),也是第一次知道原来"输出结果对"和"输出格式对"是两回事。
这类题目之所以容易劝退,不是因为算法有多难,而是因为考点从"你会不会写代码"变成了"你能不能完整地理解题目规则"。第19到21题恰好是这种转变的典型代表。我自己带过的几个学弟学妹,几乎都在这个区间发出过灵魂拷问:"我明明本地运行样例完全一样,凭什么判我错?"这种挫败感很容易让人放弃。
把这个区间单独拿出来讲,就是想说明:你在这里卡住,不代表你智商不够,只是你还没习惯OJ的规则。一旦适应了,后面再刷几十道题反而会轻松很多。
1.2 这个阶段最容易出现的三个错觉
第一个错觉是"运行结果对就是对的"。很多人写代码只看样例输出,样例过了就提交,结果WA(Wrong Answer)得莫名其妙。OJ判题比对的是你程序在任意合法输入下的完整输出,不只是题目给你看的那个样例。样例只是给你一个参考,它覆盖不了所有边界情况。
第二个错觉是"本地跑通就万事大吉"。本地编译器不会因为你数组开小了就报错,也不会因为你少初始化一个变量就警告。很多时候代码在本地能跑,只是因为内存里的脏数据碰巧没出问题,换到OJ的评测环境就露馅了。我见过一个同学在本地跑第19题,10组数据全部正确,一交上去TLE,原因是他数组开成了100000,每次循环都从1遍历到100000,实际题目数据范围只有1000,白白超时。
第三个错觉是"不会做就是自己太菜"。说实话,能卡住人的往往不是算法本身,而是没读懂题意。比如题目说"输入以0结束",你可千万别把0当成有效数据处理;题目说"每个结果后面跟一个空行",那就不是空格而是空行。这些细节在题目描述里写得清清楚楚,但新手习惯快速扫一眼就开写,自然容易翻车。
2. 在线判题系统是怎么"判"的:机制决定策略
2.1 从提交到AC,OJ到底做了什么
想搞明白为什么老错,最好先搞清楚OJ的完整工作流程。你点下"提交"按钮后,评测系统会做四件事:编译你的代码、用预设的测试数据运行你的程序、捕获程序输出、和标准答案做比对。比对时绝大多数题目是逐字符精确匹配,少数字符串或几何类题目会启用Special Judge,只验证你的输出是否在合理范围内。
所以OJ的判定结果其实就是在告诉你,你这个程序在哪个环节出了问题。汇总一下常见结果:
| 判定结果 | 含义 | 最常见的诱因 |
|---|---|---|
| AC | Accepted,通过 | 恭喜,不用改了 |
| WA | Wrong Answer,答案错误 | 算法思路不对、边界值没处理、初始化遗漏 |
| PE | Presentation Error,格式错误 | 多了空格、少了空行、输出末尾多了东西 |
| TLE | Time Limit Exceeded,超时 | 算法太慢、死循环、用了过慢的输入方式 |
| MLE | Memory Limit Exceeded,超内存 | 数组开太大、递归层数过多 |
| RE | Runtime Error,运行时错误 | 数组越界、除零、野指针 |
| CE | Compile Error,编译错误 | 语法问题、选错语言标准 |
我个人觉得,新手最需要理解的是AC和WA之间的差距。OJ里面很多题目是"一题多解",只要输出正确就行,不需要关心你用的方法是不是和答案一样。但前提是:你必须在题目规定的时间限制和内存限制内跑完。也就是说,OJ不光考你会不会,还考你写得够不够快。
2.2 输入输出格式:新手翻车重灾区
OJ题目和平时写单次运行的练习程序最大的区别,就是输入输出格式。大多数题目都支持多组测试数据,你的程序必须能循环读入,直到输入结束。如果用C语言,最常见的就是这样:
#include <stdio.h> int main() { int a, b; while (scanf("%d %d", &a, &b) != EOF) { printf("%d\n", a + b); } return 0; }这里scanf的返回值是成功读入的参数个数。读到文件末尾时,它返回EOF,循环就终止。如果你把while条件写成while (scanf("%d %d", &a, &b)),在某些评测环境下,最后一次读取失败时返回值可能是0,循环会提前结束,导致漏掉最后一组数据。这个问题在OJ上很经典,我见过不少人在第20题这种"计算并输出"的题目上,因为这里写错丢掉AC。
如果是Java,可以用while (in.hasNextInt())作为循环条件;Python则是for line in sys.stdin。原理都一样:读到流结束就停。
输出格式的坑就更多了。常见的规则有:每一行输出后要换行、多组结果之间要不要空行、行首行尾不能有多余空格。题目里如果写了"每个测试用例的输出占一行"或"样例输出"长什么样,你就照着那个格式来,多一个空格都可能变成PE。PE虽然不算错,但也不会给AC,必须重新提交。
3. 三道代表性入门题的全过程拆解
虽然不同平台上"第19题、第20题、第21题"的具体题目可能不一样,但这类题目通常非常有代表性。我以最常见的三道题为例,走一遍完整解题流程,你看完就明白这个阶段该怎么思考了。
3.1 第19题:字符串处理的边界
这类题目常见的形式是:输入一行字符串,要求统计单词个数、反转字符串或者判断回文。比如郑州轻工业大学OJ和杭电OJ的早期题目里,都有一堆字符串题。它们共同的坑点有两个:一是字符串怎么读才能读进空格,二是数组开多大才够。
先看读入。如果你用C的scanf("%s"),遇到空格就会停,根本读不了句子。要读一整行,C里建议用fgets:
#include <stdio.h> #include <string.h> int main() { char s[1005]; while (fgets(s, sizeof(s), stdin) != NULL) { // 去末尾换行符 int len = strlen(s); if (s[len - 1] == '\n') s[len - 1] = '\0'; printf("%s\n", s); } return 0; }这里有个细节:fgets会把换行符一起读进来,所以统计长度或输出时要注意。如果用C++,cin.getline也有类似情况。Python则可以直接用strip()去掉换行。
再说边界。字符串题最容易错的就是数组越界。很多题目字符串长度上限是1000,但你在循环里写for (int i = 0; i <= strlen(s); i++),多出来的那个等于号就可能访问到结尾的空字符,导致RE或逻辑混乱。我的习惯是数组长度在上限基础上加10,遍历时明确用< len,而不是用<=。
3.2 第20题:数学模拟的坑
第20题最常见的类型是素数、最大公约数、进制转换、数列求和这几种。我拿素数判断举例,因为这个题看起来简单,实际错的人一堆。
int is_prime(int n) { if (n <= 1) return 0; for (int i = 2; i * i <= n; i++) { if (n % i == 0) return 0; } return 1; }第一坑:1不是素数也不是合数,n <= 1一定要返回假。第二坑:循环边界是i * i <= n,但这里的i最好用long long,否则当n接近int上限时,i * i会溢出。第三坑:2是素数,上面这个函数里2能正确返回真,但有些写法是for (int i = 2; i < n; i++),遇到n=2时循环不执行,直接返回1,这没问题;可就怕有人初始值写成i = 1,那所有数都会被1 % 1 == 0坑掉。
数学题还有一个特点:数据范围往往会逼你选对方法。比如让你判断1000000以内所有素数,你要是每个数都从2除到sqrt(n),勉强也能过;可要是让你判断100000000以内,那个复杂度就危险了,得考虑用筛法。这就是为什么刷OJ不能只看代码能不能跑,还得估算时间。一般评测限时1秒,代码的循环次数控制在千万级别以内比较稳妥,超过一亿基本就TLE了。
3.3 第21题:多组数据与提前终止
第21题常以"输入以0结束"或者"第一行是N,接下来是N组数据"的形式出现。前者是典型的"哨兵值"控制循环,后者是"先读个数再循环"。
先看以0结束的写法:
#include <stdio.h> int main() { int n; while (scanf("%d", &n) != EOF && n != 0) { // 处理 n } return 0; }注意顺序:scanf必须成功读入后再判断是否为0,所以n != 0放在第二个条件。如果写成while (n != 0 && scanf(...)),n在第一次判断时还没被赋值,行为未定义。这种细节在OJ上是真实发生过的,别问我怎么知道的。
第一行先读N的写法更简单,但有个容易忽略的点:N后面往往跟着N行数据,如果你在前面用了scanf("%d")读N,接下来第一行数据的换行符还在缓冲区里。此时如果你用getchar或gets读字符串,可能会读到空串。解决办法是用scanf连续读,或者读数据时跳过换行符。
这个阶段练的就是这种"输入输出流程控制"的熟练度。说白了,OJ里很多题目的算法可能只需要十行代码,但前面的数据读入能把你绕晕半小时。
4. 刷题报错排查:从WA到AC的完整链路
4.1 WA案例复盘:"我明明本地是好的"
我总结过自己WA的几百次经历,大部分都能归到三句话里:变量类型不对、初始位置不对、特殊情况没处理。
类型不对最常见的是int溢出。比如求1到n的和,n是100000,和是5000050000,int根本装不下,必须用long long。OJ里的数据范围经常是这样卡着你的,你不对着题目描述看,光用int写,样例小数据能过,大数据就WA。
初始化位置不对,多发生在多组数据的题目里。很多人在主函数开头定义sum = 0,但放进while循环之后忘了每次重新赋值。结果第二组数据一进来,sum还带着上一组残留的数值,答案自然错。这属于最冤的一种WA。
特殊情况没处理,就是边界值。比如排序题里所有数相等怎么办,再比如"输出前导零"的数位题,9999和1000这种临界数。我一直以来的习惯是把样例改出几个变体自己测:最大值、最小值、单元素、重复元素、空串。这套测试样例跑下来,能过滤掉大部分WA风险。
4.2 TLE与MLE:效率是练出来的
如果你刷题遇到TLE,先别急着怀疑评测机卡,大概率是你代码本身的复杂度超了。怎么估算?看循环。如果你写了两层循环,每层都跑到十万,那总操作数就是百亿级别,1秒内绝对跑不完,必须换思路。
我常用的优化手段有三个:
- 把
cin/cout换成scanf/printf,或者加上ios::sync_with_stdio(false);加速 - 把重复计算提到循环外,比如循环内部不必要的函数调用、重复的
strlen调用 - 换算法,比如暴力枚举改成双指针、前缀和、二分查找
MLE相对少见,但一旦出现,多半是数组开得太任性。有些题明明只用到100个数据,非开成1000000的全局数组,内存直接爆掉。还有递归深度过大的问题,深度到几十万层时栈会溢出,表现为RE而不是MLE,但本质都是内存问题。
4.3 PE:OJ特有的"洁癖"
PE是OJ特有的结果,它的意思是:你的答案内容全对,但格式不对。常见原因有:行尾多了一个空格、每行之间多了一个空行、少打了一个换行、输出的英文字母大小写和题目要求不一致。
遇到PE,解决的思路很简单:把题目给的输出样例下载下来,用diff命令或者文本编辑器的对比功能,逐字节对比你的输出和标准输出。很多新手不知道这个操作,所以我在这里多说一句:在本地用文件重定向跑程序,生成out.txt,再和题目提供的sample_out.txt做比对,任何多出来的空格、空行都能一眼看出来。
提示:写OJ题的时候,输出末尾的换行通常可有可无,但行与行之间的空行必须严格按题目要求。如果题目说"每个样例后面输出一个空行",那就需要在每组结果后额外
printf("\n")。
5. 不同OJ平台怎么选:几家平台实测体验
5.1 常见OJ的特点对比
搜索热词里出现了很多OJ平台:杭电OJ、郑州轻工业大学OJ、东方博宜OJ、杭师大OJ、华为OJ等。这些平台我大多注册过,简单聊聊使用感受,方便你选一个适合自己的。
| 平台 | 适合人群 | 特点 | 注意事项 |
|---|---|---|---|
| 杭电OJ(HDU OJ) | 算法竞赛入门到进阶 | 题目全、经典题多、讨论区资料丰富 | 全英文题面,对英语有一定要求 |
| 郑州轻工业大学OJ | 大一新生、校内课程 | 中文题面多,和课程贴合紧密 | 部分题目需要校内账号,游客可刷公开题 |
| 东方博宜OJ | 编程初学者、中小学生 | 中文、题目友好、题型分类清楚 | 难度梯度较缓,适合打基础 |
| 杭师大OJ | 校内教学、课程实验 | 题目和作业绑定,有班级功能 | 外校用户可能无法访问部分题目 |
| 华为OJ | 求职笔试准备 | 题目更贴近工程场景、字符串处理多 | 部分平台已改名为华为OD机试,题面较长 |
我的建议是:如果你刚开始接触OJ,先找中文题面、难度梯度合理的平台,比如东方博宜或者郑州轻工业大学OJ,把输入输出这个基本功练熟。等你能稳定刷完50道入门题了,再切到杭电OJ刷HDU 1000到1100这个区间,感受一下经典竞赛题的风格。华为OJ这种偏求职风格的,可以放到准备面试前再专门刷,里面的题更综合,不太适合纯新手。
5.2 刷题路线:从19-21题往后怎么走
刷到第21题,说明你已经过了"连输入都不会写"的时期。接下来我不建议继续按题号硬刷,而是按主题刷。大致顺序是:
- 简单模拟和枚举(10道)
- 字符串处理(10道)
- 排序和查找(10道)
- 贪心算法(10道)
- 搜索(DFS/BFS)(10道)
- 动态规划入门(10道)
每一个主题刷的时候,把AC的代码留档,同时写一句话笔记,记录这道题的坑在哪里。这条笔记比代码本身值钱得多。因为代码是固定的,你的思维过程才是下次遇到同类问题的解题钥匙。
我还想多说一个习惯:不要死磕一道题超过一小时。如果你在一个题上卡了超过一个小时,说明方向可能有问题,这时候去讨论区看看别人怎么描述思路,比继续钻牛角尖强。OJ的讨论区不是抄答案的地方,是帮你打破思维定式的地方。
我个人刷题的时候还有一个习惯:每道题AC之后,会把我的代码拿到本地去跑一些极端数据,比如最大值、最小值、随机值,确认它不是"侥幸AC"。因为OJ的测试数据往往覆盖不到所有情况,可能你靠运气过了,但换个数据就错。这个习惯帮我在后面的比赛里少翻车很多次。这个方法也推荐给你,从第19题开始养成,后面会越来越值钱。