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]\) 之间。
  • 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]\) 之间。
  • 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, ...)

在读入后,按照题意直接进行判断,然后返回评测结果给评测机即可。

同样的,不能直接使用 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.hrand() 都禁用了,这时候我们怎么办呢? (不写 SPJ 了)

值得庆幸的是,testlib.h 还没有禁用某些随机数生成器,比如:mt19937

使用一下语句生成一个随机数,并作为传入参数传给 testlib.h 的随机数生成器即可。

mt19937 rng{random_device{}()};
rnd.setSeed(rng());

这样就可以让 SPJ 生成的随机数每次都不一样,不过,这违反了 testlib.h 的初衷,如果这个随机数影响评测结果还是不建议使用,可以在输出文件或者其他地方放一个随机种子,然后使用 SPJ 读入,作为种子一般情况就可以了。

除了有些时候出题人想整活,比如 1% 的概率会返回一个特殊的评测信息(注意,不是评测结果),才推荐使用这个方法生成随机数。

未完待续

posted @ 2023-11-10 09:50  One_JuRuo  阅读(470)  评论(0编辑  收藏  举报