新学期又到了,我带过几届大一程序设计课程,每到教材“第三章第一节”这个位置,办公室里、班级群里就会不约而同地冒出同一个问题:“老师,这个‘3.1 OJ’到底是什么意思?”。有同学以为是某个必须下载的软件,有人以为是某道题目的编号,还有人直接去搜索引擎里打“OJ学生网址”然后一脸茫然地问我是不是网站打不开了。
其实“3.1 OJ”这个说法,在不同学校、不同老师那里指的东西不完全一样,但它背后的核心几乎都是一件事:第三章第一节的作业需要在在线评测系统上完成提交。OJ全称是Online Judge,翻译过来就是在线裁判,它是一套自动接收你写的程序代码、自动编译运行、自动比对标准答案的系统。与其把它理解成一个“在线考试网站”,不如把它理解成一个特别严格、特别较真的改卷老师——它不看过程,只看你提交的程序能不能在限定的时间、限定的内存下,对一组又一组隐藏数据给出正确输出。
这篇内容不是理论科普,而是这些年我在带学生刷OJ、自己远程打比赛、以及折腾各种评测平台时攒下来的实操经验。适合刚接触编程、准备在学校课程OJ上交付第一份作业的新手,也适合想系统梳理OJ刷题方法和踩坑点的同学。我会从“3.1”这个章节编号讲到平台选择,再讲提交代码时最容易忽略的输入输出细节、调试技巧,以及C++里智能指针这类工具在OJ场景下到底该怎么用。争取让每个人看完都能少走点弯路。
1. “3.1 OJ”到底是什么:一次说清楚
1.1 章节编号背后藏着一整套教学体系
先说“3.1”这个数字。绝大多数大学程序设计教材,每周课程的推进节奏是固定的,比如第三章讲顺序结构、选择结构、循环结构,可能第一节是“程序的基本结构”,也可能第一节是“if语句与分支”。老师为了把作业和教材进度对齐,习惯性地把题单命名为“3.1作业”,然后在在线评测系统里建一个叫“3.1 OJ”的题组。
所以,当你在百度、微信公众号、课程群里看到“3.1 OJ”三个字时,不要把它当成一个固化的平台名称。它更像一种记号,代表“跟第三章第一节配套的在线评测练习”。有些评测系统会直接把这个编号做成课程模块,比如“C语言程序设计-第3章第1节-基础输入输出”,学生登录后看到的就是“3.1 OJ”这个分组。
这时候最容易发生的误会是什么?是学生以为“OJ”是一个需要另外去注册、另外去收费的网站。实际上绝大多数校内OJ都直接用学号和姓名注册,有的平台甚至已经对接了教务系统,你只要凭学号自动登录就能看到老师提前建好的作业点。关键是搞清楚自己学校推荐的是哪套系统,而不是随随便便去网上搜一个听起来很牛的OJ就开刷,因为老师建题单的平台和网上开放平台往往不是同一个,题号体系也不通用。
1.2 从“听懂课”到“程序能跑”的分水岭
从我带课的角度看,“3.1 OJ”真正想完成的教学目标,不是让学生会背语法,而是逼迫大家第一次直面程序设计的真实反馈:代码不是写完就结束了,你还要让机器愿意接受它。
很多同学在课堂上听老师讲“printf从右往左算”“scanf要取地址”都听得很顺,一到OJ上连最简单的A+B都交不上去。原因很简单,课堂听讲是线性接收知识,OJ是把你推到真实运行环境中去验证知识。你用VC6.0本地写代码时,编译器宽容,输出乱了一点也无所谓,窗口关掉就看不见了;但OJ不一样,你的程序跑完,返回结果会被精确地和标准输出文件做逐字节比对,多一个空格、少一个换行、末尾多了个回车,全部算错误。
这就像你拿到了驾照理论考试的满分,但第一次上路却不敢踩油门。“3.1 OJ”就是那条练习场里的车道,它不评价你的天赋,只看你能不能稳定地完成“读入—计算—输出”这个最基本动作。
2. 选OJ平台不是越多越好
2.1 那些常被搜索到的OJ平台到底各有什么特点
被热搜词反复提到的“江南OJ”“杭电OJ”“湘潭大学OJ”“华为OJ”“东方博宜OJ”,其实分属不同的定位。
杭电OJ是杭州电子科技大学的老牌在线评测平台,题量非常大,从入门到竞赛级别都有,很多经典题像“1002”“1020”“1096”都是几代学生刷过的习题。它的特点是题目风格老旧但经典,适合训练基本功。杭电OJ 1002是“A+B Problem II”,很多人第一次接触大数加法,就是用这道题认识到了int不够用;1020和1096也是典型的输入输出套路题,专门训练读多组数据、控制空行输出。
东方博宜OJ则是中小学信息学竞赛常用的训练平台,很多初中生、高中生在上面刷题,所以搜“东方博宜OJ答案1065”“东方博宜OJ答案1168”的往往不是大学生,而是参加信息学奥赛训练的中学生。这类平台题目偏基础,能帮你把循环、数组、递归练扎实。
江南OJ、湘潭大学OJ则更多是校内教学使用的在线评测系统,界面并不是特别华丽,但胜在题单和课程绑定紧密,老师布置的作业一般就挂在上面。它们的常见用法比较简单:老师提前把题目放进课程题库,学生登录后按章节开始作答,平台会自动显示通过率。
至于华为OJ,属于企业招聘场景下的在线编程评测。它的题目不像学校OJ那样把知识点切得那么细,更多是考察综合的算法能力和快速编码能力,比如字符串处理、链表操作、动态规划,这些题目会在机考时给你一个网址,要求在限定时间内通过全部测试点。搜索“华为OJ编程”的,大概率是应届生在准备招聘机考,而不是在刷课程作业。
2.2 我建议的选型参考表
| 平台类型 | 代表 | 主要用途 | 适合人群 |
|---|---|---|---|
| 高校教学型 | 江南OJ、湘潭大学OJ、校内平台 | 课程作业、章节练习、考试 | 大一到大二在校生 |
| 经典题库型 | 杭电OJ | 编程入门、算法基础、竞赛热身 | 想系统刷题的自学者 |
| 信息学竞赛型 | 东方博宜OJ | 中小学信息学训练、CSP/NOIP预备 | 初中生、高中生、竞赛入门者 |
| 企业招聘型 | 华为OJ、牛客网企业题库 | 笔试、机考模拟 | 应届生、社招跳槽者 |
选平台的建议只有一个:先跟课程走,再考虑拓展。如果你只是想完成“3.1 OJ”的作业,那就老老实实登录学校要求的系统,别觉得它界面土、反应慢就自己换个平台刷。因为老师布置的题单、分、截止时间都在那个平台上,你在杭电OJ刷一百道,校内OJ那道特定题目的提交记录仍是空白,期末成绩照样没有分数。
3. 从零到提交:刷OJ最核心的3个痛点
3.1 输入格式看不懂,代码写什么都没用
我观察过很多新生提交第一份OJ代码,最大的问题不是不会写计算逻辑,而是根本不知道程序应该怎么读数据。OJ题目描述里经常会有一行“Input”,它不是摆设,它决定了你的程序从第一行开始要怎么组织。
拿最常见的多组输入来举例。题目要求“每组输入两个整数,直到EOF结束”,你如果只写一次scanf("%d%d",&a,&b);printf("%d\n",a+b);,那程序跑完第一组数据就会退出,OJ输入文件里剩下的所有数据都不会被处理,结果必然是答案错误或者部分正确。正确写法一般是这样:
#include <stdio.h> int main() { int a, b; while (scanf("%d%d", &a, &b) != EOF) { printf("%d\n", a + b); } return 0; }还有一种常见格式是先给一个T,表示后面有T组测试数据。比如“第一行输入一个整数T,表示测试数据的组数”,这时候要用scanf("%d",&T); while(T--) { ... }去循环读取。这两种输入模式在3.1阶段的作业里出现频率极高,很多经典题就是专门练这个。
我教书的时候一再强调:拿到一个OJ题,先花两分钟看Input、Output两段,再想怎么写代码。因为OJ题目的本质就是“给一坨数据,让我程序吐出一坨固定格式的结果”,你连输入格式都没吃透,就算算法再对,程序读不到完整数据也白搭。
3.2 边界条件和输出格式:看起来微不足道,扣分却毫不留情
搜索热词里出现了“杭电OJ 1002”、“杭电OJ 1020”、“杭电OJ 1096”,这三个题实际上都栽过无数人。
杭电OJ 1002是A+B的大数版本,数字长度可能达到非常长,明显不能用int、long long硬扛,必须用字符串模拟竖式加法,或者用高精度数组处理。这就是第一个痛点:如果只拿题目当普通A+B做,你会反复WA,想破头也找不到原因。真正的难点是处理进位、处理两个数长度不一致、处理最高位的进位。等到你把这道题调通,你对字符串、数组、进位这三个概念的理解会直接上一个台阶。
杭电OJ 1096这类题则是输出格式的典型坑。它要求每组输出后都空一行,但最后一组后面可能又不需要额外空行,或者每一行的行末不能有多余空格。很多同学本地看输出“挺整齐的”,一提交就“Presentation Error”,原因就是程序多打了一个换行或者少打了一个空行。OJ比对的是字节流,不是“人眼看起来差不多就行”。
所以说,刷“3.1 OJ”这类基础作业,本质上是帮你养成分毫不差的习惯。本地测试过了不代表能过,你需要反复检查边界数据:0有没有考虑?负数呢?输入最大值有没有超范围?多组之间变量有没有清零?这些如果在第一次做作业时就养成习惯,后面做动态规划、图论算法题会轻松非常多。
4. 提交运行后的现场排查实录
4.1 常见提交状态速查表
第一次用OJ的人,最容易被状态栏里的英文搞懵。我把最常见的结果整理成一张表,可以帮助你少查阅很多文档:
| 状态 | 含义 | 常见原因 |
|---|---|---|
| Accepted | 通过 | 恭喜,这道题过了 |
| Wrong Answer | 答案错误 | 逻辑不正确、边界没考虑、读入不完整 |
| Presentation Error | 输出格式错误 | 多空格、多空行、末尾少回车 |
| Time Limit Exceeded | 超出时间限制 | 循环无限、算法太慢、递归没出口 |
| Runtime Error | 运行时错误 | 数组越界、除零、栈溢出、野指针 |
| Compilation Error | 编译错误 | 语法错误、选错语言、头文件缺失 |
印象最深的是有个学生交的代码一直Runtime Error,他本人怎么也想不通,明明自己的CodeBlocks里跑得好好的。后来我把代码拿过来一看,数组大小是100,题目数据范围却是10000,越界访问在本地编译器里可能不报错,到了OJ的判题环境就直接段错误。这类问题的排查思路很简单:把所有数组按题目的最大边界再放大5到10倍,把递归深度过大的写法改成循环,把指针操作加上越界判断。
4.2 在线调试的几个笨办法反而最稳
OJ不是本地IDE,不能断点调试,很多新手一遇到错误就不知道该怎么办了。我的建议是:不要硬猜,先用最笨的三板斧。
第一板斧,把题目样例输入手打一遍,放到本地运行,看输出和样例是否完全一致,包括空格和换行。很多时候你以为自己输出对了,实际用编辑器显示空白字符,才发现行末多了个空格。
第二板斧,自己构造边界测试数据。比如写了求和的题,就试一下输入0;写了循环判断的题,就试一下循环次数为1和最大值的场景;写了字符串处理的题,就试一下空字符串和超长字符串。我在指导“3.1 OJ”作业时反复强调:要有一点点“杠精精神”,专门跟自己的代码过不去。
第三板斧,加临时printf打印中间结果。测完之后要把调试输出删掉,否则这些多余内容会干扰OJ的答案比对,导致本来算法正确的结果被判为Wrong Answer。这个小细节很不起眼,但每年都有学生死在这里。
5. C++与OJ:智能指针到底该不该用
5.1 智能指针在算法题里不是万金油
搜索热词里有个关键组合:西北农林科技大学C++ OJ智能指针。这说明在很多学校的数据结构、C++程序设计课程里,老师会专门布置一道关于智能指针的题目。那智能指针在OJ里到底适合干嘛?首先要分场景。
如果你是参加ACM、蓝桥杯这类算法竞赛,特别是题量很大的情况下,我一般不推荐你用unique_ptr、shared_ptr去管理算法中的临时对象。原因是O1处理的核心是数据结构和算法,不是你手动释放了多少内存;智能指针确实能防内存泄漏,但它会引入额外的构建析构开销和代码复杂度,在时间限制严格的题目里可能得不偿失。竞赛选手普遍喜欢用裸指针加数组池来模拟链表树结构,本质上就是为了极限性能和更简单的内存布局。
但如果你是在完成学校OJ里“C++面向对象与智能指针”这个章节的作业,那情况就完全不同了。题目很可能要求你实现一个链表或者二叉树,并且用std::unique_ptr或std::shared_ptr来管理节点资源,这时候就非常有必要掌握现代C++的内存管理语义。
5.2 如何在OJ题目里安全使用智能指针
举一个比较常见的学生作业场景:题目要求用链表存储一组数据,并且要求“按顺序输出所有节点,程序结束后不能出现内存泄漏”。如果用裸指针,你得一个个delete;如果用unique_ptr,就可以把节点定义写成这样:
#include <iostream> #include <memory> struct Node { int data; std::unique_ptr<Node> next; Node(int val) : data(val), next(nullptr) {} }; int main() { auto head = std::make_unique<Node>(1); head->next = std::make_unique<Node>(2); head->next->next = std::make_unique<Node>(3); Node* cur = head.get(); while (cur) { std::cout << cur->data; if (cur->next) std::cout << " "; cur = cur->next.get(); } return 0; }这里的核心点在于:当head析构时,unique_ptr会自动递归释放后续节点,不需要手写析构逻辑。但有一个坑特别容易踩:如果你用Node* cur = head.get()去遍历链表,每次循环结束应当让cur指向cur->next.get(),而不是去移动unique_ptr本身,否则可能把所有权意外转移或者中途悬空。
至于shared_ptr,在OJ题里除非题目明确要求“多个指针共享同一个节点”,否则能不用就不用。shared_ptr需要通过引用计数来维护生命周期,多一些原子操作开销,也会让代码的可读性下降。我自己在带数据结构作业时会跟学生说:题目要求智能指针,你就老老实实用;题目没提,你就选最简单的写法。
5.3 现代C++给OJ带来的便利
智能指针只是现代C++的一部分。在3.1阶段的作业里,我更推荐熟练掌握vector、string、map和sort这类标准库组件。它们相对于手写数组和排序,能显著减少错误率。
比如读入未知个数的整数并排序,用C语言你得自己管理数组大小,用C++就可以这样写:
#include <bits/stdc++.h> using namespace std; int main() { vector<int> v; int x; while (cin >> x) { v.push_back(x); } sort(v.begin(), v.end()); for (size_t i = 0; i < v.size(); i++) { if (i) cout << " "; cout << v[i]; } return 0; }很多初学C++的同学总觉得用vector是“偷懒”,其实这恰恰是正确做法。OJ题目考察的是解决问题能力,不是考察你手写动态数组有多溜。能够用合适的高级抽象把问题解决,这本就是工程师的基本素养。
6. 最后说几句掏心窝的话
每年批改“3.1 OJ”的作业,我都会看到两类截然不同的学生。一类人把题目发到群里,第一句就是“有没有答案”,另一类人安静地把题目截图下来,自己在草稿纸上画数据流,等别人都在聊天的时候,他已经提交通过了。相比天赋,我更相信刷OJ这件事,稳定比聪明更重要。
我见过不少过了这道“3.1”作业的人,后面依然会在第4章、第5章栽跟头,这很正常。因为OJ不是通关游戏,它是耐力跑。你今天掌握了一个输入输出的写法,明天还要面对更复杂的边界条件;你今天避开了大数加法这个坑,明天还会遇到更隐蔽的位运算溢出。但只要你把“3.1”这个环节打扎实了,后面所有的问题都只是时间和练习量的问题。
最后分享一个我自己一直用的笨办法:每次做完一道OJ题,不管AC还是WA,都花30秒在文本文件里记录三件事:题目考察的知识点、我犯的错误、正确的解法思路。坚持记录两三个学期之后,你会发现自己的错误类型高度集中,真正需要警惕的从来不是题目难,而是同一个坑反复跳。