抽象的(不是)

基地之前招新机试一直使用的是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中,注意题目类型为 defaultinteractive型交互适用于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.shexecute.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

运行效果:


  1. 如何正确地在 HydroOJ 出题(作者:rui_er):https://hydro.ac/d/faqs/blog/44/624d931dfcdfb741b9e3f2a4

文章作者: 卡比三卖萌KirCute
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 卡比三卖萌KirCute的博客
运维
喜欢就支持一下吧