打开搜索引擎,敲下“郑州轻工业大学OJ C语言”,你能翻出一大堆老帖子:有学长学姐为了刷题排名熬夜提交的截图,有新生刚接触在线评测系统时满屏红色的报错,也有不少人在问“郑轻OJ怎么进去”“题目为什么总是答案错误”。作为一个在这个OJ上从大一刷到大三、把几百道C语言题从printf练到链表的过来人,我想说这座平台上藏着的,其实是国内二本院校里难得的“正规军”编程训练资源。它不如LeetCode和牛客网那么花哨,但恰恰因为题目老派、直白、语法要求苛刻,特别适合C语言零基础入门、备考计算机二级、以及想把语法基本功砸结实的人。
这篇内容就围绕郑轻OJ本身来写:它是什么类型的平台、该怎么开始第一道题、刷题过程中最常见的六类题目长什么样、提交结果里的AC、WA、TLE到底在告诉你什么,以及一条我自己验证过、可以照着走的四周刷题路线。所有代码都是C语言,所有结论都来自真实提交经验,不整虚的。
1. 郑轻OJ到底是什么——它不是普通题库,而是一台无情的裁判机器
第一次接触在线判题系统(OJ)的人,最容易犯的错误就是把OJ当成“带标准答案的练习题网站”。你写完了代码,点击提交,系统会像裁判一样去编译、运行你提交的程序,用事先准备好的多组测试数据来检验你的程序输出是否和标准答案完全一致,然后返回一个状态码。
郑轻OJ(原郑州轻工业学院在线评测系统,老地址是acm.zzuli.edu.cn)走的也是这套标准流程。它最典型、也最让新手头疼的特点是:输出要求“逐字符完全匹配”。多一个空格、少一个换行、大小写写错、printf里把“%d”写成“%D”,结果都不会是“Accept”(AC),而很可能是Presentation Error(PE)或Wrong Answer(WA)。
1.1 判题状态码就是程序的体检报告
在郑轻OJ上提交代码后,会返回下面这些常见状态:
| 状态缩写 | 中文含义 | 本质原因 |
|---|---|---|
| AC | 通过 | 程序输出和标准输出完全一致 |
| PE | 格式错误 | 答案内容基本对,但空白字符、换行不符 |
| WA | 答案错误 | 某个测试点的逻辑输出不对 |
| CE | 编译错误 | 语法有问题,或者编译器版本导致的不兼容 |
| RE | 运行时错误 | 数组越界、野指针、递归栈耗尽等 |
| TLE | 运行超时 | 算法太慢或死循环,超过时限 |
| MLE | 内存超限 | 内存使用过多,比如无限递归或大数组 |
我见过太多新生第一次看到WA就懵掉,觉得自己“明明在Dev-C++里运行得好好的,凭什么错了”。其实这不是OJ针对你,而是因为Dev-C++里你只测了题目给的样例,OJ却在你提交后跑了不止一组隐藏数据。比如题目要求“输入多组数据,每组两个整数,输出它们的和”,你没有处理多组输入,只在本地输出了一个结果,那OJ一跑隐藏数据立刻就是WA。
1.2 和PTA、洛谷相比,郑轻OJ的定位在哪里
郑轻OJ现在确实没有洛谷那么漂亮的前端界面,也没有PTA那么多配套网课。它的优势在于“老牌、题量大、语法题密集、难度衔接自然”。网上很多人口袋里揣着“翁恺C语言”视频教程,看完一个章节想找配套练习,最顺手的其实就是这类传统OJ。
洛谷的题偏向算法竞赛,很多题目对于刚学C语言半个月的人并不友好;PTA有浙江大学课程配套,但部分题集要绑定教师班级。郑轻OJ的注册门槛低,更重要的是它保留了传统OJ那种“你得自己处理输入输出的边界”的原教旨风格,而这恰恰是C语言初学者最应该被锻炼的地方——你写代码,先得学会读懂题目和掌控数据的输入格式。
2. 开刷前的准备——从本地开发环境到第一道A+B的完整流程
很多攻略会直接告诉你“去注册账号然后开始刷题”,但作为一名教过几个学弟学妹的过来人,我强烈建议先把本地开发环境搞定。你在OJ上写的代码和在IDE里写的代码有一点点差异需要提前适应,否则会白白吃几个Compile Error。
2.1 VSCode配置C语言环境,别再用古老的Dev-C++凑合
这几年VSCode配置C语言环境的搜索热度很高,因为VSCode确实比Dev-C++好用太多。简单说三步走:
- 下载安装VSCode,装一个叫“C/C++”的官方扩展。
- 安装MinGW-w64编译器,安装后把bin目录(里面含有gcc.exe)加进系统PATH环境变量。
- 在终端里执行
gcc --version,能出版本号就说明配置成功。新建一个main.c,写第一句#include <stdio.h>后,再按F5或者用终端命令gcc main.c -o main编译运行。
这里有个容易被忽略的坑:部分MinGW安装包在装完后不会自动把gcc.exe放进PATH,导致VSCode里报“找不到gcc”,这不是你代码的问题,而是环境变量没刷新。改完后记得重启终端和VSCode。
2.2 本地代码和OJ提交的差异只在一念之间
本地写完代码,编译运行,结果正确,但交到郑轻OJ却CE或者WA,原因通常是三类:
- 编译器标准不同:郑轻OJ的老编译器对C语言标准的支持偏老。你在本地用
for (int i = 0; i < n; i++)这种C99写法在新版编译器上没问题,但在一些老OJ上可能报CE。稳妥的做法是把循环变量定义在函数开头,或者干脆保存为.c文件后用gcc -std=c99自行测试。 - main函数写错:
void main()不规范,标准写法是int main(),并且最后要有return 0;。OJ的裁判不会因为少写return就直接判错,但这是一种职业习惯,都得有。 - 用到了gets函数:老编译器确实能过,但新版编译器已经把gets请出了标准库。郑轻OJ的编译器版本不算新,某些题目里用gets可能编译能通过,但如果你在自己的VSCode里装了较新的MinGW,编译直接报错。
我在实际教别人的时候会建议:输入字符串请用fgets,实在要用整行读取,用scanf("%[^\n]"),少用gets。这不仅是兼容性问题,也关系到缓冲区安全。
2.3 第一道题:A+B的完整提交演练
很多传统OJ的第一道题都是A+B:读入两个整数,输出它们的和。题目通常长这样:
输入:多个测试实例,每个实例占一行,由两个整数a和b组成。输出:每行输出a+b的值。
如果你读题不仔细,会写成:
#include <stdio.h> int main() { int a, b; scanf("%d%d", &a, &b); printf("%d\n", a + b); return 0; }这个代码在本地运行,输入1 2,输出3,没有任何问题。但提交到OJ后,结果往往是WA。原因是题目说“多个测试实例”,你的程序只处理了一组输入。正确做法是使用循环读取,直到EOF:
#include <stdio.h> int main() { int a, b; while (scanf("%d%d", &a, &b) != EOF) { printf("%d\n", a + b); } return 0; }这里最关键的知识点就是:scanf返回的是成功匹配并赋值的变量个数,EOF表示文件结束。在线评测时,OJ会向你的程序喂入一个包含多组数据的文件,程序应该读完整份输入再结束。这也是题面上经常说的“多组测试数据”背后的机制。
我第一次刷郑轻OJ时,就是这样一道A+B把我从“以为自己会了”打回“原来我连输入都不会处理”。后来想通了,反而感谢这个严格系统,因为它逼着我搞懂了scanf、EOF和输出缓冲这些基础中的基础。
3. 我在郑轻OJ上反复刷到的六类题型与核心代码骨架
郑轻OJ的题目分级很“原始”:没有华丽的标签系统,题目列表就是一个接一个的编号,从1000到几千,难度大致按编号递增,中间还会穿插一些和课程章节对应的专项训练。我根据实际刷题经验,把最常出现的六类C语言题型整理如下,每一类都配有可以直接参考的代码骨架。
3.1 顺序结构与格式化输入输出
这类题考察的是最基础的printf、scanf使用。常见题目包括:输入一个整数输出它的个位、十位、百位;输入摄氏温度求华氏温度;输入半径求圆面积等。
容易踩的坑,集中在格式控制上。比如:
- 用
scanf("%d", &a)读整数,再用printf("%c", ch)读字符,中间如果有空格、换行,缓冲区里残留的空白字符会直接影响结果。 printf("%.2f", x)保留两位小数时,C语言printf的舍入规则是四舍六入五成双吗?不,printf通常按当前编译环境的浮点舍入模式工作,也就是默认的四舍五入(实际上是IEEE 754的round-to-nearest-even,但多数题目不会考这么深),你只需要记住:OJ要求保留两位,你就老老实实写%.2f,不要自己乘100再加0.5去“手算四舍五入”,那样反而容易引入精度误差。
下面这题是典型中的典型:输入一个三位数,输出它的各位数字之和。
#include <stdio.h> int main() { int n, a, b, c; while (scanf("%d", &n) != EOF) { a = n / 100; b = n / 10 % 10; c = n % 10; printf("%d\n", a + b + c); } return 0; }这种题不值得炫耀,但非常有必要认真做完。因为通过它,你会建立“数据是怎么从输入到内存再到输出”的第一步体感。之后学到指针时,回头看&a就更容易明白:“地址”不是玄学,是变量在内存中的房间号。
3.2 分支判断与循环控制
分支和循环是C语言的骨架。郑轻OJ上这块的题有:判断闰年、判断字符是大写/小写/数字(会用到ctype.h里的isalpha、isdigit这类函数)、成绩等级转换、九九乘法表、素数判断、最大公约数等。
老生常谈的“九九乘法表”也很能说明问题。题目让你输出下三角的九九表:
#include <stdio.h> int main() { int i, j; for (i = 1; i <= 9; i++) { for (j = 1; j <= i; j++) { printf("%d*%d=%d\t", j, i, i * j); } printf("\n"); } return 0; }这题看起来简单,但它考察两个容易被忽视的点:第一是输出格式用\t还是空格,这决定PE还是AC;第二是内层循环条件j <= i而不是j <= 9,这决定表格形状。很多在纸上写得很溜的人,到了键盘上反而会在循环边界上出错。记住一个原则:写边界时先假设i=1和i=9,手动走一遍,保证限制条件在两个端点都成立。
再比如经典的冒泡排序,郑轻OJ里的排序题看似初级,但它是后续学习数据结构之前最重要的手感训练:
#include <stdio.h> // 输入n个整数,从小到大输出 int main() { int a[105]; int n, i, j, t; while (scanf("%d", &n) != EOF) { for (i = 0; i < n; i++) { scanf("%d", &a[i]); } for (i = 0; i < n - 1; i++) { for (j = 0; j < n - 1 - i; j++) { if (a[j] > a[j + 1]) { t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; } } } for (i = 0; i < n; i++) { printf("%d ", a[i]); } printf("\n"); } return 0; }关于冒泡排序,我只提醒一点:内层循环为什么要减i?因为每一轮冒泡都会把当前未排序部分的最大值送到最后,所以每轮结束,最后i个元素已经就位,没必要再比较。如果去掉- i,排序结果没错,但比较次数变多,在数据量大的OJ测试点上很容易TLE。别小看常数级别的优化,算法题里“卡常”是很常见的事。
3.3 数组、字符串与字符数组操作
学完循环之后,紧接着就是数组和字符串。郑轻OJ里这类题占比很大,包括:数组逆序、查找最大最小值、统计字符串中各类字符个数、字符串逆序(和PTA上的“字符串逆序”题很像)、把字符串按空格拆成单词等。
字符串这块最能暴露C语言新手的问题,因为很多人会拿起gets不放手,或者随手定义char *p = "hello";然后试图修改里面的字符,导致运行时错误。C语言的字符串本质上是以'\0'结尾的字符数组,这个结论必须刻在脑子里。
来看一个非常常见的题:输入一个以换行为结束的字符串,输出它的逆序。基础写法是双指针交换。
#include <stdio.h> #include <string.h> int main() { char s[1005]; int left, right; char t; // fgets会把换行符也读进来,要去掉 while (fgets(s, sizeof(s), stdin)) { s[strcspn(s, "\n")] = '\0'; left = 0; right = (int)strlen(s) - 1; while (left < right) { t = s[left]; s[left] = s[right]; s[right] = t; left++; right--; } printf("%s\n", s); } return 0; }这里如果没有strcspn那行去掉换行,你的逆序结果末尾会带一个换行符,输出时就会莫名多一个空行。这个坑在OJ上非常隐蔽,因为它只在字符串长度为某些值时表现出来。
再看“把一个字符串按空格分开输出每个单词”的题。很多人第一反应是strtok函数。能用,但strtok的副作用是会修改原字符串,把分隔符替换成'\0'。如果后续还需要用原字符串,就不合适。所以我更建议手写遍历:
#include <stdio.h> int main() { char s[1005]; int i = 0; while (fgets(s, sizeof(s), stdin)) { i = 0; while (s[i] != '\0' && s[i] != '\n') { if (s[i] != ' ') { printf("%c", s[i]); } else { printf("\n"); } i++; } printf("\n"); } return 0; }这段代码的思路是:不是字符就输出,是空格就换行。简单粗暴,而且不需要改动原数组。但它对连续多个空格处理得不好,如果题目要求“单词之间可能有多个空格,但输出时每词一行”,那还得加一个状态标记,只在“处于单词中间”时才输出。
3.4 函数、递归与实际应用
等语法熟练了,OJ题目就开始要求你自定义函数、实现递归。最常见的是:根据公式求sin(x)、cos(x)的级数展开,用辗转相除法求最大公约数,或者用递归算Fibonacci数列。
很多同学对递归有心理障碍,其实递归的核心只有两条:递归边界和递归表达式。以计算n的阶乘为例:
int fac(int n) { if (n <= 1) return 1; return n * fac(n - 1); }递归边界是n<=1时返回1,递归式是n*fac(n-1)。你不需要在心里打开一层层的函数调用栈,你只需要相信:只要边界正确,表达式正确,程序就不会错。这句话当时救了我的命。
但递归也有代价。郑轻OJ里如果出Fibonacci数列的题目,n到40还好,n到50就开始明显变慢,n到100用简单的递归直接卡死。因为简单递归的调用次数是指数级的。我建议在这类题上养成用迭代或记忆化递归的习惯:
long long fib[105] = {0}; int main() { int n, i; while (scanf("%d", &n) != EOF) { fib[1] = 1; fib[2] = 1; for (i = 3; i <= n; i++) { fib[i] = fib[i - 1] + fib[i - 2]; } printf("%lld\n", fib[n]); } return 0; }如果不小心在OJ上遇到TLE,先别急着怪系统,回头看看自己的实现是不是每一组输入都重新从第3项开始算。如果是,那每次输入都是O(n),多组输入下就容易被卡。
3.5 结构体与链表:从“面向过程”到“抽象数据”
郑轻OJ的题面虽然老,但结构体、链表的题目一点都不少。这些题往往直接对应学校课程设计,也对应计算机二级C语言的考试范围。
先看结构体基础用法:
#include <stdio.h> struct Student { char name[50]; int score; }; int main() { struct Student stu; scanf("%s%d", stu.name, &stu.score); printf("%s %d\n", stu.name, stu.score); return 0; }结构体的重点,第一是stu.name本身就是数组名,数组名在做函数参数时会退化成指针,所以scanf里不需要加&;第二是&stu.score必须取地址。这两点混在一起,不小心就是内存越界或者段错误。
链表则是结构体+指针+动态内存分配三座大山的集合。最常见的要求是:输入n个学生的成绩,创建链表,按成绩排序,最后输出。这里给出一个用头插法建立链表的核心骨架:
#include <stdio.h> #include <stdlib.h> struct Node { int data; struct Node *next; }; int main() { int n, i, val; struct Node *head = NULL, *p; while (scanf("%d", &n) != EOF) { head = NULL; for (i = 0; i < n; i++) { scanf("%d", &val); p = (struct Node *)malloc(sizeof(struct Node)); p->data = val; p->next = head; head = p; } for (p = head; p != NULL; p = p->next) { printf("%d ", p->data); } printf("\n"); } return 0; }注意头插法建立链表后,输出的顺序和输入顺序相反。如果题目要求顺序不变,那你应该用尾插法,也就是再维护一个tail指针指向链表尾部,每次把新节点挂到tail后面。
链表题的坑集中在内存管理上:malloc后不检查是否成功、free后继续访问野指针、单向链表找不到前驱节点导致删除失败。在OJ上最常见的表现是RE,严重的是本地运行都直接崩溃。
3.6 文件读写:只在题目要求时使用
郑轻OJ这几年的一些题目开始增加文件操作的内容,比如读取文件中的学生成绩,计算平均值,再写回文件。典型操作如下:
#include <stdio.h> int main() { FILE *fp; int a, b; fp = fopen("in.txt", "r"); if (fp == NULL) { printf("cannot open\n"); return 1; } fscanf(fp, "%d%d", &a, &b); fclose(fp); fp = fopen("out.txt", "w"); fprintf(fp, "%d\n", a + b); fclose(fp); return 0; }但这里我一定要说清楚:OJ的在线评测系统,手动提交的代码读的是标准输入,输出的是标准输出。只有当题目明确写了“输入数据来自文件input.txt”或者“输出到文件output.txt”时,你才需要写fopen。否则你写一行fopen("in.txt", "r")就是在给自己挖坑,OJ的工作目录里不一定有你期望的文件,程序会直接返回NULL,然后你的程序就报了运行时错误,你还一脸茫然。
郑轻OJ早期有很多题目其实是支持标准输入输出的,刷题时优先认准scanf/printf。文件操作应该是你为了应付考试或者课设而单独练习的内容,不要把它和OJ自动评测混在一起。
4. 那些年我们交出的罚时——提交结果的完整排查链路
刷OJ最磨人的部分不是写代码,而是面对各种各样的非AC结果。我把自己在郑轻OJ上的排查思路整理成一套流程,你可以按这个链路逐级排除。
4.1 编译错误(CE):先看第一行报错,别看满屏红色
第一次写C语言的人看到CE往往慌得不行。其实编译器已经在报错信息里告诉你问题所在了。常见原因有:
- 变量未声明就使用。
- 函数名拼写错误,比如
printf写成print。 - 中文字符混进代码,比如全角分号。
- C99的for循环变量声明在老编译器上不被支持。
- 结构体定义后忘记加分号。
我的建议很简单:在本地终端把报错信息截下来,读第一行,它通常指到某个文件和行号,再去对应行找问题。别盯着整屏红色发呆,排查CE是最不需要动脑的。
4.2 答案错误(WA):先区分格式错和逻辑错
遇到WA先别急着改代码。我见过最冤的情况是:程序逻辑完全正确,只是输出时最后多了一个空格。OJ系统是逐字符比对的,结尾多一个空格也是WA。郑轻OJ有时候会把这种情况判成PE,但有些老旧题目的数据不够严格,也会直接判WA。
建议的排查顺序:
- 检查输出格式,包括空格、换行、逗号、大小写。
- 检查边界值,比如输入为0、为负数、为最大值时,你的程序会不会崩。
- 检查多组输入的处理,是不是只跑了一次。
- 检查数据范围,是不是该用long long而你用了int。
举个例子,题目说“输入两个整数a和b,输出a+b”,但没说范围。如果你用了int,交上去WA,这时候把int改成long long很可能就AC了。不要问为什么题目数据能这么大,传统OJ的题目描述有时候就是不留余地,参赛者要自己预判风险。
4.3 运行时错误(RE):数组越界、野指针、递归爆栈的自我体检
RE是C语言新手在OJ上最崩溃的状态,因为本地编译能过、运行偶尔正常、交上去却直接崩。最常见的三大元凶是:
- 数组开小了。郑轻OJ的题面如果写“输入一个长度不超过1000的字符串”,你的数组至少要开
char s[1005],给结束符留位置。如果你恰好开char s[1000],在字符串长度为999时还能凑合,但如果数据不小心是1000,'\0'会写到数组外面,行为就不可预测了。 - 忘记对malloc返回的指针做空判断,或者free后继续访问。
- 递归没有出口,导致函数调用栈无限增长,最终触发栈溢出。
出现RE时,先在本地用最极端的数据自测一遍,比如输入题目允许的最大值、空串、单字符、最大n等。这比我教你十种调试技巧都管用。
4.4 运行超时(TLE):不是机器慢,是你的代码不够经济
TLE在入门阶段多半来自死循环。最容易写死循环的地方是while(scanf(..., &n) != EOF)配合了奇怪的跳出条件,或者是循环里变量忘记更新。
等语法熟练以后,TLE更多是算法复杂度问题。比如冒泡排序处理一万条数据完全没问题,但处理十万条就吃力了;单纯递归求Fibonacci的50项也会卡。这时候你需要学会“观察数据量级”:n小于1000,O(n^2)基本能过;n大于10万,想过就得靠O(n log n)乃至O(n)的思路了。
5. 一条可以完全复制的四周刷题路线,和时间投入之外的两个奖励
最后分享一个具体的路书。这是我带过几个零基础学弟学妹时验证过的方案,每周保证5到7小时的投入即可,不用每天打鸡血。
5.1 四周路线表
| 周次 | 目标 | 刷题内容 | 核心验收标准 |
|---|---|---|---|
| 第1周 | 熟悉OJ和基础输入输出 | 完成A+B系列、顺序结构题20道左右 | 能不看任何参考,独立写出多组输入输出循环 |
| 第2周 | 分支、循环、数组 | 判断闰年、素数、九九乘法表、冒泡排序等30道左右 | 手写冒泡排序不卡壳,能够处理第一行输入n的套路 |
| 第3周 | 字符串、函数、递归 | 字符串逆序、按空格拆词、统计字符类型、递归实现gcd等25道左右 | 能说出字符串和字符数组的关系、函数传参的地址传递 |
| 第4周 | 结构体、动态内存、链表 | 学生成绩排序、链表创建遍历、简单文件读写20道左右 | 独立完成单链表插入和遍历,不再出现空指针崩溃 |
前两周有同学会觉得太简单,但我要说的是:简单题的“红利”在于你不需要动脑就能把语法的“手感”练出来,就像练投篮要先做徒手动作。直接上难题只会打击信心。
到了第3周,你会明显感到题目的难度爬升了。这一阶段如果把字符串题做顺,后面指针、链表就会学得轻松很多。第4周代码量变大,也是第一次真正需要你管理内存,错了不要紧,多调试几遍。
5.2 除了刷题数量,郑轻OJ还给两样额外的东西
一是对“计算机二级C语言”考试的直接帮助。二级上机题的难度,刚好就落在郑轻OJ的前200道题这个区间。很多考二级的人还在到处找模拟软件,其实拿郑轻OJ练手完全够用,因为上机题考的就是语法细节和流程控制,和传统OJ的考察方式高度重合。
二是一种“敢读题、敢提交”的心理素质。现在LeetCode、牛客网上很多题面试时根本不会给你AC的机会,一道题想一天是常态。但在郑轻OJ这个难度温床上,你可以在很短时间内积累大量AC,这种正反馈对建立编程信心特别重要。等你以后去了更大的平台,回忆起当年第一次看到绿色AC时的兴奋感,你会明白这种“小胜利”积累起来的力量。
最后,也算是我自己折腾这么多年的一点体会:刷再多的题,都不如你亲手把一份WA改到AC的过程来得值钱。那个过程里你会学会看报错、查边界、读题面上没有明说的潜台词,这些东西不是天赋,而是实实在在练出来的肌肉记忆。
郑轻OJ不是最高大上的编程平台,但如果你是一个刚接触C语言、想知道“我会不会写代码”的人,它绝对是最值得一站的起点。