在 Hydro OJ 上配置基于 Grader 交互的类 LeetCode 题目
抽象的(不是)
基地之前招新机试一直使用的是Hydro OJ作为机试平台,作为对调库跑包能力要求比较高的竞赛团队,传统OJ平台采取的基于标准输入输出流的答案逐字匹配方案饱受诟病,急需一种可以让选手作答类LeetCode题目(不编写主函数且不使用标准输入获取数据,而是编写自定义函数且从形参获取数据)的方案。之前我们曾经尝试过手改题目,但是出分效率太低且不能让选手现场得知自己的分数。经过查找有关资料终于得知此类题目的交互方式有一个专有名词叫Grader交互,本文参考教程《如何正确地在 HydroOJ 出题》1给出几类Grader交互题目的命题方式。
命制最基础的Grader交互类题目
Grader交互的基本原理是:既然我们不希望选手代码编写主函数并与输入输出样例直接交互,那么我们就可以考虑自行编写一个主函数与选手代码链接,用链接好的完整代码进行测评。
传统的交互方式是这样的:
Grader交互类题目的交互方式是这样的:
以Problem A+B为例,首先编写 main.cc
,内容如下,该文件后续将与选手代码共同编译。
#include "stdio.h"
extern float plus(float x, float y);
int main() {
float x, y;
scanf("%f%f", &x, &y);
printf("%.1f", plus(x, y));
return 0;
}
extern
关键字是C语法,表示这个函数的定义不在这个文件中给出,在主函数中,我们读入两个 float
型变量并将其调用 plus
函数的计算结果打印出来。
接下来编写样例,本文使用的样例如下所示:
序号 | .in | .out |
---|---|---|
1 | 1.0 2.0 | 3.0 |
2 | 0.1 0.2 | 0.3 |
3 | 1.0 0.5 | 1.5 |
编写 compile.sh
规定编译方法,在Hydro OJ中,用户的代码会被命名为 foo.cc
,因此在 compile.sh
中写入如下内容:
g++ foo.cc main.cc -o foo
编写 config.yaml
,将几个额外文件添加在 user_extra_files
中,注意题目类型为 default
,interactive
型交互适用于IO交互型题目。
type: default
user_extra_files:
- compile.sh
- main.cc
subtasks:
- score: 100
id: 1
type: sum
cases:
- input: 1.in
output: 1.out
- input: 2.in
output: 2.out
- input: 3.in
output: 3.out
递交如下代码测试评测系统:
float plus(float x, float y) {
return x + y;
}
结果:
Grader交互与SPJ结合
Grader交互还可以与SPJ结合使用,这使得对题目正确性的判定可以完全放进 main.cc
中,main.cc
可以对选手的接口实现检查完毕后,将成绩通过标准输出的方式传递给SPJ,而SPJ仅根据结果直接得出成绩。这么做的好处是可以实现LeetCode上常见的设计型题目,边存边取类题目,还可以命制一些题目输入与选手代码的中间结果有关的题目(举例:题目有三个子问题A、B、C,其中子问题A有0和1两个正确答案,如果选手代码对子问题A回答0,要求正确回答子问题B,不需要回答子问题C,如果选手代码对子问题A回答1,要求正确回答子问题C,不需要回答子问题B)。
继续创建评测文件 checker.cc
,内容如下:
#include "testlib.h"
#include <cstdio>
#include <cmath>
int main(int argc, char* argv[]) {
setName("Problem A+B");
registerTestlibCmd(argc, argv);
double o = ouf.readReal(), a = ans.readReal();
if (o == a) quitf(_ok, "Completely correct.");
else if (abs(o - a) < 0.5) quitf(_pc(50), "Partly correct.");
else quitf(_wa, "Wrong answer.");
return 0;
}
使用该SPJ,当选手输出与标准答案完全相等时,测试点得全部分数,若选手输出与标准答案的差距在0.5以内,得到一半分数,否则得0分。
修改 config.yaml
启用SPJ:
type: default
checker_type: testlib
checker: checker.cc
user_extra_files:
- compile.sh
- main.cc
subtasks:
- score: 100
id: 1
type: sum
cases:
- input: 1.in
output: 1.out
- input: 2.in
output: 2.out
- input: 3.in
output: 3.out
递交如下代码测试评测机:
float plus(float x, float y) {
return (int)x + (int)y;
}
结果:
使用一个下面这样的SPJ,可以让Grader交互代码 main.cc
直接向标准输出输出一个0-100之间的分数作为测试点的分数:
#include "testlib.h"
#include <cstdio>
int main(int argc, char* argv[]) {
setName("Problem A+B");
registerTestlibCmd(argc, argv);
quitf(_pc(ouf.readInt()), "");
}
Grader交互与I/O交互结合
使用Grader交互与SPJ结合,已经能完成大部分题目的命制,但Grader+SPJ有一个很严重的缺点,就是题目接受的每一种语言,都需要有一个Grader与之对应,在复杂的题目背景下使用Grader+SPJ的组合,对作答正确性的判断很可能位于Grader中,这将使命题组花费大量时间用于为每一种语言编写Grader。Grader与I/O交互结合命题可以有效解决这一问题,在此类问题中,对作答正确性的检查交由只需要一个版本的交互器完成,Grader只负责将来自交互器的输入翻译为对具体接口的调用,命题成本大大降低。
在“命制最基础的Grader交互类题目”章节命题的基础上,创建交互代码 interactor.cc
。
#include "testlib.h"
#include <cstdio>
#include <cmath>
int main(int argc, char* argv[]) {
setName("Problem A+B");
registerInteraction(argc, argv);
double x = inf.readReal(), y = inf.readReal();
std::cout << x << ' ' << y << std::endl;
std::cout.flush(); // 必须要有
double a = ans.readReal(), res = ouf.readReal();
if (abs(res - a) < 0.01) quitf(_ok, "Accepted");
else quitf(_wa, "Wrong answer");
}
修改 config.yaml
以启用交互器:
type: interactive
interactor: interactor.cc
- compile.sh
- main.cc
subtasks:
- score: 100
id: 1
type: sum
cases:
- input: 1.in
output: 1.out
- input: 2.in
output: 2.out
- input: 3.in
output: 3.out
递交“命制最基础的Grader交互类题目”章节的代码测试评测机:
除此之外,使用I/O交互还可以实现随机生成输入数据,相关方法在《如何正确地在 HydroOJ 出题》1中已有论述,本文不过多赘述。
为C/C++以外的语言配置Grader交互
到此为止本文都讲述的是C/C++语言作答情况下的Grader交互配置,对于非C/C++语言尤其是解释型语言而言,若干源文件无法像C++那样用类似gcc的工具编译为一个可执行文件,应该如何配置Grader交互呢。
Hydro OJ网站管理员在“控制面板->系统设置->编程语言设置”中可以看到Hydro OJ是如何处理各种编程语言的源文件的,比如对于Python 3,其使用以下shell指令进行编译:
/usr/bin/python3 -c "import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True)"
使用以下shell指令来执行:
/usr/bin/python3 foo
对于Java,其使用以下shell指令编译:
/usr/bin/bash -c "javac -d /w -encoding utf8 ./Main.java && jar cvf Main.jar *.class >/dev/null"
使用以下shell指令执行:
/usr/bin/java -cp Main.jar Main
如果要为C/C++以外的语言配置Grader交互,我们就需要自己定义编译和执行方法,分别将其写入 compile.sh
和 execute.sh
中,并像上文那样将其写入 config.yaml
文件的 user_extra_files
字段中。
但是经过亲自尝试,笔者并没有成功为Python 3作答配置多文件编译的Grader交互评测机,遇到的主要报错为Interrupt、Hungup等多种Runtime Error。这里提供一种合并评测代码的替代方案,比如对于Python 3,我们首先编写以下Grader交互程序 main.py
:
# 这里的几个空行是故意的,
# 这是为了防止选手的作答结尾没有空行,
# 导致Grader代码首行与作答尾行直接连接造成错误
if __name__ == '__main__':
inp = [float(s) for s in input().split()]
print(plus(inp[0], inp[1]))
接着我们编写编译脚本 compile.sh
,在其中将两个脚本直接拼接:
cat main.py >> foo.py && /usr/bin/python3 -c "import py_compile; py_compile.compile('/w/foo.py', '/w/foo', doraise=True);"
编写如下 config.yaml
:
type: default
user_extra_files:
- compile.sh
- main.py
subtasks:
- score: 100
id: 1
type: sum
cases:
- input: 1.in
output: 1.out
- input: 2.in
output: 2.out
- input: 3.in
output: 3.out
langs:
- py.py3
- py
递交如下代码测试评测系统:
def plus(x, y):
return x + y
运行效果:
如何正确地在 HydroOJ 出题(作者:rui_er):https://hydro.ac/d/faqs/blog/44/624d931dfcdfb741b9e3f2a4 ↩ ↩