主页
搜索
最近更新
数据统计
申请密钥
系统公告
1
/
1
请查看完所有公告
自制题目、SPJ、交互等 详细教程(含举例)
最后更新于 2025-07-31 09:53:31
作者
zxh_qwq
分类
算法·理论
复制 Markdown
查看原文
删除文章
更新内容
## 前言 写这篇文章还是因为猜交互题应该怎么配置,一个下午没弄出来红温了写的。 感觉洛谷帮助文档讲自制题目、SPJ、交互不是很详细。 而且举例仅有代码,没有别的(例如放置位置,注意事项等)详细说明。 所以这篇文章就来详细讲讲。 --- ## 正文 ### Part 1 自制题目 大部分人都自制过题目吧。这里仅为基础内容介绍。 少部分第一次自制题目没有经验的,建议看这里。 #### 1.1 题目创建 题目创建分为两类:个人与团队。 普通的题目创建只能创建两类:U 题库(个人)或 T 题库(团队)。当然你是管理员的话当我没说。 个人题目创建方法:打开个人主页-题库-我创建的题目-创建题目即可创建题目。书写好各项信息之后即可保存。 团队题目的创建方法类似。 新手一开始可能会乱点标签,这个不要紧,但是记住别点到特殊题目标签就行了。至于这些标签有什么用,将会在接下来提到。 #### 1.2 题目数据 如果你要配置一个题目的数据,你需要至少 $1$ 个、至多 $100$ 个测试点的输入与输出。(此处,官方说最高可以 $50$ 个测试点,但是亲测 $100$ 个) 这些输入数据和输出数据应当成对出现,其输入数据文件的扩展名为 `.in` 而输出文件的扩展名为 `.out`。同时,测试点文件名中只能允许有连续的一段数字。例如 `t1.in , 1.in , problemA1.out` 等。形如 `T1-1.in` 的形式是不被允许的。 如官方所说,直接将若干数据点打包成一个 zip 压缩包,rar 和其他格式不能成功。 可以用 windows 自带的方式压缩压缩包,或者下载 7zip 等压缩工具。 注意此处有一个细节: 如果你配置完成后,双击打开 zip 文件夹发现里边是一个文件夹,文件夹中存放着其他文件,那这样的压缩包就是不被允许的。 解决这种问题很简单,直接点入没被压缩过的文件夹,框选所有需要的文件再压缩就好了。 注意文件夹中没有任何文件夹或者其他无关文件。经过我们自行测验,压缩前大小不能超过 100MB。 生成数据可以使用 zxh 的[数据生成器](https://www.luogu.com.cn/problem/U558495)。 #### 1.3 题目测试点配置文件 其实说实话这一部分[官方文档](https://help.luogu.com.cn/manual/luogu/problem/testcase-config)已经讲的很详细了,但测试点配置文件确实没什么大用处。 官方文档中的 `timeLimit` 表示时间限制,`memoryLimit` 表示空间限制,`score` 表示分数,`subtaskId` 表示子任务编号,`isPretest` 表示是否为 pretest 测试点。(当然,现在已经没有用了。好奇 pretest 是什么的可以去搜。) #### 1.4 其他 注意正常的数据中是不能出现中文的。出现了中文读入时不会正常读入。 像我们平时要是想写捆绑测试的话,我的做法是将一道题内所有的测试点都设为 $100$ 分,然后将得分计算设为最小值。对于有部分分的题目,只需要对每个 subtask 单独操作就行了。 --- ### Part 2 Special Judge (SPJ) 大部分人肯定第一次写 SPJ 时都会有点迷茫。因为大部分人都是从来没用过 `testlib` 库的。 #### 2.1 testlib 库 [下载地址](https://github.com/MikeMirzayanov/testlib/releases/download/0.9.41/testlib-0.9.41.zip)。 这个库有很多函数,一些常用的如下: `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);`,给出部分正确,并且获得该点 $34\%$ 的分数。具体给出分数可以通过更改 `0.34` 所在位置的值来改变。 这里我们还要提到一点,就是 testlib 库会把大部分 C++ 内置库给禁用掉。所以我们原本的随机数不能再用 `srand` 和 `rand` 来实现了。 怎么实现呢?首先我们要在开头的时候调用一次 `registerGen(argc, argv, 1)`,然后你可以用 `rnd.next(L,R)` 来取随机数。如果 `L,R` 是整数,返回值也是整数。`L,R` 是浮点数的时候,返回值也是浮点数。 #### 2.2 SPJ 上传 在我们打包题目数据时,将 SPJ 命名为 `checker.cpp`,并且一起打包进题目数据压缩包里。 为了让 SPJ 发挥作用,我们需要进入题目标签,并且给题目打上「Special Judge」标签。 运行时可以调用上述函数。 上述函数的调用可以从三个位置输入:`inf` 指的是输入文件,`ouf` 指的是做题者输出的信息,`ans` 表示答案文件。 具体格式详见[此题](https://www.luogu.com.cn/problem/U554837)题目与附件。 #### 2.3 其他 之前的时候 SPJ 运行时间算在选手程序中,现在不算了。其余就没什么好强调的了。 --- ### Part 3 自定义计分脚本 [官方文档](https://help.luogu.com.cn/manual/luogu/problem/scoring-script)讲的很明白了。我就不讲了,再讲一遍没什么必要。 唯一要强调的一点是,自定义计分脚本的放置位置是在上传完数据之后,在数据界面上将总分(或你想要的 Subtask)计分方式改为自定义。 --- ### Part 4 交互题 重点! 很多人第一次写交互题的配置都会有个误区,就是认为 `interactive_lib.cpp` 是用来交互的文件(不知道这个是什么也没关系,下面会提到)。其实不然,这个文件只是提供交互库的。真正用来交互的文件是 `checker.cpp`。 下面来详细讲讲。 #### 4.1 交互题文件 交互题在 SPJ 的基础上还会多出一个文件,也就是我们上面所说的 `interactive_lib.cpp`。这个文件是交互库文件,也就是说,你可以通过这个文件给选手提供一些可以使用的函数。而选手实现的函数交互库也可以调用。 而在 IO 交互题中,原 `checker.cpp` 则负责与选手交互。 #### 4.2 交互方式 交互库交互是我们要提到的第一种交互方式(例如[猜数](https://www.luogu.com.cn/problem/P1947)): 你需要在 `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` 函数的输出与答案文件比较。 具体的例子可以看题目[猜数](https://www.luogu.com.cn/problem/P1947)。接下来是另一种交互方式。 IO交互是第二种交互方式(例如[猜数(IO 交互版)](https://www.luogu.com.cn/problem/P1733),以及[这道题(带数据包)](https://www.luogu.com.cn/problem/U557269)): 你可以使用 `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` 的下一个数。 一种可能的交互库如下: ```cpp #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 比较。 做题者的一种可能的代码如下: ```cpp 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 交互的方法,建议查看[这篇文章](https://znpdco.fun/2024/07/27/interactive/)。 #### 4.6 其他 像上述的第二种交互,一定不要在 `interactive_lib.cpp` 内定义 `main` 函数。否则选手定义 `main` 时将会出现重定义 `main` 的情况导致 CE。 为了使交互题能够正常评测,我们需要将题目打上「Special Judge」和「交互题」标签。 另外,因为洛谷评测机技术的问题,交互题不能使用 C++14 (GCC 9) 提交。 --- ## 后记 希望大家不要和我一样调这个东西调到红温。 若有什么地方写错、语法不通顺、不完善,欢迎联系 [zxh_qwq](https://www.luogu.com.cn/chat?uid=919709) 指出错误,我将会十分感谢。 都看到这儿了,点赞收藏再走呗 QAQ :::info[Updates:]{open} [2025.4.29] 增加「4.4 交互题文件配置」。 [2025.4.29] 增加「4.5 交互本地测试」。 [2025.5.2] 「1.2 题目数据」中“可以放 $50$ 组数据”改为“可以放 $100$ 组数据”。 [2025.5.3] 「1.2 题目数据」中增加数据生成器。 [2025.7.30] 「2.1 testlib 库」中增加关于 testlib 库的随机数使用方式。
正在渲染内容...
点赞
112
收藏
132