3.14这个数字懂的都懂——圆周率无限不循环。而3.14 OJ在我眼里恰恰就是这种状态的完美写照你永远刷不完OJ上的题但也正是因为有刷不完的题才一直有得刷、有得学、有得成长。先说句大白话OJ是Online Judge的缩写也就是在线判题系统。你写一段代码提交上去系统自动编译、扔进沙箱跑测试数据然后告诉你结果是对是错、超时还是超内存。杭电OJ、东方博宜OJ、华为机试的练习题甚至国外那些竞赛平台本质都是同一个东西。这篇文章我打算把我这几年来在OJ平台刷题、带新人、甚至自己动手做判题服务攒下的经验一次性讲清楚。适合三类人看刚接触OJ、想系统刷题的算法新手准备校招机试或者考研复试上机的求职考研党以及不满足于刷题、想搞明白判题背后原理的开发者。读完你不仅知道怎么选平台、怎么刷题还能照着代码自己搭一个mini判题服务出来。1. 项目逻辑拆解为什么是3.14OJ到底在训练什么1.1 OJ不是刷题工具而是一套反馈闭环很多人把OJ当成题库觉得上面有题、有答案、能提交就完事了。这么理解不能说错但会漏掉OJ最值钱的东西判题机制带来的提交-反馈-修改-再提交闭环。我经常拿驾考科目二来打比方。你倒车入库压线了考试系统不会告诉你到底是方向盘打早了还是回晚了只会报一个不合格。你只能自己复盘、练车、再考。OJ也是这样——Wrong Answer就是Wrong Answer它不会告诉你哪组测试用例挂了、你的程序输出和标准输出差在哪里一切都要靠你自己去推。这种黑盒反馈逼着你做两件事第一建模的时候想清楚所有边界条件第二调试的时候学会系统性地排查问题而不是瞎改碰运气。这两件事恰恰是工程开发里最核心的能力。写业务代码的时候需求不明确、边界条件一堆、线上bug复现不出来本质上和面对一个WA的状态是一模一样的。所以你说OJ是刷题工具也好、是竞赛训练场也好在我看来都不如反馈闭环训练器这个定位准确。1.2 从热搜词看OJ的刷题场景早就出圈了说到OJ我的感觉是这个词的热度这几年一直在涨而且涨得很有意思。你看搜索词里不仅有OJ刷题OJ平台这种通用词还有华为OJ对应大厂机试刷题东方博宜OJ对应信息学教学场景oj在线判题系统项目java鱼皮对应开发者想自己动手搭OJ的需求甚至还有高校平台的注册和答案搜索。这说明OJ早就不只是ACM竞赛圈子的专属工具了。它已经渗透到了至少四个场景大学课程作业和考试、考研复试上机、校招笔试机试、中小学信息学竞赛。每个场景对OJ的诉求还不一样。大学生要的是能过课程测试用例求职者要的是适应机试环境和时间限制竞赛党要的是高难度题目和全球排名。所以后面聊平台选型的时候我特意按照这几类人群去拆而不是简单说哪个OJ题多就选哪个。回到3.14 OJ这个名字。3.14是圆周率的前三位后面跟着无限不循环的数字。OJ题库也是这样题目永远在更新解法永远有优化空间。但换个角度看π的每一位都是确定的就像每一道题都有标准答案你只要按部就班地训练AC就是AC。这种看似无限实则每一步都可验证的状态我觉得就是OJ最迷人的地方。2. 核心细节解析判题系统的工作原理与平台选型2.1 判题核心流程你的代码是怎么被审判的很多人在OJ上提交代码提交了好几百次但从来没想过后台到底发生了什么。我因为自己动手写过判题服务所以对这套流程印象特别深拆开来看其实就五个环节。第一步是提交。你从前端页面把代码贴上去后端先把代码存下来记下题目编号、用户ID、提交时间。第二步是编译。判题机会根据你选的编程语言调用对应的编译器。比如说C/C就调gcc/gJava就调javacPython一般不编译而是直接走解释器。编译这一步挂了返回的状态就是CECompile Error。第三步是沙箱运行。编译出来的可执行文件会被丢进一个隔离环境里运行这个环境限制了它能用的内存、CPU时间甚至文件系统权限。为什么一定要隔离两个原因一是防止恶意代码比如有人提交一个死循环或者一个删文件的程序搞破坏二是保证公平把所有程序放在同样的资源配置下跑。第四步是喂测试数据。判题机把题目的输入文件喂给正在运行的程序程序处理后产生标准输出。第五步是比对。判题机把你的输出和事先准备好的标准答案放在一起比对完全一致就是ACAccepted不一致就是WAWrong Answer。这整个过程通常要求在非常短的时间内完成还经常是并发处理几十上百份提交。所以你会发现好的OJ平台后端的任务队列、资源调度、缓存设计都做得很讲究这些对想自己搭OJ的人来说都是绝佳的练手项目素材。2.2 常见判题状态AC之外的那些死法新手刚上OJ的时候最懵的事情之一就是看到各种莫名其妙的缩写。我把常见的状态码整理成了一张表每个我都配了实际的踩坑场景你对照着看会清楚很多。状态码全称含义常见原因ACAccepted答案正确无恭喜WAWrong Answer答案错误算法思路不对、边界条件漏了、精度问题TLETime Limit Exceeded超时算法复杂度过高、死循环、IO太慢MLEMemory Limit Exceeded超内存数组开太大、递归栈溢出、内存泄漏RERuntime Error运行时错误数组越界、空指针、除零、栈溢出CECompile Error编译错误语法错误、头文件缺失、版本不兼容PEPresentation Error输出格式错误多了空行、空格、大小写不一致OLEOutput Limit Exceeded输出超限死循环里疯狂打印、输出过大这里我特别想聊一下PE。很多萌新看到PE会以为我的答案是对的只是格式有点问题然后在评论区嚷嚷。但实际比赛中PE通常被算作错误而且大多数OJ不会单独标PE直接归进WA。我最早在杭电OJ刷AB题的时候就因为输出最后多了一个空格从PE改到WA又改到AC来回折腾了快半小时。后来养成习惯输出行末不要有多余空格题目要求换行就老老实实换行这种细节真的是基本功。还有一个容易被忽略的是TLE。TLE不一定代表你的算法是错的更可能是复杂度压根撑不住数据范围。打个比方题目给的数据量是10万你写了个O(n²)的冒泡排序1秒的时间限制基本稳挂这时候你把cin换成scanf也救不回来只能换算法思路。要能提前判断这点就得会算时间复杂度。2.3 主流OJ平台怎么选别盲目跟风按目标来我见过太多人一上来就问哪个OJ最好这种问题其实没法答因为不同OJ的定位完全不同。我把几个用的人最多的平台列出来你自己对照需求选。平台特点适合人群杭电OJHDU OJ老牌ACM题库题量大经典题目多竞赛入门、想打基础的人东方博宜OJ界面友好题库分层明确信息学初学者、中小学场景华为OJ牛客网机试练习模拟企业机试环境三题模式准备校招、机试的求职者Codeforces全球顶级竞赛平台实时Rating想进阶、想挑战思维的竞赛党AtCoder日本人气平台题解质量高从入门到进阶都适合各大高校OJ郑轻OJ、XTU OJ等校内教学题贴合课程和考试在校生刷作业、准备期末补充一个选型逻辑。如果你是为了考研复试上机优先看你目标院校自己的OJ或者同类难度的高校OJ题目风格和难度最接近。如果你是为了校招机试那华为OJ或者牛客网上的企业真题更合适因为这些场景的输入输出处理往往比纯算法题更繁琐需要单独练。如果是为了打竞赛Codeforces和AtCoder的题目质量确实比多数国内平台高讨论氛围也好题解思路能看到很多种解法。我曾经带过一个新人一开始抱着杭电OJ刷题刷到一百多题还是感觉没进步后来发现他全在刷最简单的AB类水题没有梯度。这就是典型的平台选对了但策略没对——同一个平台上题单的选择比平台本身更重要这个问题我在下一个章节细讲。3. 实操过程从零到一规划你的OJ刷题路线3.1 先定目标再定平台我观察到一个特别普遍的现象很多人刚开始刷OJ的时候热情高涨第一天立flag说要刷500题结果一周后连50题都没到然后就放弃了。问题不在于意志力而在于目标太模糊。刷题之前你得先回答一个问题——你到底为了什么而刷如果你是为了大学课程和期末考试那目标很明确把老师讲的知识点对应的题刷熟反复做本校OJ上的往年题。这种场景不需要贪多把数组、字符串、结构体、排序、指针如果学C、递归这些基础点搞扎实就够了。如果你是为了考研复试上机那重点是往年的机试真题尤其是目标院校爱考的题型比如模拟题、字符串处理、简单图论难度通常不超过OJ入门到中等。如果你是准备校招机试那就要两线并进一边刷经典数据结构和算法题另一边练快速处理输入输出的手速因为机试有时间压力很多题不是不会做是没时间做完。目标定清楚了刷题的题单、节奏、复盘方式全都不一样。定完目标之后我强烈建议按专题刷题法来推进而不是按题目序号盲目刷。比如这周只刷二分查找专题那就把各个平台上二分相关的题集中刷20道下周刷动态规划就只刷DP。这样做的好处是你会在一段时间内反复接触同类问题的不同变体大脑很容易总结出这类题目的通用套路。我自己的体会是10道同专题题的收获远大于分散刷50道不同类型的题。3.2 一套可复现的刷题SOP从看题到AC的完整流程刷题绝对不是看题-写代码-提交-看结果这么简单。我刷了几年题带过不少人慢慢总结出一套固定流程按这个走效率和AC率都会明显提升。第一步读题三遍建模之前先画样例。很多WA的原因不是不会做而是把题目意思理解偏了。我要求自己先看输入输出格式再手动模拟样例数据确认自己完全明白题目在问什么才动手。特别是那种描述很长、带故事的题目核心往往就一两句话读题的时候先把约束条件圈出来数据范围、时间复杂度暗示、特殊边界。第二步估复杂度想清楚能不能过。拿到一道题看一眼n的范围心里马上要有个数。一般1秒的时间限制下C/C大概能跑10^8次简单运算Java慢一些Python更慢。如果n是10^5你准备写O(n²)的算法那基本宣判TLE这时候就得想有没有O(n log n)或者O(n)的做法。这一步我每次写代码之前都会做哪怕是一个看起来很简单的题因为很多简单题的坑恰恰就在隐藏数据范围里。第三步动手写代码注重可读性。给自己看的代码也要注意结构关键函数单独拆出来变量名别用a1、b2这种不然过了两周自己都看不懂。调试的时候你会发现代码结构清晰能省下一半时间。第四步本地自测先把边界值喂一遍。这一步很多人偷懒写完了直接提交然后被WA打回来再调试来回浪费时间。正确的做法是在本地把几类数据先测掉最小数据比如n1、数组为空、最大数据看看会不会爆int、重复数据比如所有元素都相等、特殊数据负数、0、空字符串。我之前刷一道题本地随便测几个用例都过了一提交就WA最后发现是数组越界恰好OJ的评测数据里有边界用例而我自己没测到。从那以后边界自测再也不敢省。第五步提交然后无论AC还是WA都要复盘。AC了也别急着开下一题想想还有没有更优解WA了更要仔细分析是算法错、边界漏了还是格式问题。真正的能力增长永远发生在复盘环节而不是提交通过的那一秒。这套流程听着麻烦但一旦养成习惯做题速度反而会提升。因为大部分错误都被拦截在提交之前你不会反复因为同一个低级错误被OJ打回。3.3 进阶玩法自己动手搭一个mini OJ判题服务刷题刷到一定阶段很多人会冒出这样一个念头我自己能不能写一个OJ搜索词里那个oj在线判题系统项目java鱼皮就说明这个需求很普遍。我给自己的回答是能而且是一个非常棒的练手项目因为它同时涉及前端、后端、任务队列、进程管理、编译器调用、沙箱设计技术点非常密。我们先说最简架构。一个mini OJ至少要包含四个模块题目管理模块存题目描述、输入输出数据、提交模块接收用户代码、记录提交状态、判题核心模块编译、运行、比对输出、前端展示模块排行榜、提交列表、题目列表。判题核心是最有意思的部分也是难点所在。我拿Java打比方一个最简判题流程可以这样写。假设你已经把用户代码存成了本地文件Main.java现在要编译并运行它然后拿运行结果和标准答案比对// 判题核心编译 运行 比对极简版 public class JudgeCore { public static JudgeResult judge(String userCodeDir, String inputFilePath, String answerFilePath, int timeLimit) { JudgeResult result new JudgeResult(); // 1. 编译阶段 ProcessBuilder compilePb new ProcessBuilder(javac, userCodeDir /Main.java); Process compileProcess null; try { compileProcess compilePb.start(); boolean finished compileProcess.waitFor(10, TimeUnit.SECONDS); if (!finished) { compileProcess.destroyForcibly(); result.setStatus(TLE); // 编译超时 return result; } if (compileProcess.exitValue() ! 0) { result.setStatus(CE); // 编译错误 return result; } } catch (Exception e) { result.setStatus(SE); // 系统错误 return result; } // 2. 运行阶段带输入重定向 ProcessBuilder runPb new ProcessBuilder(java, -cp, userCodeDir, Main); runPb.redirectInput(new File(inputFilePath)); runPb.redirectOutput(new File(userCodeDir /user_output.txt)); runPb.redirectError(new File(userCodeDir /user_error.txt)); try { Process runProcess runPb.start(); boolean finished runProcess.waitFor(timeLimit, TimeUnit.SECONDS); if (!finished) { runProcess.destroyForcibly(); result.setStatus(TLE); // 运行超时 return result; } if (runProcess.exitValue() ! 0) { result.setStatus(RE); // 运行时错误 return result; } } catch (Exception e) { result.setStatus(SE); return result; } // 3. 输出比对 try { String userOutput new String(Files.readAllBytes( Paths.get(userCodeDir /user_output.txt))).trim(); String answer new String(Files.readAllBytes( Paths.get(answerFilePath))).trim(); if (userOutput.equals(answer)) { result.setStatus(AC); } else { result.setStatus(WA); } } catch (IOException e) { result.setStatus(SE); } return result; } }这个代码虽然简单但已经能跑通编译-运行-比对的主链路。需要注意的是这只是用来理解原理的玩具版本生产环境里的OJ远比这个复杂。最大的区别在两点一是沙箱隔离真实OJ会把用户程序丢进Docker容器或者用seccomp这类内核级机制做资源限制防止死循环、内存炸弹或者读系统文件二是评测数据真实OJ每道题会准备几十上百组测试数据还有Special Judge这种允许输出不唯一的判题模式不是简单字符串比对就行的。如果你真的想把这个项目做完整我建议按这个顺序来先写命令行版本的判题核心保证能判AC/WA/TLE/RE然后接一个简单的web界面支持注册登录和提交代码最后再引入数据库做题目管理和排行榜。每一步都能独立验收不会做到一半因为太复杂而放弃。4. 常见问题与排查技巧实录4.1 刷题翻车现场本地能过提交就挂如果让我统计一下带新人的时候提问频率最高的一句话那一定是我本地跑得好好的为什么提交上去就WA/TLE/RE。这个问题几乎每个人都会碰到背后的原因来来去去就那几个。数组越界是头号杀手。C/C里数组越界是未定义行为本地小数据可能碰巧没崩OJ大数据一到直接RE或者输出错乱。我在杭电刷过一道题本地试了5组数据全对提交WA了整整7次最后用for (int i 0; i n; i)排查了半天才发现是i n数组开小了1个位置。这种问题唯一的解法就是养成习惯开数组的时候在题目要求的基础上多开几个a[100005]而不是a[100000]用1-based索引就开n 5。循环边界写错导致死循环是TLE的常见来源尤其是while (l r)这种二分边界建议在所有循环入口打上明确的退出条件。全局变量没有复位也很坑多组测试数据的题目上一组的数组和计数器没清零直接污染下一组我的习惯是每次循环开头统一memset。输入输出格式问题则是最冤的一种挂法。题目要求多组输入读到EOF你只处理了一组就退出自然WA。C语言要用while (scanf(%d, n) ! EOF)C用while (cin n)Java用while (sc.hasNextInt())这个模板我建议直接背下来。还有行末空格、输出大小写、浮点数保留位数这些都是PE和WA的源头。4.2 OJ平台FAQ与避坑清单我把这些年见过的高频问题整理成一个速查表新人遇到问题可以先翻这里大概率能解决。问题产生原因解决建议本地跑正确提交WA隐藏测试用例边界没覆盖自测边界值检查数组越界、整数溢出、浮点精度逻辑正确但TLE时间复杂度太高降低复杂度优化IO避免无意义的拷贝用cin/cout超时流同步导致IO速度慢加ios::sync_with_stdio(false)或改用scanf/printf浮点数比较出错直接用了用fabs(a - b) 1e-8判断相等int范围不够数据超过2^31-1换long long注意Java用long递归层级过深递归栈溢出改迭代或手动扩栈多组输入只处理一组没有处理EOF按平台模板循环读入再分享几个我在实际刷题中摸出来的独家经验。第一C的bits/stdc.h虽然好用但不是所有OJ都支持保守起见还是写全头文件。第二Java玩家慎用Scanner处理大输入一个10万行的数据用Scanner和用BufferedReader的效率差距能到好几倍机试的时候这差距就是TLE和AC的差别。第三别太迷信代码越短越好我见过为了把代码压短而牺牲可读性的新手最后自己都看不懂调试起来加倍痛苦。第四提交前看一眼题目的空间限制和栈限制有的OJ默认栈空间很小递归稍微深一点就直接RE。最后说一个心态层面的坑。很多人连续WA几次就开始烦躁然后开始瞎改代码越改越乱最后整个重写。我自己的做法是连续WA三次就停下来把手头代码放一边重新读一遍题在纸上把算法流程画一遍往往是画着画着就发现自己哪个边界漏了。刷OJ最忌讳的就是用提交次数来撞答案那是把时间花在感动自己上。说实话我在OJ上拿到的第一个AC是杭电的1000题AB Problem。那天下午我先后经历了CE、PE、WA最后AC弹出来的时候整个人从椅子上跳起来了。后来自己写判题服务、带新人刷题遇到的bug千奇百怪但主线永远是同一条提交、反馈、复盘、再提交。3.14后面的小数是无限不循环的OJ的题单也是无限延展的但这条路上每一步都能验证、都能复盘这也是我觉得它值得投入的原因。最后送一个小建议别只盯着AC数认真对待每一次WA和TLE那才是长本事最快的地方。
阅读完成 · 觉得有帮助?