好像集训队互测马上要开始了,简单从vfk的文档中抄写一下怎样配置自己的题目。
题目
只有具有 problemset-permission 的管理员有新建题目的权限。如果你拥有权限,你可以点击题库页面右下方的 “新建题目” 按钮来新建一个题目。
如果你投稿的题目已经被选中,那么青鱼应该在你的互测轮次对应的比赛中给你添加了你的题目的管理权限。
然后,你就可以在题目后台对题目进行管理。后台分为若干个选项卡:
- Edit:主要语言题面编辑页面
- Statements List:题面语言列表与其他语言编辑页面
- Managers:题目管理员列表管理页面
- Permissions:用户访问权限(即,设定哪些用户组可访问本题目)
- Test data:题目数据管理页面
- Upload PDF:上传 PDF 文件作为题面或题解
- Upload Images:将题目图片上传至 QOJ 服务器。
如果你不是超级管理员,请联系超级管理员帮忙添加题目,然后帮你加上管理该题目的权限。
下面我们将逐一介绍这三个选项卡(当然还有最后个 “返回” 选项卡退出后台,就不介绍了),然后我们将详细介绍题目数据需要满足的格式要求。
一、选项卡:编辑
该页面可以对题面本身进行编辑,还可以管理题目的标签。
▶ 编辑题面
题面用 Markdown 编写。
理论上题面是可以自由编写的,但还是有一些推荐的格式。
一些推荐的规则:
- 中文与英文、数字之间加一个空格隔开。
- 输入输出样例用
<pre>标签包围,并且以一个空行结尾。(方便大家复制样例到终端后不用再打回车) - 题面中最高级标题为三级标题。这是因为题目名称是二级标题。
- 名称超过一个字符的数学符号要用 mathrm。例如
\mathrm{sgn},\mathrm{lca}。 - 注意
\max,\min,\gcd都是有现成的。 - 注意 xor 这种名称超过一个字符的二元运算符请用
\mathbin{\mathrm{xor}}。 - 一行内的计算机输入输出和常量字符串表达式请用
<samp>标签,例如请输出 “<samp>NO SOLUTION</samp>”,<samp>aaa</samp> 中有三个 <samp>a</samp>。 - 一行内的计算机代码请用
`括起来,就像上面的规则那样。
可参考下面这个例子:
读入一个整数 $n$,表示题目中提到的 $n$ 位大爷 AC 的总题数。 请分别输出他们 AC 的总题数。
如果你不能正确读入,那么你将获得 $0$ 分。
前 $3$ 个测试点你正确读入即可获得 $6$ 分,第 4 个测试点你正确读入只能获得 $3$ 分。
如果你不会做这道题,请直接输出 “<samp>WOBUHUI</samp>”。
下面是一个样例:
<pre>
233
</pre>
▶ 编辑标签
只需要用英文逗号隔开标签填入文本框就行。
一般而言,标签内包含题目的来源,例如 NOI 2024 Day 2、IOI 2023 Day 2、Europe Championship 2024、Petrozavodsk Winter 2023 Day 5 等等。同一来源的题目标签格式应统一,如果不确定,可在题库中搜索相似来源的题目来参考格式。
超级管理员可以在题库中查看到后缀数字,每个数字代表一个可以查看该题目的权限组。特别的, 题目的权限会由我们配置,保持默认即可。0代表所有用户均可查看该题目。
▶ 多语言题面
待续。 参加集训队互测的应该都会中文,所以没有多语言题面。如果有不回中文的,再说。
二、标签页:管理者
可以通过这个页面增加或删除题目管理员。+mike 表示把 mike 加入题目管理员列表,-mike 表示把 mike 从题目管理员列表中移除。
题目管理员和超级管理员都能看到题目后台,但部分敏感操作需要对应的权限:
- 下载单个题目数据需要用户在拥有此题目管理权限的同时具有
download-tests特权。 - 下载完整题目 package 需要用户在拥有此题目管理权限的同时具有
download-archive特权。 - 自定义题目的 judger 需要用户在拥有此题目管理权限的同时具有
use-custom-judger特权。
三、标签页:数据
配置题目一般分两步,第一步是上传题目数据,第二步是让 QOJ 根据上传来的数据,生成一份供测评使用的数据包。下面我们将依次进行介绍题目上传的流程,而题目的详细配置在下一部分介绍。
▶ 上传原始数据
上传题目时,你需要将题目的所有数据压缩为一个 zip 压缩包。所有数据都应该放在压缩包根目录下,不应该嵌套子文件夹。点击 Upload the test data 后会弹出一个对话框,然后你就可以直接上传一个包含题目数据的 zip 压缩包了。
由于 Cloudflare 的限制,压缩包压缩后的大小不能超过 100MB。如果题目压缩后超过 100MB,你可以:
- 将题目分为多个压缩包上传。后一次上传会添加新的文件(并覆盖同名文件),不会删除此前上传的文件。
- 联系青鱼帮忙直接上传。
虽然这不是一个强制上限,我们建议集训队互测的题目数据大小不要超过 2 GiB,测试点数量不要超过 500。超出此限制请至少提前 48 小时联系小青鱼。
▶ 生成测评数据包(同步测试数据)
上传了原始数据之后,你可以点击右侧按钮 “Sync Test Data” 来让 QOJ 对你上传的原始数据进行格式检查,并最终生成测评时使用的数据包。一般而言,这一步骤主要完成的是如下操作:
- 检查原始数据中的输入输出文件里的换行符。如果是
\r\n,会被自动替换为\n。多余的行末空格也会被替换掉。 - 编译测评所需的文件,比如答案检查器(chk)、标准程序(std)、数据检验器(val)。
- 将 download 文件夹下的文件打包成压缩包,作为该题的附件供选手下载。
总之,你需要在上传数据之后点击同步,检查是否有报错。顺利生成测评数据包之后,这道题的测评功能才会正常工作。
另外还要注意 Hack 功能的开关。如果题目是允许 Hack 的。这意味着你只有在上传 std 和 val 后,“同步” 才会停止报错。但如果你不想写,或者暂时不打算写,你可以点击 “禁止使用 Hack” 的按钮来生成测评数据包。
四、题目数据格式
下面我们先从传统题的配置讲起,然后再依次展开介绍各类非传统题的配置方式。
▶ 传统题的数据格式
1. 数据配置文件
假设你要上传一道传统题,输入文件是 www1.in, www2.in, ... 这个样子,输出文件 www1.ans, www2.ans, ...。你想设时间限制为 1s,空间限制为 256MB,那么你需要在上传这些输入输出文件的同时,编写一个类似下面这样的数据配置文件 problem.conf:
use_builtin_judger on
use_builtin_checker ncmp
n_tests 10
n_ex_tests 5
n_sample_tests 1
input_pre www
input_suf in
output_pre www
output_suf ans
time_limit 1
memory_limit 256
output_limit 64
当然你也可以通过点击 “修改数据配置” 按钮,来在线修改该数据配置文件的内容。
测评类型:use_builtin_judger on 这一行指定了该题使用内置的测评器进行测评,绝大多数情况下都只需要使用内置的测评器。后面我们将介绍如何使用自定义的测评器。
输入输出文件:QOJ 会根据 #!c++ printf("%s%d.%s", input_pre, i, input_suf) 这种方式来匹配第 i 个输入文件,你可以将 input_pre 和 input_suf 改成自己想要的值。输出文件同理。特别的,如果 input_pre 与 output_pre 相同,则可以使用 problem_name www 来同时设置 input_pre 与 output_pre。而如果你的题目不包含前缀,即数据使用 1.in, 1.ans 来存储,则可以直接不填写这一部分,默认即为无前缀。
额外数据:extra test 是指额外数据,在 AC 的情况下会测额外数据,如果某个额外数据通不过会被倒扣3分。额外数据中,输入文件命名形如 ex_www1.in, ex_www2.in, ... 这个样子,一般来说就是 #!c++ printf("ex_%s%d.%s", input_pre, i, input_suf),输出文件同理。
样例数据:样例必须被设置为前若干个额外数据。所以数据配置文件里有一个 n_sample_tests 参数,表示的是前多少组是样例。你需要把题面中的样例和大样例统统放进额外数据里。
答案检查器(chk):数据配置文件中,use_builtin_checker ncmp 的意思是将本题的 checker 设置为 ncmp。checker 是指判断选手输出是否正确的答案检查器。一般来说,如果输出结果为整数序列,那么用 ncmp 就够了。ncmp 会比较标准答案的整数序列和选手输出的整数序列。如果是忽略所有空白字符,进行字符串序列的比较,可以用 wcmp。如果你想按行比较(不忽略行末空格,但忽略文末回车),可以使用 fcmp。
如果想使用自定义的 checker,请使用 testlib 库编写 checker,并命名为 chk.cpp,然后在 problem.conf 中去掉 “use_builtin_checker” 这一行。
时空限制:time_limit 控制的是一个测试点的时间限制,单位为秒,可以是小数(至多三位小数,即最高精确到毫秒)。
memory_limit 控制的是一个测试点的空间限制,单位为 MB。output_limit 控制的是程序的输出长度限制,单位也为 MB。注意这两个限制都不能为小数。
虽然这不是一个强制上限,但我们强烈建议:
- 单个测试点的时间限制不要超过 20.0 秒。
- 单个测试点的时间限制 * 子任务总数 不要超过 300.0 秒。
- 空间限制不要超过 4 GiB
特别的,以下限制为硬限制:
- 预期可得到分数的代码运行时间不要超过 1200 秒,因为超过 1200 秒(20 分钟)的提交会被直接视作 Judgement Failed。
- 空间限制不得超过 6 GiB。
2. 配置 Hack 功能
如果想让你的题目支持 Hack 功能,那么你还要额外上传点程序。
数据检验器(val):数据检验器的功能是检验你的题目的输入数据是否满足题目要求。比如你输入保证是个连通图,validator 就得检验这个图是不是连通的。但比如 “保证数据随机” “保证数据均为人手工输入” 等就无法进行检验。请使用 testlib 库编写,写好后命名为 val.cpp 即可。
标准程序(std):标准程序的功能是给定输入,生成标准答案。默认时空限制与选手程序相同。Hack 时,chk 将会根据 std 的输出,判断选手的输出是否正确。
3. 辅助测评程序
我们暂且将 chk、val、std 这种称为辅助测评程序。上面我们已经介绍了这些辅助测评程序各自的用途,下面我们介绍两个有用的配置。
配置语言:默认情况下,类似 std.cpp 的文件名将会被识别为 C++(即编写 UOJ 核心代码时 g++ 的默认 C++ 语言标准)。
配置时空限制:默认情况下,chk 和 val 的时空限制为 5s + 1GiB,std 的时空限制与选手程序相同。如果你想让这些辅助测评程序拥有更宽松的时空限制,你可以在 problem.conf 中加入
<name>_time_limit 100
<name>_memory_limit 1024
来将其时间限制设置为 100s,空间限制设置为 1024MB。其中,chk 对应的 <name> 为 checker,val 对应的 <name> 为 validator,std 对应的 <name> 为 standard。
4. 测试点分值
题目的满分默认为 100 分。如果想要修改满分的分值,可以使用 full_score S,其中 S 即为分值。
默认情况下,如果测试点全部通过,那么直接给 S 分。否则,如果你有 n 个测试点,那么每个测试点的分值为 S/n。
如果你想改变其中某个测试点的分值,可以在 请在集训队互测中使用子任务捆绑测试。。problem.conf 里加入类似 point_score_5 20 这样的配置(意为把第 5 个测试点的分值改为 20)。
有些题目(特别是提答题)会有给单个测试点打部分分的需求。此时请使用自定义答案检查器 chk,并使用 quitp 函数。在整数模式下,假设测试点满分为 $S$,则
quitp(x, "haha");
将会给 S * x 分。
如果你的题目是一道以子任务形式评分的题目,那么你可以用如下语法划分子任务和设定分值:
n_subtasks 6
subtask_end_1 5
subtask_score_1 10
subtask_end_2 10
subtask_score_2 10
subtask_end_3 15
subtask_score_3 10
subtask_end_4 20
subtask_score_4 20
subtask_end_5 25
subtask_score_5 20
subtask_end_6 40
subtask_score_6 30
可以用 subtask_type_5 <type> 将第 5 个子任务的评分类型设置为 packed 或 min。其中 packed 表示错一个就零分,min 表示测评子任务内所有测试点并取得分的最小值。默认值为 min。
可以用 subtask_dependence_5 3 来将第 3 个子任务添加为第 5 个子任务的依赖任务,相当于测评时第 5 个子任务会拥有第 3 个子任务的所有测试点。所以如果第 3 个子任务中有一个错了,并且第 5 个子任务的评分类型为 packed,那么第 5 个子任务也会自动零分。如果你想给一个子任务添加多个依赖任务,比如同时依赖 3 和 4,那么你可以用如下语法:
subtask_dependence_5 many
subtask_dependence_5_1 3
subtask_dependence_5_2 4
如果某个子任务 $x$ 需要依赖 $1$ 至 $x-1$ 所有子任务,可以使用
subtask_dependence_5 strict
5. 目录结构
下面是存储一道传统题数据的文件夹所可能具有的结构:
www1.in,www2.in, ... :输入文件www1.out,www2.out, ... :输出文件ex_www1.in,ex_www2.in, ... :额外输入文件ex_www1.out,ex_www2.out, ... :额外输出文件problem.conf:数据配置文件chk.cpp:答案检查器val.cpp:数据检验器std.cpp:标准程序download/:一个文件夹,该文件夹下的文件会被打包成压缩包,作为该题的附件供选手下载。require/:一个文件夹,该文件夹下的文件会被复制到选手程序运行时所在的工作目录。
▶ 提交答案题的配置
范例:
use_builtin_judger on
submit_answer on
n_tests 10
input_pre www
input_suf in
output_pre www
output_suf out
So easy!
不过还没完,因为一般来说你还要写个 chk。当然如果你是道 最小割计数 就不用写了,直接用 use_builtin_checker ncmp 就行。
假设你已经写好 chk 了,你会发现你还需要发给选手一个本地可以运行的版本,这怎么办呢?如果只是传参进来一个测试点编号,检查测试点是否正确,那么只要善用 registerTestlib 就行了。如果你想让 checker 与用户交互,请使用 registerInteraction。特别地,如果你想让 checker 通过终端与用户进行交互,请使用如下代码(有点丑啊)
char *targv[] = {argv[0], "inf_file.out", (char*)"stdout"};
#ifdef __EMSCRIPTEN__
setvbuf(stdin, NULL, _IONBF, 0);
#endif
registerInteraction(3, targv);
那么怎么下发文件呢?你可以把编译好的 chk 放在文件夹 download 下,该文件夹下的所有文件都会被打包发给选手。
▶ IOI 风格的交互题的配置
UOJ 内部并没有显式地支持交互式,而是 QOJ 提供了 require、implementer 和 token 这几个东西。
1. require 文件夹
除了前文所述的 download 这个文件夹,还有一个叫 require 的神奇文件夹。在测评时,这个文件夹里的所有文件均会被移动到与选手源程序同一目录下。在这个文件夹下,你可以放置交互库,供编译时使用。
另外,require 文件夹的内容显然不会被下发……如果你想下发一个样例交互库,可以自己放到 download 文件夹里。
2. implementer 代码
再来说 implementer 的用途。如果你在 problem.conf 里设置 with_implementer on 的话,各个语言的编译命令将变为:
- C++:
g++ code implementer.cpp code.cpp -lm -O2 -DONLINE_JUDGE - C:
gcc code implementer.c code.c -lm -O2 -DONLINE_JUDGE - Pascal:
fpc implementer.pas -o code -O2
这样,一个交互题就大致做好了。你需要写 implementer.cpp、implementer.c 和 implementer.pas。当然你不想支持某个语言的话,就可以不写对应的交互库。
如果是 C/C++,正确姿势是搞一个统一的头文件放置接口,让 implementer 和选手程序都 include 这个头文件。把主函数写在 implementer 里,连起来编译时程序就是从 implementer 开始执行的了。
如果是 Pascal,正确姿势是让选手写一个 pascal unit 上来,在 implementer.pas 里 uses 一下。除此之外也要搞另外一个 pascal unit,里面存交互库的各种接口,让 implementer 和选手程序都 uses 一下。
3. token 机制
下面我们来解释什么是 token。考虑一道典型的交互题,交互库跟选手函数交流得很愉快,最后给了个满分。此时交互库输出了 “AC!!!” 的字样,也可能输出了选手一共调用了几次接口。但是聪明的选手发现,只要自己手动输出一下 “AC!!!” 然后果断退出似乎就能骗过测评系统了哈哈哈。
这是因为选手函数和交互库已经融为一体成为了一个统一的程序,测评系统分不出谁是谁。这时候,token 就出来拯救世界了。
在 problem.conf 里配置一个奇妙的 token,比如 token wahaha233,然后把这个 token 写进交互库的代码里(线上交互库,不是样例交互库)。在交互库准备输出任何东西之前,先输出一下这个 token。当测评系统要给选手判分时,首先判断文件的第一行是不是 token,如果不是,直接零分。这样就避免了奇怪的选手 hack 测评系统了。这里的 token 判定是在调用 checker 之前,测评系统会把 token 去掉之后的选手输出喂给 checker。(所以一道交互题的 token 要好好保护好)
不过这样的防御手段并不是绝对的。理论上只要选手能编写一个能理解交互库的机器码的程序,试图自动模拟交互库行为,那是很难防住的。
另外,记得在 C/C++ 的交互库里给全局变量开 static,意为仅本文件里的代码可访问(即仅交互库可访问)。
▶ I/O交互题的配置
这里我们考虑最简单的I/O交互题。有一个交互器 interactor,选手程序的输出会变成 interactor 的输入,而 interactor 的输出会变成选手程序的输入。
如果你想配置这样的题目,只需要在 problem.conf 中加一行
interaction_mode on
然后编写程序 interactor.cpp,跟输入输出文件放在一起即可。interactor 也属于辅助测评程序,可以参照 “辅助测评程序” 这一小节的内容进行配置。比如,通过添加一行 interactor_time_limit 2 可将 interactor 的时间限制设为 2s。
如果你是超级管理员,详细的例子可以通过 QOJ 其他现成的 I/O 通信题来学习。
如果需要 I/O 交互题,或者你的交互题需要反作弊,建议先联系 Qingyu。
▶ 通信题的配置
太难了,联系青鱼吧。
四、样题
在这个仓库里我们 UOJ 公开了一些来自 UOJ 官网的题目数据,供大家参考。
五、还有啥来着
啊反正如果有的地方看不太明白,可以直接传上去试试,还有问题就联系青鱼。爱你。