自制题目、SPJ、交互等 详细教程(含举例)
前言
写这篇文章还是因为猜交互题应该怎么配置,一个下午没弄出来红温了写的。
感觉洛谷帮助文档讲自制题目、SPJ、交互不是很详细。
而且举例仅有代码,没有别的(例如放置位置,注意事项等)详细说明。
所以这篇文章就来详细讲讲。
正文
Part 1 自制题目
大部分人都自制过题目吧。这里仅为基础内容介绍。
少部分第一次自制题目没有经验的,建议看这里。
1.1 题目创建
题目创建分为两类:个人与团队。
普通的题目创建只能创建两类:U 题库(个人)或 T 题库(团队)。当然你是管理员的话当我没说。
个人题目创建方法:打开个人主页-题库-我创建的题目-创建题目即可创建题目。书写好各项信息之后即可保存。
团队题目的创建方法类似。
新手一开始可能会乱点标签,这个不要紧,但是记住别点到特殊题目标签就行了。至于这些标签有什么用,将会在接下来提到。
1.2 题目数据
如果你要配置一个题目的数据,你需要至少
这些输入数据和输出数据应当成对出现,其输入数据文件的扩展名为 .in
而输出文件的扩展名为 .out
。同时,测试点文件名中只能允许有连续的一段数字。例如 t1.in , 1.in , problemA1.out
等。形如 T1-1.in
的形式是不被允许的。
如官方所说,直接将若干数据点打包成一个 zip 压缩包,rar 和其他格式不能成功。
可以用 windows 自带的方式压缩压缩包,或者下载 7zip 等压缩工具。
注意此处有一个细节:
如果你配置完成后,双击打开 zip 文件夹发现里边是一个文件夹,文件夹中存放着其他文件,那这样的压缩包就是不被允许的。
解决这种问题很简单,直接点入没被压缩过的文件夹,框选所有需要的文件再压缩就好了。
注意文件夹中没有任何文件夹或者其他无关文件。经过我们自行测验,压缩前大小不能超过 100MB。
生成数据可以使用 zxh 的数据生成器。
1.3 题目测试点配置文件
其实说实话这一部分官方文档已经讲的很详细了,但测试点配置文件确实没什么大用处。
官方文档中的 timeLimit
表示时间限制,memoryLimit
表示空间限制,score
表示分数,subtaskId
表示子任务编号,isPretest
表示是否为 pretest 测试点。(当然,现在已经没有用了。好奇 pretest 是什么的可以去搜。)
1.4 其他
注意正常的数据中是不能出现中文的。出现了中文读入时不会正常读入。
像我们平时要是想写捆绑测试的话,我的做法是将一道题内所有的测试点都设为
Part 2 Special Judge (SPJ)
大部分人肯定第一次写 SPJ 时都会有点迷茫。因为大部分人都是从来没用过 testlib
库的。
2.1 testlib 库
下载地址。
这个库有很多函数,一些常用的如下:
void registerTestlibCmd(argc, argv)
初始化 SPJ,必须在最前面调用一次。
char readChar()
读入一个 char
。
char readChar(char c)
和上面一样,但是只能读到限定的字母。如果读入的不是限定的字母则直接 WA。
char readSpace()
,等同于 readChar(' ')
。
string readToken()
,读入一个字符串,但是遇到空格、换行、eof(指的是文件结束)为止。
long long readLong()
,读入一个 long long
。
long long readLong(long long L, long long R)
,限定范围(包括 L,R)。
int readInt()
,读入一个 int
。
int readInt(int L, int R)
,限定范围(包括 L,R)。
double readReal()
,读入一个实数。
double readReal(double L, double R)
,限定范围(包括 L,R)。
double readStrictReal(double L, double R, int minPrecision, int maxPrecision)
,读入一个限定范围精度位数的实数。
string readString(),string readLine()
,读入一行 string
,到换行或者 eof 为止。
void readEoln()
,读入一个换行符(通常用于限定输出格式时使用)。
void readEof()
,读入一个 eof(通常用于限定输出格式时使用)。
返回结果时,需要用到如下函数:
quitf(_ok, "The answer is correct. answer is %d",ans);
,给出 AC。
quitf(_wa, "The answer is wrong: expected = %f, found = %f", jans, pans);
,给出 WA。
quitp(0.34,"Partially Correct get %d percent", 50);
,给出部分正确,并且获得该点 0.34
所在位置的值来改变。
2.2 SPJ 上传
在我们打包题目数据时,将 SPJ 命名为 checker.cpp
,并且一起打包进题目数据压缩包里。
为了让 SPJ 发挥作用,我们需要进入题目标签,并且给题目打上「Special Judge」标签。
运行时可以调用上述函数。
上述函数的调用可以从三个位置输入:inf
指的是输入文件,ouf
指的是做题者输出的信息,ans
表示答案文件。
具体格式详见此题题目与附件。
2.3 其他
之前的时候 SPJ 运行时间算在选手程序中,现在不算了。其余就没什么好强调的了。
Part 3 自定义计分脚本
官方文档讲的很明白了。我就不讲了,再讲一遍没什么必要。
唯一要强调的一点是,自定义计分脚本的放置位置是在上传完数据之后,在数据界面上将总分(或你想要的 Subtask)计分方式改为自定义。
Part 4 交互题
重点!
很多人第一次写交互题的配置都会有个误区,就是认为 interactive_lib.cpp
是用来交互的文件(不知道这个是什么也没关系,下面会提到)。其实不然,这个文件只是提供交互库的。真正用来交互的文件是 checker.cpp
。
下面来详细讲讲。
4.1 交互题文件
交互题在 SPJ 的基础上还会多出一个文件,也就是我们上面所说的 interactive_lib.cpp
。这个文件是交互库文件,也就是说,你可以通过这个文件给选手提供一些可以使用的函数。而选手实现的函数交互库也可以调用。
而在 IO 交互题中,原 checker.cpp
则负责与选手交互。
4.2 交互方式
交互库交互是我们要提到的第一种交互方式(例如猜数):
你需要在 interactive_lib.cpp
中用 extern "C"
关键字定义一些可以被做题者调用的函数,并且声明做题者需要实现的函数。例如猜数的交互库中的这部分:
extern "C" {
extern int Chtholly(int n, int c);
extern int Seniorious(int x) {
const int lim = 3000000;
if(++cnt > lim) cnt = lim;
return (k < x) ? 1 : ((k == x) ? 0 : -1);
}
}
你需要实现 main
函数,而做题者不需要,也不应该定义 main
函数。
此时评测机会调用交互库内的 main
函数,并且将交互库内 main
函数的输出与答案文件比较。
具体的例子可以看题目猜数。接下来是另一种交互方式。
IO交互是第二种交互方式(例如猜数(IO 交互版),以及这道题(带数据包)):
你可以使用 checker.cpp
的标准输出作为做题者代码得到输入。你可以通过从 ouf
或者从标准输入获得代码的输出。
此时你仍可以从 inf
与 ans
的文件中获取信息,但 inf
文件会对做题者代码不可见。换言之,做题者不能直接从输入文件获得输入。
这类的交互一般将 interactive_lib.cpp
放空,而在 checker.cpp
文件中实现交互。
4.3 缓冲区
如果你输出的文件没有清空缓冲区,则数据不会被做题者程序获取。反之同理。清空缓冲区的方法如下:
- 对于 C/C++:
fflush(stdout)
; - 对于 C++:
std::cout << std::flush
; - 对于 Java:
System.out.flush()
; - 对于 Python:
stdout.flush()
; - 对于 Pascal:
flush(output)
; - 对于其他语言,请自行查阅对应语言的帮助文档。
特别的,对于 C++ 语言,在输出换行时如果你使用 std::endl
而不是 '\n'
,也可以自动刷新缓冲区。
4.4 交互题文件配置
首先我们来讲交互库交互的文件写法。
交互库 interactive_lib.cpp
是被链接到用户程序的一个模块。如果你只想让做题者实现一些函数,则你需要实现 main
函数。例如下面这一道题:
给定一个数 a
以及一个加法函数 int plus(int a,int b)
。要求你实现一个函数 int next_num(int a)
,能够求出数字 a
的下一个数。
一种可能的交互库如下:
#include<bits/stdc++.h>
extern "C"{
extern int plus(int a,int b){
return a+b;
}
extern int next_num(int a);
}
namespace std{
int main() {
int a;
cin>>a;
cout<<next_num(a);
return 0;
}
}
这段代码中,很明显分为了三个部分,头文件、extern "C"
部分以及 main
函数。头文件就不讲了,主要看后两部分。
首先是 extern "C"
部分。我们能够看到,这里我们定义了一个函数 int plus(int a,int b)
,并且用 extern
关键字声明。这样我们就为做题者提供了一个函数 plus
。而我们又用 extern
函数声明了一个函数 int next_num(int a)
,这样我们就可以调用选手实现的 next_num
函数。
然后对于 main
部分,判题的程序会调用交互库内的 main
函数,并且输出 next_num(a)
的值,供 SPJ 比较。
做题者的一种可能的代码如下:
extern "C" int plus(int a,int b);
extern "C" int next_num(int a){
return plus(a,1);
}
一句话总结,你需要用 extern "C"
声明做题者实现的函数,实现做题者用 extern "C"
声明的函数。
另外一种,如果你想让做题者实现整个做题的过程,而你只是想提供一些可以被做题者利用的函数,则你不需要实现 main
函数,只需要将上面的 main
函数部分让做题者实现,其余不变。
IO 交互的交互库写法,向选手提供函数的方式如上。但通常,我们在写这种交互库时会将交互库放空,由 SPJ 实现 IO 交互。
其余就没什么好讲的了。
4.5 交互本地测试
本地的函数交互方式如下:
首先,我们将 interactive_lib.cpp
即交互库,放到与选手的代码(或 std)同一目录下。
接下来在你的代码中添加一行 #include "interactive_lib.cpp"
。
直接运行你的代码(或 std)即可。这样就可以做到本题测试函数交互。
本地实现 IO 交互的方法,建议查看这篇文章。
4.6 其他
像上述的第二种交互,一定不要在 interactive_lib.cpp
内定义 main
函数。否则选手定义 main
时将会出现重定义 main
的情况导致 CE。
为了使交互题能够正常评测,我们需要将题目打上「Special Judge」和「交互题」标签。
另外,因为洛谷评测机技术的问题,交互题不能使用 C++14 (GCC 9) 提交。
后记
希望大家不要和我一样调这个东西调到红温。
若有什么地方写错、语法不通顺、不完善,欢迎联系 zxh_qwq 指出错误,我将会十分感谢。
都看到这儿了,点赞收藏再走呗 QAQ
Updates:
增加「4.4 交互题文件配置」。
增加「4.5 交互本地测试」。
「1.2 题目数据」中“可以放
50 组数据”改为“可以放100 组数据”。「1.2 题目数据」中增加数据生成器。