SPJ 学习笔记
前言
本博客只会讲本蒟蒻目前学习到的地方,所以如果有没讲到的地方还请见谅,等蒟蒻有时间学了再说。
虽然本博客都是一些较为浅显的知识,但是写一道验证较为简单的 SPJ 题还是足够的。
另外,本文的 SPJ 都以 C++ 为例。
Step.1 SPJ 基础
Special Judge(简称:spj,别名:checker)是当一道题有多组解时,用来判断答案合法性的程序。--OI Wiki
也就是说,当某道题没有确定的答案时,可以使用一个程序来判断答案是否符合要求。
而 Testlib.h
就是一个专门用来写 SPJ 的 C++ 库。
像 Codeforces,因为存在 hack 机制,无论是否题目是否存在多组解,都需要写一个 SPJ,就是一个经常使用 SPJ 的 OJ。
一般的 SPJ 程序如下:
#include"testlib.h"
int main(int argc,char *argv[]) {
registerTestlibCmd(argc,argv);
// your code
if(xxx) quitf(_ok,"xxx");
else quitf(_wa,"xxx");
}
首先,主函数的写法就和平时不太一样,这里传入的两个参数是为了让 SPJ 能够从命令行输入数据,使得 SPJ 可以从标准输入、标准输出和选手输出中读取数据。
registerTestlibCmd(argc,argv);
语句则是用于初始化 checker,在主函数的最前面必须调用一次,直接复制即可。
然后就是自己的代码,但是读入并不能使用 cin
或者 scanf
等方式输入,而需要 testlib.h
内置的读入方式。
在介绍读入方式前,需要先介绍三个结构体:
inf
,代表标准输入文件。ans
,代表标准输出文件。ouf
,代表选手输出文件。
所以在写 SPJ 的时候,最好别用这三个名字作为某些变量名或者结构体名。
而读入,则需要调用内置于结构体内的函数实现。
比如,我要从标准输入文件中读入一个整数 \(n\),则可以使用以下代码:
inf.readInt();
以下是一些比较常用的读入函数:
char readChar()
- 读入一个字符。
char readChar(char c)
- 读入一个字符,且必须为 c。
char readSpace()
- 读入一个字符,且必须为空格,等同于
readChar(' ')
。
- 读入一个字符,且必须为空格,等同于
string readToken()/string readWord()
- 读入一个串,直到遇到空白字符,如:空格、Tab、EOLN。
string readToken(string regex)/string readWord(string regex)
- 读入一个字符串,且必须与 regex 匹配。
long long readLong()
- 读入一个
long long
类型的整数。
- 读入一个
long long readLong(long long L, long long R)
- 读入一个
long long
类型的整数,必须在 \([L,R]\) 之间。
- 读入一个
vector<long long> readLongs(int n, long long L, long long R)
- 读入 \(N\) 个
long long
类型的整数,且要求均在 \([L,R]\) 之间。
- 读入 \(N\) 个
int readInt()/int readInteger()
- 读入一个
int
类型的整数。
- 读入一个
int readInt(int L, int R)/int readInteger(L, R)
- 读入一个
int
类型的整数,必须在 \([L,R]\) 之间。
- 读入一个
vector<int> readInts(int n, int L, int R)/vector<int> readIntegers(int n, int L, int R)
- 读入 \(N\) 个
int
类型的整数,必须均在 \([L,R]\) 之间。
- 读入 \(N\) 个
double readReal()/double readDouble()
- 读入一个
double
类型的浮点数。
- 读入一个
double readReal(double L, double R)/double readDouble(double L, double R)
- 读入一个
double
浮点数,必须在 \([L,R]\) 之间。
- 读入一个
double readStrictReal(double L, double R, int minPrecision, int maxPrecision)double readStrictDouble(double L, double R, int minPrecision, int maxPrecision)
- 读入一个
double
类型的浮点数,必须在 \([L,R]\) 之间,小数位数必须在 \([minPrecision,maxPrecision]\) 之间,不得使用指数计数法等非正常格式。
- 读入一个
string readString()/string readLine()
- 读入一行(包括换行符),同时将流指针指向下一行的开头。
string readString(string regex)/string readLine(string regex)
- 读入一行,必须与 regex 匹配。
void readEoln()
- 读入 EOLN(在 Linux 环境下读入 LF,在 Windows 环境下读入 CR LF)。
void readEof()
- 读入 EOF。
void quit(TResult verdict, string message)/void quitf(TResult verdict, string message, ...)
- 结束程序,若 Stream 为 ouf 返回 verdict,否则返回 _fail;输出 message。
void quitif(bool condition, TResult verdict, string message, ...)
- 如果 condition 成立,调用
quitf(verdict, message, ...)
。
- 如果 condition 成立,调用
在读入后,按照题意直接进行判断,然后返回评测结果给评测机即可。
同样的,不能直接使用 cout
或者 printf
等方式直接输出,而却需要使用 testlib.h
内置的函数 quitf
。
使用的格式是 quitf(A,B);
其中 A
部分是评测结果的返回值,一下为比较常用的返回值:
_ok
,表示选手程序通过该测试点,获得满分。_wa
,表示选手程序的输出不符合题意,错误。pc
,表示选手程序可以得到部分分,具体写的时候需要写一个小数,表示能获得分数的比例,如:\(0.5\) 表示能获得一半的分数。
除此之外还有:
-
_pe
,表示选手程序格式错误,在多读入或者少输出、多输出时出现,一般不需要自己写,testlib.h
可以自动判断。 -
__fail
,运行选手程序失败。 -
_dirt
,输出文件含有多余信息 -
_unexpected_eof
,文件读完时仍然尝试读入。
可能还有其他的,但是大部分 SPJ 其实都不需要了。
除了这些以外,其他正常的错误,如:RE、TLE、MLE 等,就不需要你自己写了,评测机可以自行判断。
那么,B
部分就是返回的评测信息,书写格式和写 printf
没什么区别。
比如一种例子:"Expected %d, but found %d.",a,b
评测机就会在这个测试点先是对应内容。
一般来说,可以写的较为详细,一方面方便自己调试,一方面可以让选手清晰地知道自己哪里错了。
当然,也可以写的很简略或者干脆不写。
以上就是要写一个 SPJ 的基础部分,掌握了就可以写大部分简单的 SPJ。
一下是一个 SPJ 的例子,要求从输入文件中读入一个整数 \(n\),然后从选手输出文件中读入 \(n\) 个整数,要求这 \(n\) 个整数是一个排列。
#include"testlib.h"
using namespace std;//可加可不加,看个人习惯
int n,a[1000005],p[1000005];
int main(int argc,char *argv[])
{
registerTestlibCmd(argc,argv);
n=inf.readInt()
for(int i=1;i<=n;++i)
{
a[i]=ans.readInt();
if(a[i]<0||a[i]>n) quitf(_wa,"Required a permutation, but the %dth element is within the range [1,%d].",i,n);
if(p[a[i]]) quitf(_wa,"Required a permutation, but the %dth element is equal to the %dth element.",p[a[i]],i);
p[a[i]]=i;
}
quitf(_ok,"Yes, it is a permutation.")
}
Step.2 随机数
有些时候,我们可能会使用随机数来满足某些需求,不过 testlib.h
禁用了大部分 C++ 原有的库,也就导致一些原本的随机数生成函数也无法使用,比如 rand()
,而内置了一些随机数生成函数。
首先,我需要初始化 Generator,使用以下语句即可:
registerGen(argc,argv,1);
大部分情况直接复制上去即可,最后一个参数是 Generator 的版本号,1 已经是最新版本,不需要改动。
那么初始化后,我们就可以使用 rnd
了。
以下是 rnd
的成员函数:
rnd.next()
- 随机生成一个 \([0,1)\) 之间的浮点数。
rnd.next(R)
- 若 \(R\) 是整数,则随机生成一个 \([0,R]\) 之间的整数;若 \(R\) 是浮点数,则随机生成一个 \([0,R)\) 之间的浮点数。
rnd.next(L,R)
- 若 \(L,R\) 是整数,则随机生成一个 \([L,R]\) 之间的整数;若 \(L,R\) 是浮点数,则随机生成一个 \([L,R)\) 之间的浮点数。
rnd.any(c)
- c 是一个 vector 或者 string,会随机返回容器内一个随机元素。
rnd.next("a|b|c")
- 随机返回字符串 a,b,c 中的一个。
rnd.wnext(i,t)
- 不均匀随机数生成器,若 \(t=0\),则均匀生成;若 \(t>0\),则均匀生成 \(t\) 次,并取最大值;若 \(t<0\),则均匀生成 \(-t\) 次,并取最小值。
但是需要特别注意的是,testlib.h
的所有随机数生成器,在传入参数相同情况下,生成的数据都保证了一定相同,甚至环境不同,生成的数据也都完全一样,这是为了防止评测波动和方便调试。
但是有些情况,我们又需要完全随机的数据,但是 testlib.h
把 rand()
都禁用了,这时候我们怎么办呢? (不写 SPJ 了)
值得庆幸的是,testlib.h
还没有禁用某些随机数生成器,比如:mt19937
。
使用一下语句生成一个随机数,并作为传入参数传给 testlib.h
的随机数生成器即可。
mt19937 rng{random_device{}()};
rnd.setSeed(rng());
这样就可以让 SPJ 生成的随机数每次都不一样,不过,这违反了 testlib.h
的初衷,如果这个随机数影响评测结果还是不建议使用,可以在输出文件或者其他地方放一个随机种子,然后使用 SPJ 读入,作为种子一般情况就可以了。
除了有些时候出题人想整活,比如 1% 的概率会返回一个特殊的评测信息(注意,不是评测结果),才推荐使用这个方法生成随机数。