我看大家都希望牛客的比赛质量可以对标CF,这个我认为这个对于现阶段的牛客有点难度,未来可期吧。我队友是圈内知名选手时津风嘛,之前蹭他的车一起出了CF Round和HDU多校。首先CF他们是有自己的一套标准和流程的,而且非常成熟。至少我认为CF的出题流程是一套工业级别的流程,但是也未必适合牛客,不过或许也有一定的参考价值?
大家都知道cf也并不是没有锅的,但是总的来说他们家题目的出锅率远低于1%,这1%怎么来的,你看完本贴就会知道,这就是cf的流程牛 X的地方了。
我先提前说一句,这套流程我轻易不用,除非是重要比赛或者人手比较多的情况,因为太繁琐了,没5个人以上的人力,你就干个半个月到一个月吧。
一、初审
初审的时候需要向cf那边提供题目的简要概括,也就是这个阶段有个出题的idea就行了。然后cf对你这些idea感兴趣就会跟你详细聊,不然可能会被退回。什么样的题会在此阶段被退回呢?
- 网络中已有的原题,cf至少会检测这个题是不是已经出现在了以前的round中。(当然,如果你跟他们讲“我不确定是不是原题”,cf那边会给你一些建议的方向。即便是“原题”,魔改起来就不那么“原”了)
- 缺乏趣味性的,练习性非常强的题(比如巨型数据结构题:设计一个XX数据结构,支持XX操作,大模拟)
- 整体的难度过高或者过低(比如你试图仍6个F题上去,那大概率初审就被退回了)
对于2并不是说纯数据结构题和纯模拟就上不去,这种题不太符合cf round的风格。除非你这个数据结构维护起来有什么说法,模拟题在模拟的过程中用了什么神奇的思想。这样的题也算是“有趣”的范畴
二、初期录题
Polygon和testlib.h
在cf上录题使用到的工具是polygon和testlib.h库
Polygon也就是俗称的cf后台,cf题库中所有的录题、验题工作全部在这上面完成。它的标语就是“专业的竞赛题目筹备工具”。它是面向全体用户免费的,不论你是否真的在cf出题,只要注册就可以使用Polygon。
testlib.h是一个编写出题相关代码的辅助库,为了降低出锅概率,保证随机数据的强度,降低出题所需的代码量,不管你是不是在cf出题,每一个出题人都要学会使用testlib.h。
你可以在Polygon上下载testlib.h。
对于testlib.h如何使用,洛谷日报,oiwiki早已有非常详细的教程。
2.1. General Info
General Info就是题目的基本信息,包括题目的“标题”,“时间限制”,“空间限制”,“输入文件名”,“输出文件名”。
在初期录题阶段,标题是瞎起的,随手填一个进去就行。
时间限制默认1s,空间限制默认256Mb。
默认输入输出文件是标准输入输出流stdin,stdout。
基本上全部默认就行。
2.2. Statements
Statements就是所谓的题面,cf比赛是支持多种语言题面的,平时的cf round也至少包括英文和俄文。每一种不同语言的题面包括,“题目描述”,“输入格式”,“输出格式”,“备注”,“题解”。
在这个阶段,“题目描述”是瞎填填的,一般是题意的概括(就是没有故事,让你干啥干啥),也不用管LaTeX或者格式,内部人员能看懂就行。“题解”中的内容不会在比赛结束以前公布,不过初期也不用写的很详细,基本上也是瞎糊糊,内部人员能看懂就行。
在此阶段,首先被确定下来的是“输入格式”,“输出格式”这两块内容。尤其是“输入格式”这个一般都不乱写了,直接用LaTeX写的好的。“输入格式”中必须包括每个变量的变量名,含义,变量的数据范围,变量满足的特殊性质。因为下一步要用到,这里不定下来下一步工作没法搞了。
2.3. Validator
Validator也就是数据校验器,它会检查input文件是否严格符合你定义的输入格式,作用包括但不限于
- 检查输入数据的范围是否符合定义。
- 检查题目中的特殊限制条件是否保证。
- 检查数据格式是否严格保证(例如是否多空格,回车等空白字符,行末是否严格存在eol符,文件末是否严格存在eof符,文件是否冗余,是否丢失数据)
- 当数据出现问题时报错,同时报告错误信息。(对于题目中需要保证的特殊性质也可以用ensure自定义一些错误)
除此之外Validator还有一个额外的作用,它在Invocation阶段会产生一个报告:题目中的input涉及到了哪些变量,你的测试用例是否存在至少一个测试点包含该边界值(极大值极小值分开来算的)。如果不存在,Validator会发出警告,提醒你是否是弱数据。
Validator是cf比赛的地基,一旦Validator出现问题,比赛必锅。(很大程度上也是因为Validator和hack机制的直接挂钩,hack数据必须通过Validator的校验,实际上真的有几次cf出大锅ur就是Validator锅了)写好Validator至少降低50%以上的出锅率。
#include <bits/stdc++.h> using namespace std; #include "testlib.h" int main(int argc, char* argv[]) { registerValidation(argc, argv); int n=inf.readInt(1,100000,"n"); inf.readEoln(); for(int i=0;i<n;i++) { inf.readInt(0,1000000000,"a_i"); if(i!=n-1)inf.readSpace(); } inf.readEoln(); inf.readEof(); return 0; }这段代码是Codeforces Round #573 2D1B 的Validator.cpp文件
inf.readInt(1,100000,"n");这句话表示读入一个大小在[1,100000]的正整数,读入的这个变量的变量名是n。(给出变量名很关键,编写Validator的时候不要偷懒,务必给出变量名,便于产生与该变量相关的报告)
2.4. testcase for Validator
既然Validator这么重要,那肯定不是写完就放着了,还需要对Validator单独去创建测试用例。(一般是手出3~5组合法非法的测试用例) 注意,是测试数据校验器的测试用例,不是测试题目用的测试用例。你需要提供几组合法的input,几组非法的input。
这里有个小建议:建议将准备用于题面描述的样例输入也拿过来用于测试Validator。
上传完相应的测试用例点击Run tests就开始测试数据校验器是否正常运行了。
像这样,就运行结束,Validator comment中的内容是Validator的运行结果,人工过一遍是不是符合预期就行了。
2.5. Checker
Checker也就是俗称的裁判程序,不管你是不是需要spj(特判),都需要提供裁判程序。不过cf预置了一些常用的裁判程序,例如“文本比对”,“带误差的浮点数比较”,“忽略大小写的字符串比对”等等。
出传统题目的话直接选fcmp即可。什么?spj啊,自己去学testlib.h咯。
2.6. testcase for Checker
Checker也算比较重要的程序,所以也是需要用测试用例去测的。
如果是用cf预置的程序,可以跳过此步骤。
这里同Validator,你需要举一些选手output和stdoutput的例子测一下。
2.7. pretests和Example
众所周知cf的测试用例集有两个,一个是用于赛中判题的pretests测试用例集,另一个是用于最终的systemtest的最终用例集。 在cf中,所有的测试用例仅包含input文件,不需要上传output文件。所有的output文件都是由input文件+标程自动生成的。
规定example必须出现在pretests当中。
一般手写几个example,也就是样例输入。(对于example,并且仅example可以指定output文件的内容,避免一些特殊的构造题样例暴露std,其他的一般测试点均不能手动设置output内容)
由于cf的报点机制(WA的时候会报第一个WA掉的测试点编号),所以数据集中的测试点采取弱数据在前,强数据在后的的排序方式。
在pretests测试用例集中,前若干个数据点一般都是手出的。并且你还不能乱出,你得避免测试点同质化。
在填写测试点的时候需要注明,为什么要放这个点,也就是要填写Description,测试点不是用来凑数的,它得有目的在里面。“这个测试点是用来干嘛干嘛,它有什么特殊性质”。
2.8. generator
generator也就是数据生成器,俗称gen。
我年少无知的时候在本地造的大数据,然后上传到上面。结果cf那边验题人锤过来了,说你不要上传大型测试点,上传数据生成器。
你的generator中任何所需的参数是需要主函数传参进来的,说来惭愧,我第一次实际用到主函数传参是在cf上写generator,毕竟算法题都是int main(),那会真的没见过int main(int argc, char* argv[])。
#include<bits/stdc++.h> using namespace std; #include "testlib.h" int main(int argc, char* argv[]) { set<int>st; vector<int>v; registerGen(argc, argv, 1); int n=atoi(argv[1]); for(int i=1;i<n;++i) { v.push_back(1000000000-i+1); } v.push_back(rnd.next(1,1000000000)); shuffle(v.begin(),v.end()); println(n); println(v); return 0; }这段代码是我在Codeforces Round #573 2D1B 这道题上编写的其中一个generator。对, 其 中 一 个。
最后是8个gen+一个val,一共9个程序。然而这才刚开始。
2.9. Test Run Script
对于上传好的generator,要编写一个调用脚本去生成测试用例集。这个就不需要什么技巧,就轮着调用一遍,调调参数。(其实写这个也挺烦的)
三、中期录题,验题
一般题目出到这个阶段,cf那边的管理员和验题人就差不多入场了。后序的工作基本上是一块干的。
3.1. Solutions
Solutions是解题程序,包括但不限于“std标算”,“暴力程序”,“瞎JB贪心”,“瞎JB剪枝”。
出题人需要上传
- 出题人认为的标算std
- 可以通过小范围数据的bf暴力程序
- 各种奇怪复杂度,奇妙剪枝,奇妙贪心的程序
- 能够被坑点hack的缺陷程序,最好备注缺陷类型
这个地方是最麻烦的,反正最终你要保证至少有3个以上的ac代码,各种不能通过的代码若干,基本上是和验题人一起干的。
Type这个属性就是代码的类型,表示是WA,AC还是TLE,RE什么情况。Main correct solution表示std。
3.2. Invocations
Invocations基本上就是验题环节了
有数据点,有一下Want to run solutions?这个按钮,它就开始自己跑了。
同时,如果std的运行时间超过0.5倍时限,会黄标警告。cf上的题目基本都杜绝卡常,这里也是一个原因。
如果验题结果不符合预期(该TLE的没有TLE,该WA的代码AC了,或者干脆其中一个AC代码没过),那么这次Invocation会直接报错。
只要在Invocation之前上传了Validator,那么在验题的时候Validator也会自动检查数据的合法性,一旦有非法数据,或者gen生成了非法数据,则立刻中断,标红报错。
反正就是验题没过看错误报告就行了,省心。
跑完之后,会收到一份表格和一个报告。比如这里它诉你,输入的变量"n"没有任何数据命中最小值。(这里能够识别出输入文件的变量名全靠Validator,所以写Validator必须指明变量名)
我觉得至此,你的比赛已经能够杜绝90%以上的锅了。
3.3. Stresses
这个东西是我觉得cf最牛 X的东西,这啥玩意呢,这是大型对拍器。
使用它的时候依赖之前写过的所有东西,包括generator,Checker,Validator和Solutions,总之就是之前写过的所有代码。
使用它的时候需要制定若干的对拍规则。每一条对拍规则以秒为单位,最多可达120秒。也就是两分钟。
编写对拍规则,需要填写Script pattern,即脚本参数(这个就和写短的运行脚本差不多),然后题目的时限,内存(默认和题目一致),选中参与对拍的Solution文件。
你的对拍规则也要是本质不同的,比如“小数据和暴力对拍”,“大范围某个变量符合XX”之类的。
比如这里,每一个对拍规则要强制运行120秒,也就是两分钟。(不跑满不会停的那种)
然后一共18条规则,拍一次拍半小时。
和之前Invocation一样,每一轮对拍都是按照如下的流程不停的循环
调用generator产生数据->调用Validator检查生成数据合法性->运行Solutions->运行Checker->看是不是Solutions符合制定的规则(该T的该WA的,AC的是不是能过)
如果运行的过程中产生了不符合预期的结果,Stresses也是强行中断并报错。
这波之后我真的觉得杜绝了99%以上的锅。然而cf他就是厉害,他还没完,人家后面还有一个步骤,能做到99.9%以上的题目没有锅。
四、题面的打磨,不同语言的翻译
Statements一开始是瞎写的题干,这个时候一般题目数据,程序不会大改了,基本上是打磨一下题面,编编故事,修修语病。
英文题面一般是出题人负责的,俄文全部交给cf那边就好,cf那边管理员验题人基本上都是从录题就全程参与的。
基本上题面都是互相看的,感觉哪里有语病或者加什么描述就随手加上去了。
五、内测
如果你比赛打得多的话经常能看到,thanks XXX,XXX.. for test the contest,这个里面的就是来内测比赛的选手了。
这个是cf那边管理员会去搞的,基本上就是大街上抓俩人,“嘿,有没有兴趣帮助cf测试一下即将到来的比赛”之类的。内测找人的时候不会刻意的去找大佬,基本上就是一个颜色的选手来一个。
内测的时候和正常打cf没什么区别,测试结束之后会给反馈。比如说“XX题目读起来读不懂”,“XX题目难度不符合预期”之类的。
或者由于fst人数过多,需要调整增强pp数据。
六、根据反馈进行调整
除了描述性的修改,很多时候也要对测试数据进行一定的调整。这个时候有人问,那我数据动过了,是不是需要重新验题?诶,还真不用。
因为polygon这种专业级别的工具,肯定是自动化控制的啊,每当你用polygon去生成题目封包的时候,它都会Invocation一次(Invocation启动run script生成测试用例集,Validator校验数据,然后对拍....每当你更新题目的版本,都会自动重复这个自动化验题过程)。如果过程中任何一个程序报错,都会中断package操作。
七、Last Check
在比赛开始之前出题人这边基本上过一遍没啥问题,cf管理员那边会检查一下cf系统是不是正常。这个时候如果发现问题,一般就延个10分钟之类的。
(答疑的时候碰到质疑std,质疑数据的,直接No response打回去,就是这么自信)
八、System Test之前,对于数据的最终调整
CF有的时候数据强它不是强的没有原因的,在比赛结束到System Test真正开始之前,出题人会把一些hack数据加到最终的测试集中。牛客题目的测试用例在赛中强度对标CF,那就不可能,直说了吧,不可能。
比如这个题,出题组若干大佬在手出各种情况,各种生成器,尽其所能所能的去增强数据也只造了152个测试点,但是在System Test这个阶段,测试用例被增加到了194组。
其来源是各种的成功hack。
杂谈
如果想降低比赛的出锅率,建议看一下本帖的2.3、3.1、3.2、3.3这个几个部分。
我跟你讲我这个人很怂的,我真的怕出锅,我早期来牛客出题的时候就是严格按照cf的标准和流程去走的。(虽然我现在基本不走这套流程,因为太烦了,没这个精力)
如果真的有精力,想要把题目出好,不出锅。不妨按照cf的标准和流程试一试。
全部评论
(3) 回帖