工作室内部程序设计训练赛指南:从出题、赛制到复盘 3月4日我们程序设计工作室办了本学期第一场内部训练赛。说是“训练赛”其实没那么多官方气息——上午九点人到齐我拉着一台退役的服务器当判题机两个学长负责出题一个学弟管榜单剩下二十来号人分成三个梯队坐在机房里从热身赛到正赛整整打了一下午。比起以往各学各的这种定期内部赛对新人成长的速度刺激我在过去两年里体会太深了。这篇就好好聊聊怎么策划、出题、组织、复盘一场类似的工作室训练赛把踩过的坑和验证过的经验都摊开说给想在校内搞同类活动的朋友一份可以直接抄的作业。我一直觉得程序设计能力不是靠看教程看出来的也不是靠网上找“python程序设计答案”背出来的而是要在限时、有竞争、有压力的场景里一遍遍试错打磨出来的。工作室内部训练赛的价值就是把平时松散的自学变成有节奏的刻意练习。新人能在三个小时内体验一次完整的竞赛流程老人能保持状态准备区域赛的队伍能通过模拟实战检验配合组织者也能通过榜单数据摸清每个人的强弱项。这篇文章里提到的题目设计、赛制安排、复盘方法基本就是我和身边几所学校工作室朋友反复调整后沉淀下来的版本适合十到五十人的团队规模。1. 为什么工作室要搞内部训练赛1.1 训练赛解决的不只是能力问题新成员普遍有个问题学了一段时间语法和基础算法真上了OJ做题就卡住或者只会做模板题题目稍微换个马甲就完全没思路。这不是智力问题是缺少在陌生问题面前拆解、试错、调整的实战训练。内部训练赛模拟的正是这种陌生感和时间压力——你必须在有限时间里读题、建模、写代码、调试、提交所有动作都像正式比赛一样被计时。这个环境比平时自己刷题更像真实战场练一次顶得过自己闷头写十道题。另外一层价值是氛围。编程是一件正反馈周期很长的活动写十个程序可能只有两三个能跑新人很容易从兴奋滑向挫败。而训练赛带来的即时反馈非常直接榜单在刷新隔壁的人AC了你还在改一个编译错误——这种刺激会推着你往前走。工作室不是培训班讲完课就散了它需要一种共同的目标感和节奏感定期训练赛恰恰是维持这种氛围最轻量的方式。1.2 三张赛表入门、进阶、对抗我们工作室从第二次内部赛开始就不再所有成员打同一套题了。原因很简单新人连基础输入输出还不熟上来打一套包含线段树和网络流的题目基本就是在椅子上坐三个小时看别人AC然后下决心退队。所以我们的内部赛分三档入门组面向刚入工作室、还没接触过竞赛算法的成员题目风格偏向程序设计基础训练比如模拟、简单枚举、贪心、基础排序每题都能用最直观的思路写出来。进阶组面向已经能独立做出中等题目的成员题目里会开始出现二分、搜索剪枝、简单动态规划、数据结构入门风格接近部分省份省赛的低档题。对抗组面向有比赛经验的老队员和准参赛队伍题目强度完全模拟ICPC区域赛的中低难度经常直接用往年区域赛真题改编强调代码量和实现细节。三张赛表同时跑对组织者的出题压力翻倍但收益也是翻倍每个人都处在自己的最近发展区不会因为题目太简单而觉得浪费也不会因为太难而直接放弃。3月4日这场我们就准备了3套题入门5道进阶6道对抗5道总题量够所有人打满三个小时。2. 赛前准备题目设计与人员组织2.1 题目怎么出才不“喂不饱”也“劝不退”一场训练赛的成败七成在题目。我们通常提前三天把题出好然后内部试做一遍。很多工作室搞训练赛翻车就是出题人丢个题干上去就完事了结果不是数据错了就是描述有歧义比赛过半才有人举手说样例和题目对不上。所以我的原则是每道题都必须由出题人自己写完标程所有样例数据必须用标程跑通并覆盖至少一组边界数据。题目难度分布要成阶梯状。以入门组为例第一题一定是签到题——不涉及复杂思路只要知道语法就能过目的是让人快速进入状态拿到第一次AC的兴奋感。第二第三题开始有简单逻辑比如一个模拟题加一个排序题。后面两道就需要动点脑子比如用前缀和优化区间查询或者写个稍微绕一点的DFS。绝不能把难题放在前面否则所有人耗在第一题上后面反而没时间做简单题。检验难度是否合理有个朴素办法出题人自己写标程的时间乘以3到4倍大约就是选手正常所需时间。如果一道题标程只写二十分钟给选手留一个半小时也不算离谱如果标程要写五十分钟这个难度对非正式参赛的成员就偏高了。3月4日那场对抗组有一道模拟题我故意写得比较复杂自己写标程用了四十多分钟结果比赛时只有两支队伍AC其中一支还是卡着时间打出来的这个难度控制还算勉强合格。说到具体出题方向要记得训练赛的目标是练能力不是炫技。我们常用的题目来源有三类一是改编经典题把已知题目改变问法或增加限制二是从真实场景抽象出题目比如用Java Web开发中常见的会话管理来出状态机题目或者拿微信小程序开发里的并发请求做调度模拟——这些题目既贴近工程实践又能控制难度三是直接用往届区域赛或校赛的提交记录和题面自己造数据重新评测。三种方式结合起来既能保证质量也能节省出题时间。2.2 赛制选择与时间安排训练赛的赛制我比较建议用ICPC模式就是ACM队常用的三人一机、提交错误加罚时、按AC数和总用时排名的规则。为什么不用OI赛制因为OI赛后统一评测、赛时只能看题不能提交对新人而言太不友好了看不到实时反馈他们很容易失去耐心。ICPC模式有实时反馈和罚时机制更像体育比赛能让人立刻看到自己的排名变化参与感强得多。时间安排上我们的标准流程是热身赛30分钟正赛3小时中间休息10分钟赛后题解讲解与自由交流1小时。热身赛长度不用太长放两三道极简单的题主要让大家熟悉环境、确认账号、测试提交流程。很多新手第一次用比赛系统不知道代码里有读文件还是标准输入输出热身就是给他们犯错的缓冲期。3月4日的热身赛我特意放了一道“输入一个正整数n输出1到n的累加和”——就这么简单居然有三人提交时读的是文件而不是标准输入好在热身赛阶段发现了不然后面要浪费好几次罚时。正赛的起止时间最好固定一旦开始就不要因为有人迟到而延时。迟到的人自己承担罚时这在正式比赛里也是惯例。训练赛本质上是在模拟真实赛场过分宽容反而会养成坏习惯。2.3 环境和工具清单内部赛不用大规模部署但环境还是得提前确认好。我们的标准配置是这样的机房或实验室的电脑装有统一的编程环境至少支持C/C、Java、Python三种语言。有的成员做Web方向喜欢用JavaScript写题如果题目允许也可以加入Node.js作为评测语言支持但前提是判题环境配好了对应的运行沙箱。判题系统优先用开源的在线评测平台比如开源的OJ项目搭建在内网服务器上。如果短期比赛不想折腾搭建也可以用共享文档配合本地判题把题目发给选手让他们本地跑测试样例再通过脚本批量接收代码并统一测试数据。不过这种方式反馈严重延迟只适合十人以下的小规模活动。数据生成器。这一步经常被忽略但非常关键。比如你出了一道精度题手工造数据永远覆盖不全边界必须写一个随机数据生成器跑一遍标程把输出存成标准结果。生成题目数据的时候记得多生成一组n最大值、数组最大值、空输入、重复输入等边界数据这些最容易卡到选手也最容易暴露标程的漏洞。赛前我还要做的两件事一是用测试账号把每道题的提交、评测、榜单刷新流程完整跑一遍二是准备一个“环境检查清单”发给所有人让他们提前装好IDE、确认编译器版本、测试输出格式。这些事看着琐碎但任何一环出问题都会打断比赛节奏。有一次训练赛我们换了新服务器忘了配Python的评测环境结果有选手用Python交题全部CE折腾了二十分钟才有人举手反馈。从那以后开赛前我会在每台选手机上跑一个“环境自测脚本”一次性检查编译器、解释器和输出路径。3. 从热身到正赛3月4日这天的完整流程3.1 热身赛把坑都踩一遍上午九点半人基本到齐。我在黑板上写清楚服务器地址、比赛链接、登录账号。热身赛十点整开始题目两道难度都刻意压得极低。第一道是AB问题第二道是一个简单的字符统计。这两道题的目的不是筛选谁能力强而是确保每个人都知道怎么打开题目、编译代码、提交并拿到“Accepted”。很多新人紧张到手抖连文件名的前缀都写不对热身赛就是让他们在低压力下完成整个流程。热身赛期间我会安排两个学长四处走动看有没有人卡在环境问题上。曾经有个成员电脑上的Dev-C调试功能有毛病一运行就闪退换了个编辑器才解决。如果热身赛不发现等到正赛再处理会很浪费时间。热身赛结束后我们当场公布热身题题解顺带讲一讲比赛系统的罚时计算规则。罚时是很多新人不理解的东西一道题目如果第一次提交错了要等20分钟才能再次提交同一道题实际规则是“提交错误会产生罚时每错一次增加20分钟罚时AC时间是从比赛开始到成功提交的分钟数”。简单说如果你第30分钟AC了一道题但之前错了两发那你的这道题用时就是302×2070分钟。明白这个规则大家才知道什么叫“提交前多一点思考不要瞎交”。3.2 正赛阶段不同梯队的真实状态正赛十一点开始。我们分三个机房入门组和进阶组在一个大机房对抗组在隔壁小机房。其实分区还有个好处不同梯队的选手不会互相造成心理压力——入门组不用担心旁边的人十分钟就AC了三道题对抗组也不会被新人问基础语法问题干扰。开场前十五分钟是所有人读题时间。我反复强调的一点是别急着敲键盘先花几分钟把所有题目都扫一遍。很多竞赛老手会在前面几道题里找规律哪道题通过率最高、哪道题看起来最长但其实是水题。入门组有人一上来就卡在第三题完全没看后面的第四第五题结果第四题反而很简单白白丢了分。这种情况太常见了所以我在比赛开始前会专门喊一句“先把五道题的题面都看一遍再决定从哪道开始写。”实际效果每次都不错但还是有人不听。对抗组那边三支队伍各有各的分工方式。有的队伍习惯谁先读完题谁抢键盘有的队伍固定一名队员负责数学和思维题、另一名负责代码量大的题目、第三名负责数据结构和难题。3月4日我们观察到一支新组建的队伍在配合上出了明显问题一个人一口气读了所有题另外两个人干坐着等他把题意翻译白白浪费了十五分钟。中场我们轮流过去看情况提了一句“先分头读题十分钟后碰头交流”他们后面就顺畅多了。这种配合问题靠讲没用必须在实战里犯错、纠错。正赛进行到一小时左右新人组已经有人开始焦虑。我看到有个女生盯着屏幕半天没动手过去看了一眼她的代码只写到一半因为怕提交错扣罚时一直不敢交。其实她写的那题基本是对的我让她直接提交结果一次AC。这个案例后来复盘时被当作典型聊了很久罚时只是用来排名的不是什么不可饶恕的罪过为了一个可能的罚时不敢交题反而浪费了更多时间。训练赛就是要逼人改掉这种犹豫心理。3.3 实时榜单与氛围营造我们用的是开源OJ自带的榜单主屏幕投影到大教室幕布上每隔几秒刷新一次。实时榜单是ICPC模式最有魅力的地方——它把所有人拉进一个共同的信息场你看到自己排名掉了就会想赶紧再做一题你看到某道题的通过人数在涨就知道这题可能没有想象中难。3月4日的榜单上入门组有一道“机器人搬运货物”的模拟题AC人数从三个人缓慢涨到六个人虽然不算多但每次刷新都带动几个还在犹豫的人尝试去写。训练赛的氛围就是靠这种透明排名营造出来的。不过也要注意实时榜单可能带来的副作用某些心理承受能力比较弱的选手会盯着自己的罚时次数唉声叹气或者看到排名掉到倒数就完全不想打了。所以我在比赛开始前会强调一句话训练赛的榜单不是用来羞辱人的是用来发现问题的。你可以把内部赛当成一面镜子看到的不是“我菜”而是“我哪类题还不会”。正赛结束前五分钟我让所有人在代码里补上注释并整理思路不要急着做大改。最后几分钟交题往往最容易出低级错误——赶时间改了半个变量名提交后编译失败白白多一笔罚时。倒不如停下来把已经做完的题的代码检查一遍。4. 赛后复盘比比赛本身更值钱的部分4.1 题解讲解的正确方式下午两点正赛结束。榜单定格我们拉出所有题目开始讲题。很多工作室训练赛结束就结束了大家看一眼排名各自散场这是最大的浪费。赛后讲题是训练效果的关键一环必须认真对待。讲题不是把标准代码念一遍。我一般要求每个出题人上台讲出自己的解题思路重点不是说“这题我们用一个数组存状态”而是说清楚“拿到这个题我是怎么想到用数组存状态的”以及“这题除了正解还有哪些常见错误思路”。尤其要把题设里的陷阱讲透——为什么有人会超时为什么有人会WA在边界条件上。3月4日对抗组有一道二分答案的题现场共有四支队伍提交错了11发。讲题的时候出题人专门放了两段错误代码让大家猜错在哪里这种互动式复盘比单向灌输有效太多。如果时间有限至少也要把每道题的AC人数、提交次数、常见错误类型用一两句话带过。对于没AC的选手题解不是让他们抄答案而是给他们一张“更高效刷题地图”这题用到哪几个算法对应哪些经典题可以继续练。所以每次赛后我们都会把题解整理成文档附上标程和AC代码同步传到工作室的在线文档里方便没听懂的人回头再看。4.2 数据复盘与针对训练赛后我会花半小时做一份简单的数据复盘统计每个成员在每道题上的首次AC时间、错误次数、最终提交代码字节数。这些数据一行行看过去能发现许多有意思的规律。比如入门组有人前三题一次都没错第四题却交了5发那说明他的问题在于状态设计或边界处理有人前两题写得飞快第三题开始瞎试直接定位到“读题不仔细”。数据复盘的核心目的不是排名而是为每个人找到下一步的训练方向。我们把成员分成三类处理如果某类题是稳定短板下一周就专门给他布置这类题如果某个人总是卡在模拟题上说明实现能力偏弱让他少看题解多练代码量大的题如果某个人擅长思维题但不爱写代码则强制他参加对抗组训练逼他提高编码速度和熟练度。训练赛本身解决的是“发现问题”后续两周的针对性训练才是真正“解决问题”的阶段。4.3 个人与团队的进步档案从去年开始我给每个正式成员建了一份训练档案每次训练赛的得分、AC题数、薄弱知识点全部记录在案。档案不是用来考核的而是三个月后再回头看的参照物。很多成员光凭记忆觉得自己“好像没什么进步”但把三次训练赛的数据拉出来一对比明明第一次只能AC入门组两题三个月后已经能稳定过进阶组四题了这种实打实的正反馈比任何口头表扬都管用。团队维度也一样。对抗组的队伍档案会记录每次比赛谁写代码、谁查错、谁负责思维推导。几次之后队伍分工的合理性会逐渐浮出水面。3月4日有一支队三个人写代码用三种不同的码风合代码时互相看不懂后来档案里记下“建议统一代码风格至少变量命名要能互相看懂”。这个建议后来帮他们少踩了不少协作的坑。5. 内部赛常见问题与避坑经验5.1 这些坑我基本都踩过搞了十几场内部赛踩过的坑能列一长串。挑最常见的几条说第一数据出锅。有一场我出了一道需要求最大公约数才能过的数学题自己造数据时没考虑分子分母范围结果标程在中间计算时溢出了OJ判题数据全部炸掉赛后被成员吐槽了一个星期。从那以后所有数据生成器里都强制加大数边界测试标程也必须用long long跑一遍极限数据。第二赛前没有测试评测系统。有一次我们图省事用了新搭建的OJ没怎么压过测试开赛后第一批提交就把判题队列堵死了评测延迟十几分钟选手体验极差。这个问题的根源在于缺少一次“试运行”。后来每次训练赛前一天我都要求至少十个人随机交几道测试题模拟真实负载。第三题目说明有歧义。中文题目经常出现“不超过”“不小于”这类模糊表述赛时被反复追问。现在我们的所有题目都会加一句“数据范围0≤n≤10^9保证输入均为非负整数”把所有边界写死歧义降到最低。第四没有准备备用题。训练赛进行到一半经常出现某道题所有人卡住的情况导致后半场没题可做。所以我现在都会多准备一到两道难度居中的备用题如果有人提前做完所有题赶紧放出来维持比赛节奏。5.2 比赛后的滑坡期怎么破内部训练赛有个后遗症比赛那天大家打了鸡血散场后热情迅速滑坡下一周才开始又回到刷手机的老样子。这个现象很正常——高强度刺激之后的倦怠期。我们试过几种方法来对冲一是赛后当天布置一组“赛后小作业”把比赛中没AC的题目重新做一遍要求第二天晚上之前在OJ上提交。这个任务通常不难因为已经看过题解但亲手重写一遍和看别人代码是两码事。二是把下一次训练赛的时间提前定好并放出“题目预告”。比如3月4日结束时就告诉大家下一场会考动态规划专题和字符串基础。这个预告给所有人一周的准备方向很多人为了不想在下一场太难看会主动去做相关练习。三是设立一个简单的“连续打卡”规则每场训练赛的AC题数计入积分赛季末积分可以兑换一些实用奖品比如机械键盘、技术书或者工作室专有的贴纸。这个机制不用很重但能让自律性没那么强的人多一个坚持的理由。5.3 从训练赛到区域赛的衔接内部训练赛不只是给新人玩的。对于准备区域赛的队伍它是成本最低的模拟赛。我们会在正赛环节让参赛战队按照正式比赛规则组队完全模拟现场环境三人共用一台电脑禁言交流以外的一切外部沟通严格罚时。通过这样的模拟队伍能测试出很多问题——比如三个人谁的语言风格更适合在紧张时清晰地描述想法谁更适合在压力下盯细节谁在时间过半时容易慌乱。区域赛队伍还有一个必修课做往年区域赛真题。我们把最近五年的区域赛题目按专题拆开在内部赛的对抗组里循环使用。有些题目数据量太大不适合训练赛的有限时间我们会降数据范围并标注“模拟缩短版”。这样队伍每周都能体验正式比赛的感觉又不至于被完整难题耗光耐心。另外内部赛的一个隐性功能是选拔。区域赛名额有限怎么选最公平连续几场内部赛的综合排名比队长一个人拍脑袋要客观得多。我们统计每次对抗组比赛的有效得分AC时间中位数以下的不计分准确来说是按真实罚时算三个月为一个周期队伍名额根据周期总积分浮动。这套规则写出来贴在墙上所有人都知道该朝哪个方向努力。6. 一些真心建议最后聊几件我自己的体会。内部训练赛这件事最怕的不是办砸而是办砸之后再也不办。如果你所在的团队还没有搞过类似活动相信我第一次不需要太完美题目可以少一点设备可以简陋一点但一定要有一个严格的时间轴和一个愿意认真复盘的组织者。哪怕只有十个人参加也比一百个群友互不交流强。出题这件事不要一个人扛。让每个人都有机会当一次出题人是很好的学习方式——为了出一道自己都不会的题你往往要先把那个算法研究透。我们工作室现在实行“轮流出题制”每场训练赛由一个小组负责题目讲解也由他们来。一个学期下来每个成员的选题、出题、验题、讲题能力都得到锻炼这本身也是程序设计实践的重要内容。再分享一个小技巧把每次训练赛的题目和题解整理成一份“内部题解集”按专题归档。一年下来就是几十道高质量的原创题这些题以后也可以用于招新、校内友谊赛或者社团公开活动。我们自己积累的题解有一部分已经在学院的新生赛上被二次使用过反馈非常好。内部训练赛的终点不是榜单而是让每个人都带着新问题走回平时。赛后我经常站走廊上听到大家还在争论某道题的正解那个时刻我就知道这一下午的时间花得值。