如何正确地在 HydroOJ 出题

前言

本文同步发布于:博客园

本博客不是官方文档,仅供参考。同时我不能保证当 HydroOJ 更新时本博客依然有效。

为什么要写个这个呢?还不是因为 HydroOJ 更新了不少东西,但是官方的帮助文档FAQ测试题域都已经过时了,完全没有新的功能的介绍,只能自己摸索,于是就想写这么一篇文章造福大家(?)

当然,这篇文章的内容都是我艰难地摸索出来的,如果我漏了什么好用的 feature,欢迎私信或在评论区告知!(HydroOJ 博客没有评论区

文章较长,快速定位你需要的内容请善用 Ctrl+F。

创建题目

首先进入一个自己有创建题目权限的域(如果没有就创建一个),然后进入题库点击“创建题目”,就进到了编辑界面啦!这个界面相信大家都能看懂,就不多加说明了,写好题面点创建即可。特别地,难度缺省则为自动计算。

接下来会跳到“文件”界面,要求你上传测试点。你可以不立即上传测试点,当然这一步总是绕不过的。下面接着讲测试点的配置,分别举了各种题型当例子,希望可以讲明白 qwq

传统题

我们以 A+B Problem 为例,讲解传统题测试点的配置。

首先你需要写一个数据生成器,并在本地生成出所有测试点,当然 A+B 这种萌萌题直接手打也行。特别提醒,如果用 time(0) 之类的作为随机数种子,为了保证数据强度,请确保没有两组数据在同一秒生成。(好像跑题了

把测试点保存为 plus1.inplus1.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.inxxx1.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 还支持子任务依赖,就是只有通过了某些前置子任务,这个子任务才会计分,否则计 00 分。

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 练习准备我们还是配置一下。我才不会说是我造完这个才发现有过了。

A+B Problem

这个的实现不难想,拿 config.yaml 把所有测试点塞到一个子任务里,这个子任务记 11 分即可。

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

经典的非传统题了,写一个程序输出自己源代码,包含至少 1010 个非空格的可见字符。

没找到现成的题,自己造的。

小知识:HydroOJ 供 SPJ 获取的存放源代码的文件叫 user_code

准备一组空的 1.in1.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 试试吧!