如何正确地在 HydroOJ 出题(非官方文档,超级详细)
如何正确地在 HydroOJ 出题
前言
本文同步发布于:HydroOJ。
本博客不是官方文档,仅供参考。同时我不能保证当 HydroOJ 更新时本博客依然有效。
为什么要写个这个呢?还不是因为 HydroOJ 更新了不少东西,但是官方的帮助、文档、FAQ、测试题域都已经过时了,完全没有新的功能的介绍,只能自己摸索,于是就想写这么一篇文章造福大家(?)
当然,这篇文章的内容都是我艰难地摸索出来的,如果我漏了什么好用的 feature,欢迎私信或在评论区告知!(HydroOJ 博客没有评论区
文章较长,快速定位你需要的内容请善用 Ctrl+F。
创建题目
首先进入一个自己有创建题目权限的域(如果没有就创建一个),然后进入题库点击“创建题目”,就进到了编辑界面啦!这个界面相信大家都能看懂,就不多加说明了,写好题面点创建即可。特别地,难度缺省则为自动计算。
接下来会跳到“文件”界面,要求你上传测试点。你可以不立即上传测试点,当然这一步总是绕不过的。下面接着讲测试点的配置,分别举了各种题型当例子,希望可以讲明白 qwq
传统题
我们以 A+B Problem 为例,讲解传统题测试点的配置。
首先你需要写一个数据生成器,并在本地生成出所有测试点,当然 A+B 这种萌萌题直接手打也行。特别提醒,如果用 time(0)
之类的作为随机数种子,为了保证数据强度,请确保没有两组数据在同一秒生成。(好像跑题了
把测试点保存为 plus1.in
、plus1.out
等的形式,注意文件名中必须带有数字,否则可能无法正确识别。
然后把这些测试点文件拖到题目的“文件”界面里的“测试数据”,就上传上去了,当然强大的 HydroOJ(不是打广告)还支持在线编辑文件。
然后呢?然后就没了,快点击“递交”测一测你的 A+B 吧~
如果需要自定义单个测试点分数,或者时空限制,你需要创建一个 config.yaml
,包含如下内容:
score: 20 # 单个测试点分数
time: 1s # 时间限制
memory: 256m # 内存限制
客观题
我们以 1+1 Problem 为例,讲解客观题测试点的配置。
HydroOJ 支持单选题和填空题,大概是出初赛题用的吧。
在编辑题目界面,在题面里写如下内容:
- desc: 请从下面所给的 A、B、C 三个选项中选择最佳选项。
choices:
- A. 1 + 1 = 1
- B. 1 + 1 = 2
- C. 1 + 1 = 3
- desc: 请完成填空:1 + 1 = ?
就准备好了一半。这部分大家对照着我的题面看看就能知道是什么意思了。
由于这题的测试点配置比较独特,我们不再需要 xxx1.in
、xxx1.out
这种东西,只需要一个测试点配置文件 config.yaml
。
对于客观题来讲,文件配置大致如下:
type: objective # 告诉评测机这题是一道客观题
outputs: # 答案列表,格式是 [答案, 分值]
- [B. 1 + 1 = 2, 50] # 选 B. 1 + 1 = 2,得 50 分
- ['2', 50] # 填 2,得 50 分
然后题面中的 desc
之类的奇怪东西就被替换成我们想要的单选框个填空了。
文件读写
如果题目用在模拟赛里的话,可能希望模拟真实比赛环境,加上文件读写,这个 HydroOJ 也是支持的。
我们依然举 A+B Problem 为例子,这时候我们希望选手们从 plus.in
而不是标准输入读入数据,并将答案写到 plus.out
而不是标准输出。
类似于上面“传统题”部分讲的,先把测试点上传上去,然后由于特殊需求,我们也需要写一个 config.yaml
。
这个文件里面只需要写明希望操作的文件名就好了,其他缺省会默认成传统题的一般配置:
filename: plus
子任务和子任务依赖
我们依然以 A+B Problem 为例(谁叫这个最简单呢
害怕脚造数据,或者有时候遇到这种困难,就是不同的乱搞的最差情况不同,卡了一个就放了另一个?没关系,我们有子任务!
一个乱搞过了最大的部分分,却在较强的小数据挂掉了?HydroOJ 还支持子任务依赖,就是只有通过了某些前置子任务,这个子任务才会计分,否则计 \(0\) 分。
在 config.yaml
里面如下配置:
subtasks: # 表示本题采用子任务
- score: 20 # 这个子任务分值
id: 0 # 子任务编号
# type: min # min/max/sum,表示子任务得分怎么由所包含测试点计算得到,缺省默认 min
# time: 1s # 可以给每个子任务设置不同的时空限制
# memory: 256m
cases: # 子任务包含的测试点列表
- input: plus1.in
output: plus1.out
- score: 40
id: 1
cases:
- input: plus2.in
output: plus2.out
- input: plus3.in
output: plus3.out
- score: 40
id: 2
if: [0, 1] # 子任务依赖,这个子任务得分需要 id 为 0、1 的两个子任务都对
cases:
- input: plus4.in
output: plus4.out
- input: plus5.in
output: plus5.out
小技巧:如果把测试数据命名为 xxx1-1.in xxx1-2.in xxx2-1.in xxx2-2.in 这种格式,就会自动归类 subtask
自定义校验器(Special Judge)
依然是 A+B Problem,这题没有 SPJ 的必要,只是作为示例解释如何使用。
首先你需要写一个 checker.cc
(名字可以随便起,注意不是 .cpp
),例如:
#include "testlib.h"
int main(int argc, char* argv[]) {
setName("compares two signed integers");
registerTestlibCmd(argc, argv);
int ja = ans.readInt();
int pa = ouf.readInt();
if (ja != pa)
quitf(_wa, "expected %d, found %d", ja, pa);
quitf(_ok, "answer is %d", ja);
}
然后在 config.yaml
里面注明使用 SPJ 评测:
checker_type: testlib # 根据官方文档,支持 default(忽略行末空格和文末回车), ccr, cena, hustoj, lemon, qduoj, syzoj, testlib,可以选用自己熟悉的,但我只用过 testlib
checker: checker.cc
PDF 题面
如果题目用在模拟赛的话,可能也希望使用 PDF 题面,这也是支持的 Link。
首先要在我的文件(Link)上传 PDF 文件(其他格式也成),注意是我的文件而不是题目文件。
然后题面这么写就行:
@[doc](https://hydro.ac/d/rui_er/file/44/statement-a-plus-b.pdf)
记得把 url 改成自己上传的文件的。
如果有需要展示 PPT 的话,把上面那行的 doc
改成 slide
就行。
ACM 赛制
这里说的不是比赛的赛制,而是题目的赛制。
HydroOJ 的比赛选 ACM 赛制好像一切问题都解决了,不过为了 ACM 练习准备我们还是配置一下。我才不会说是我造完这个才发现有过了。
这个的实现不难想,拿 config.yaml
把所有测试点塞到一个子任务里,这个子任务记 \(1\) 分即可。
subtasks:
- score: 1
id: 0
cases:
- input: plus1.in
output: plus1.out
- input: plus2.in
output: plus2.out
- input: plus3.in
output: plus3.out
- input: plus4.in
output: plus4.out
- input: plus5.in
output: plus5.out
理论上如果是省选以下模拟赛出题人之类的,看到这里就够了,下面是一些特殊题目的配置方法。
提交答案题
单文件提答
A+B Problem,这次我把输入都给你了,求出来输出之后告诉我。我不要程序,只要输出。
由于是单文件提答,我们要求你只提交一个文件,在每一行给出每个问题的答案。
只造一组数据(可以考虑多测来放多组),然后显然需要配置一下 config.yaml
:
type: submit_answer # 告诉评测机这是个提答题
这就完了?确实。
提交方法比较不友善,点进递交发现还是要选代码语言,咋办?交输出还是交代码?
让你交输出就交输出啊,随便选个你觉得可爱的语言直接交就行,就这样:
2919
3
18
12958
19992
多文件提答
A+B Problem,上传数据的时候格式不太一样,由于是提答题评测机不想要你的输入文件,因此输入文件内容改成希望从压缩包中读取的文件名称如 plus1.out
,输出文件不变。
至于 config.yaml
,你还需要告诉评测机是多文件提答,如下:
type: submit_answer
subType: multi
交互题
Grader 交互(函数式交互)
这里吐槽一句:测试题库里面那个函数式交互根本不是比赛中的函数式交互好吗。。
于是自己造轮子,搞一个真正的 Grader 交互的 A+B Problem。
我们先准备好 plus.h
:
//By: Luogu@rui_er(122461)
int inc(int);
int dec(int);
int myPlus(int, int);
然后是我们的 Grader,这里叫 plus.cc
:
//By: Luogu@rui_er(122461)
#include "plus.h"
#include <bits/stdc++.h>
using namespace std;
int inc(int x) {return x + 1;}
int dec(int x) {return x - 1;}
int main() {
int x, y;
assert(scanf("%d%d", &x, &y) == 2);
printf("%d\n", myPlus(x, y));
return 0;
}
考虑一下这种交互怎么实现,选手提交的代码是一些函数,主函数和判题的一些操作在 Grader 里面,那自然就要把这两个文件编译到一起(多文件编译)。
于是就需要知道交上去的文件被存成了啥名字,我在讨论:(已解决)【提问】HydroOJ 是否支持传统 Grader 交互题中提问了,得到的回答是,C 语言在 foo.c
,C++ 语言在 foo.cc
。
HydroOJ 还支持自定义编译方法:写一个 compile.sh
。
于是就可以实现这一功能了。
最终运行时运行的是 ./foo
,所以多文件编译出来的名字要是这个。
compile.sh
:
g++ foo.cc plus.cc -o foo -O2
config.yaml
:
type: default # 传统题!不是 interactive 交互题!
user_extra_files: # 被放到工作目录下的文件
- compile.sh # 用来编译的
- plus.h # 头文件
- plus.cc # 交互库
这是答案示例:
//By: Luogu@rui_er(122461)
#include "plus.h"
#include <bits/stdc++.h>
int myPlus(int a, int b) {
return inc(a) + dec(b); // 直接 a + b 也行,这只是展示一下可以调用我们给的函数
}
I/O 交互
大概是 CF 等在线网站比较常用的交互方式。
A+B Problem,这时我们需要写一个交互库了。
交互库是干啥的?I/O 交互中是用来处理询问和发送数据的,交互库的标准输入是提交的代码的标准输出,交互库的标准输出是提交的代码的标准输入。
本题的交互库就是这样:
//By: Luogu@rui_er(122461)
#include "testlib.h"
#include <bits/stdc++.h>
#include <random>
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;
template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}
int main(int argc, char* argv[]) {
setName("Interactor A+B");
registerInteraction(argc, argv);
rnd.setSeed(time(0)+clock()); // 测试数据不可避免地可能会在同一秒生成,于是下面几行乱搞一下尽量生成得不同,亲测有效
mt19937 myRnd(time(0)+clock()*20);
uniform_int_distribution<int> dist;
rnd.setSeed(time(0)+clock()+rnd.next(0, 10000)+dist(myRnd)+dist(myRnd));
int a = rnd.next(0, 10000); // 生成数据
int b = rnd.next(0, 10000);
printf("%d %d\n", a, b); // 发送给提交的程序
fflush(stdout); // 记得刷新缓冲区!记得刷新缓冲区!!记得刷新缓冲区!!!
int c;
scanf("%d", &c); // 读进来提交的程序给出的答案
if(a + b == c) quitf(_ok, "Accepted! (%d + %d = %d)", a, b, c); // 并判断
else quitf(_wa, "Wrong answer. (%d + %d = %d, but %d found)", a, b, a+b, c);
return 0;
}
显然也需要一个 config.yaml
,如下:
type: interactive # 交互题
interactor: interactor.cc # 我们的交互库
cases:
- input: /dev/null # 没有输入和答案,数据是交互库动态生成的,所以留空
output: /dev/null
- input: /dev/null
output: /dev/null
- input: /dev/null
output: /dev/null
- input: /dev/null
output: /dev/null
- input: /dev/null
output: /dev/null
- input: /dev/null
output: /dev/null
- input: /dev/null
output: /dev/null
- input: /dev/null
output: /dev/null
- input: /dev/null
output: /dev/null
- input: /dev/null
output: /dev/null
通信题
这个我还没搞好,搞好之后再补,可以先参考 @ 的博客 Link。
远端评测题(Remote Judge)
不知道为啥它挂了。
特殊题目
Quine
经典的非传统题了,写一个程序输出自己源代码,包含至少 \(10\) 个非空格的可见字符。
没找到现成的题,自己造的。
小知识:HydroOJ 供 SPJ 获取的存放源代码的文件叫 user_code
。
准备一组空的 1.in
、1.out
,只是占位用,显然这题评测不需要测试点。
类似上面说的 SPJ,我们先配置 config.yaml
:
checker_type: testlib
checker: checker.cc
然后考虑 SPJ 咋写。
我们已经知道咋获取源代码了,就好办多了,直接读文件比较即可,注意去掉行末空格、文末回车。
给个我的实现:
//By: Luogu@rui_er(122461)
#include "testlib.h"
#include <bits/stdc++.h>
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;
template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}
int main(int argc, char* argv[]) {
setName("quine checker");
registerTestlibCmd(argc, argv);
string pans = "", jans = "";
ifstream cod("user_code"); // code stream
int cnt = 0, lines = 0;
while(!ouf.eof()) {
++lines;
pans = ouf.readLine();
getline(cod, jans);
int n = jans.length();
for(;jans[n-1]==' '||jans[n-1]=='\n'||jans[n-1]=='\r';--n);
jans = jans.substr(0, n);
if(pans != jans) quitf(_wa, "Wrong answer on line %d. (Expected '%s', but '%s' found)", lines, jans.c_str(), pans.c_str());
for(auto i : pans) if(i >= 33 && i <= 126) ++cnt;
}
if(cnt < 10) quitf(_wa, "Code is too short.");
quitf(_ok, "Accepted! (%d characters)", cnt);
return 0;
}
其他特殊题目
那就要看你具体想干啥了,仿照 Quine 自己写一个 SPJ 试试吧!