关于 Special Judge 的编写和本地测试

最近有几位同学来问我 Special Judge 怎么写?为了让大家可以写出 Special Judge 方便在本地调试和对拍,我就想写一篇文章来介绍 Special Judge。

Special Judge 是什么?有什么用?

大家可以先看这样一篇文章:Special Judge - OI Wiki (oi-wiki.org)

Special Judge(简称:SPJ,别名:checker)是当一道题有多组解时,用来判断答案合法性的程序。

—— OI - Wiki

Special Judge 的写法

下面这两篇文章是关于 Special Judge 的写法:

因为洛谷使用的是 CodeforcesTestlib,所以就按照 Testlib 的 SPJ 写法即可。

但是本篇文章的重点不在于怎么编写 Special Judge,因为上面的文章都给出了很详细的写法,而是如何利用它来进行本地调试和对拍

怎么利用 Special Judge 进行对拍

鉴于 CodeForces 自带的 Testlib 不好用,也有可能是因为我不会用,所以我手写了一个 Testlib 来方便调试。

只需要把下面的代码保存为 testlib.h 和 checker 在同一文件夹下即可。

//the code is from chenjh
#include<cstdio>
#include<string>
#include<cassert>
#include<regex>
#include<windows.h>
struct TestLib{
    FILE *f;
    char readChar(){char c;std::fscanf(f,"%c",&c);return c;}
    char readChar(char ch){char c=readChar();assert(c==ch);return c;}
    char readSpace(){char c=readChar();assert(c==' ');return c;}
    void unreadChar(char ch){/*暂且不会实现。*/}
    std::string readToken(){char s[1<<20];std::fscanf(f,"%s",s);std::string str=s;return str;}
    std::string readToken(std::string regex){std::string s=readToken();assert(regex_match(s,std::regex(regex)));return s;}
    std::string readWord(){return readToken();}
    std::string readWord(std::string regex){return readToken(regex);}
    long long readLong(){long long x;std::fscanf(f,"%lld",&x);return x;}
    long long readLong(const long long&L,const long long&R){long long x=readLong();
    assert(L<=x && x<=R);return x;}
    int readInt(){int x;std::fscanf(f,"%d",&x);return x;}
    int readInteger(){return readInt();}
    int readInt(int L,int R){int x=readInt();assert(L<=x && x<=R);return x;}
    double readDouble(){double x;std::fscanf(f,"%lf",&x);return x;}
    double readReal(){return readDouble();}
    double readDouble(const double&L,const double&R){double x=readDouble();
    assert(L<=x && x<=R);return x;}
    std::string readLine(){char s[1<<20];std::fscanf(f,"%[^\n]",s);std::string str=s;return str;}
    std::string readString(){return readLine();}
    void readEoln(){char c;std::fscanf(f,"%c",&c);assert(c=='\r'||c=='\n');if(c=='\r')std::fscanf(f,"%c",&c);assert(c=='\n');}
    void readEof(){char c;std::fscanf(f,"%c",&c);assert(~c);}
}inf,ouf,ans;
void registerTestlibCmd(int argc,char* argv[]){
    inf.f=std::fopen("data.in","r");
    ouf.f=std::fopen("data.out","r");
    ans.f=std::fopen("data.ans","r");
}
/*
Color:
    需要 16 进制 0x 前缀。 
    0 = 黑色       8 = 灰色
    1 = 蓝色       9 = 淡蓝色
    2 = 绿色       A = 淡绿色
    3 = 浅绿色       B = 淡浅绿色
    4 = 红色       C = 淡红色
    5 = 紫色       D = 淡紫色
    6 = 黄色       E = 淡黄色
    7 = 白色       F = 亮白色
*/
#ifndef CHECK
    template<typename... Args>void COLOR_PRINT(const int color,const char*s,Args...x){
        HANDLE handle=GetStdHandle(STD_OUTPUT_HANDLE);
        SetConsoleTextAttribute(handle,FOREGROUND_INTENSITY|color);
        std::printf(s,x...);
        SetConsoleTextAttribute(handle,FOREGROUND_INTENSITY|7);
    }
#endif
const int _ok=0,_wa=1;
template<typename... Args>void quitf(const int ret,const char*s,Args...x){
    #ifndef CHECK
        std::printf("Score: ");
        if(ret) COLOR_PRINT(0xa,"100\n");
        else COLOR_PRINT(0xa,"0\n");
        std::puts("Checker comment");
        if(ret) COLOR_PRINT(0x4,"wa ");
        else COLOR_PRINT(0xa,"ok ");
        COLOR_PRINT(0x8,s,x...);
        std::putchar('\n');
    #endif
    std::exit(ret);
}
template<typename... Args>void quitp(const double&ret,const char*s,Args...x){
    #ifndef CHECK
        std::printf("Score: ");
        COLOR_PRINT(0xa,"%.1lf\n",ret);
        std::puts("Checker comment");
        COLOR_PRINT(0x4,"wa ");
        COLOR_PRINT(0x8,s,x...);
    #endif
    std::putchar('\n'),std::exit(_wa);
}

因为 void unreadChar(char ch) 暂且还不会实现,我猜你们也不会用到这些函数,如果有大佬会实现,欢迎私信给我提出指导意见以及建议。

下面我解释一下我手写的 Testlib 各部分的作用和一些函数的意思:

函数 void registerTestlibCmd(int argc,char* argv[])

这个函数里初始化了文件的读入流。

因为要同时读入多个文件,所以我使用了 cstdio 库中的 fopen 函数。

函数的原型是 std::FILE* fopen( const char* filename, const char* mode );

具体使用方法可以参考 std::fopen - cppreference.com

cassert 库中的 assert() 函数

在标头 <cassert> 定义:

#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif

如果传进该函数的参数为假,则会返回一个非 0 值(即 RE 段错误)。

cstdio 库中的 fscanf() 函数

在标头 <cstdio> 定义:

int fscanf( std::FILE* stream, const char* format, ... );//从文件流 stream 读数据,按照 format 转译并存储结果于给定位置。

上面这个函数的使用方法其实和 scanf() 差不多,只需要多加一个参数(即文件流)即可。

Special Judge 的对拍程序

Special Judge 的对拍程序

首先我们以 AT_dp_f LCS 这一道题为例。

首先将上面我手写的 testlib.h 保存(注:头文件不需要编译!)。

根据上面 SPJ 的编写方法,我们可以写出这样一个 checker.cpp(请注意请和 testlib.h 放在同一个文件夹下方)并用 C++14 标准进行编译(最好开启无限栈):

//the code is from chenjh
#include "testlib.h"
#include<string>
#define WA quitf(_wa,"WA!")
#define AC quitf(_ok, "Correct.")
using namespace std;
string s,t,ou,jans;
int main(int argc,char* argv[]){
	registerTestlibCmd(argc,argv);
	s=inf.readToken();t=inf.readToken();
	ou=ouf.readToken();jans=ans.readToken();
	if(ou==jans){AC;return 0;}
	if((int)ou.length()!=jans.length()){WA;return 0;}
	int l=ou.length(),l1=s.length(),l2=t.length(),j=0;
	for(int i=0;i<l;i++){
		for(;ou[i]!=s[j] && j<l1;j++);
		if(j>=l1 || ou[i]!=s[j]){WA;return 0;}
	}
	j=0;
	for(int i=0;i<l;i++){
		for(;ou[i]!=t[j] && j<l2;j++);
		if(j>=l2 || ou[i]!=t[j]){WA;return 0;}
	}
	AC;
	return 0;
}

接着把出现错误需要对拍的代码命名为 code.cpp,正确的代码命名为 std.cpp,并将它们都进行编译(不需要在代码中提前加入文件重定向)。

code.cpp(这里选用了来自 @hky0311800272 分代码):

//the code is from hky(st20242008).
//Submission number is #502175.
#include<bits/stdc++.h>
using namespace std;
string ans[2][3010];
int f[2][3010],i,j,lena,lenb;
char a[3010],b[3010];
struct B{
	int i,j,f;
}pre[3010][3010];
void print(int i,int j){
	if(i<0||j<0||i==0&&j==0)return;
//	printf("%d %d\n",i,j);
	if(pre[i][j].i>0||pre[i][j].j>0)print(pre[i][j].i,pre[i][j].j);
	if(pre[i][j].f)printf("%c",a[i]);
}
main(){
	scanf("%s",a);
	scanf("%s",b);
	lena=strlen(a);
	lenb=strlen(b);
	for(i=0;i<lena;++i){
		for(j=0;j<lenb;++j){
			if(j>0&&f[(i&1)^1][j]<f[i&1][j-1]){
				f[i&1][j]=f[i&1][j-1];
				pre[i][j]={i,j-1};
			}
			else{
				f[i&1][j]=f[(i&1)^1][j];
				pre[i][j]={i-1,j};
			}
			if(a[i]==b[j]){
				if(j>0&&f[i&1][j]<=f[(i&1)^1][j-1]+1){
					f[i&1][j]=f[(i&1)^1][j-1]+1;
					pre[i][j]={i-1,j-1,1};
				}
				else{
					pre[i][j].f=1;
				}
			}
		}
	}
	if(a[0]==b[0])printf("%c",a[0]);
	print(i-1,j-1);
}

std.cpp(选用了我的 100 分代码):

//the code is from chenjh(c1120241702)
#include<bits/stdc++.h>
using namespace std;
char s1[3005],s2[3005];
int dp[3005][3005],p[3005][3005];
void print(int x,int y) {
	if(x==0||y==0)return;
	if(p[x][y]==1)
		print(x-1,y-1),putchar(s1[x]);
	else if(p[x][y]==-1)print(x-1,y);
	else print(x,y-1);
}
int main(){
	cin>>(s1+1)>>(s2+1);
	int l1=strlen(s1+1),l2=strlen(s2+1);
	for(int i=0;i<=l1;i++) dp[i][0]=0;
	for(int i=0;i<=l2;i++) dp[0][i]=0;
	for(int i=1;i<=l1;i++){
		for(int j=1;j<=l2;j++){
			if(dp[i-1][j]>=dp[i][j-1])p[i][j]=-1;
			dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
			if(s1[i]==s2[j])
				dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1),p[i][j]=1;
		}
	}
	print(l1,l2);
//	printf("%d\n",dp[l1][l2]);
	return 0;
}

将制造数据的代码命名为 maker.cpp 然后进行编译(同样也不需要在代码中提前加入文件重定向)。

//the code is from chenjh
#include<cstdio>
#include<string>
#include<ctime>
#include<cstdlib>
using namespace std;
int rand(int l,int r){return 1ll*rand()*rand()%(r-l+1)+l;}
string lcs="";
int main(){
	unsigned int *seed=new unsigned int;
	srand(time(0)*(*seed+1));
	delete seed;
	int llen=rand(1,500);
	for(int i=0;i<llen;i++) lcs+=(char)rand('a','z');
	int nowlen=3000;
	for(int i=0;i<llen;i++){
		int mlen=rand(1,(3000-llen)/llen);
		putchar(lcs[i]);
		for(int j=0;j<mlen;j++) putchar(rand('a','z'));
		nowlen-=mlen+1;
	}
	while(nowlen--) putchar(rand('a','z'));
	putchar('\n');
	nowlen=3000;
	for(int i=0;i<llen;i++){
		int mlen=rand(1,(3000-llen)/llen);
		putchar(lcs[i]);
		for(int j=0;j<mlen;j++) putchar(rand('a','z'));
		nowlen-=mlen+1;
	}
	while(nowlen--) putchar(rand('a','z'));
	return 0;
}

如果其中运用了随机数,并且种子和时间(例如 time(0) 相关)建议使用一个为初始化过的 int 类型的变量对时间进行相乘再设定为种子。

个人版对拍

接下来就是对拍程序(命名为 check.cpp 并进行编译,根据题目要求修改程序第 7 排的时间限制)了:

//the code is from chenjh

//用户自行配置: 
//时间限制(单位为毫秒):
const int TimeLimit=2000;

#include<fstream>
#include<cstdio>
#include<ctime>
#include<cstdlib>
#include<string>
#include<windows.h>
using namespace std;
/*
Color:
	需要 16 进制 0x 前缀。 
	0 = 黑色	   8 = 灰色
	1 = 蓝色	   9 = 淡蓝色
	2 = 绿色	   A = 淡绿色
	3 = 浅绿色	   B = 淡浅绿色
	4 = 红色	   C = 淡红色
	5 = 紫色	   D = 淡紫色
	6 = 黄色	   E = 淡黄色
	7 = 白色	   F = 亮白色
*/
template<typename... Args>void COLOR_PRINT(const int front_color,const int back_color,const char*s,Args...x){
	HANDLE handle=GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleTextAttribute(handle, BACKGROUND_INTENSITY | back_color*16 | FOREGROUND_INTENSITY | front_color);
	std::printf(s,x...);
	SetConsoleTextAttribute(handle,FOREGROUND_INTENSITY|7);
}
bool fileExists(const std::string&filename){std::ifstream infile(filename);return infile.good();}//文件是否存在。
void SaveData(int &c){
	string s="data\\data"+to_string(c++);
	string str="copy data.in "+s+".in";
	system(str.c_str());
	str="copy data.ans "+s+".out";
	system(str.c_str());
}
int main(){
	system("md data");system("cls");
	if(!fileExists("maker.exe")) return puts("Can\' t find file maker.exe!"),0;
	if(!fileExists("checker.exe")) return puts("Can\' t find file checker.exe!"),0;
	if(!fileExists("std.exe")) return puts("Can\' t find file std.exe!"),0;
	if(!fileExists("code.exe")) return puts("Can\' t find file code.exe!"),0;
	ofstream outvbs("KillCode.vbs");
	outvbs<<"WSCript.Sleep "<<TimeLimit+200<<'\n';
	outvbs<<"Set WshShell = WScript.CreateObject(\"WScript.Shell\")\nWshShell.Run \"taskkill /im code.exe /f\", 0, True\n";
	outvbs.close();//用来关闭超时的进程。
	int maxc;
	printf("Enter the total number of pairs of shots you wish to obtain:");
	scanf("%d",&maxc);//输入你想要的数据组数。
	for(int t=1,c=1;c<=maxc;t++){
		system("maker.exe > data.in");
		system("std.exe < data.in > data.ans");
		bool sd=0;
		system("start /B KillCode.vbs");
		double tm=clock();
		unsigned int ls=system("code.exe < data.in > data.out");
		tm=clock()-tm;
		system("taskkill /f /im wscript.exe 1>nul 2>nul"),system("taskkill /f /im cscript.exe 1>nul 2>nul");
		printf("\n\nTest#%d: ",t);
		if(tm>TimeLimit) sd=1,COLOR_PRINT(0x7,0x1,"Time Limit Exceeded"),printf("! Time used %.0lf ms.\n",tm);
		else if(ls>0xc0000000u) sd=1,COLOR_PRINT(0x7,0x5,"Runtime Error"),printf("! Time was unavailable!\n");//RE:3221225725
		else if(ls) COLOR_PRINT(0x7,0x0,"Unknown Error!\n"),system("pause");
		else{
			unsigned int rs=system("checker.exe");
			if(rs>0xc0000000u) COLOR_PRINT(0x7,0x5,"Checker Runtime Error"),printf("! Time was unavailable!\n"),system("pause");
			else if(!rs) COLOR_PRINT(0x7,0xA,"Accepted"),printf("! Time used %.0lf ms.\n",tm);
			else if(rs==1)sd=1,COLOR_PRINT(0x7,0x4,"Wrong Answer"),printf("! Time used %.0lf ms.\n",tm);
			else COLOR_PRINT(0x7,0x0,"Unknown Error!\n"),system("pause");
		}
		if(sd) SaveData(c);
		putchar('\n');
	}
	return 0;
}

运行效果图:

效果1

效果2

开始运行程序时输入一个整数(int 类型范围内)表示你需要多少组数据来 Hack 你的代码(提示:请勿作死输入一些很奇怪的数)。

上面的对拍程序会自动将你的代码 WA/TLE/RE 的测试数据(暂不支持判断内存是否超过内存限制)存至同文件夹下的 data 文件夹中。

团队版对拍

需要写一个 src.txt,每一行为对拍程序的名称(不需要后缀名!),然后将所有代码保存到同文件夹下的 src 文件夹中。

不建议输入中文,因为会出现一些奇奇怪怪的编码错误。

//the code is from chenjh
#if __cplusplus < 201103L
#error This code must be enabled with the -std=c++11 or -std=gnu++11 compiler options.
#endif
#include<fstream>
#include<cstdio>
#include<ctime>
#include<cstdlib>
#include<string>
#include<vector>
#include<windows.h>
using std::string;

//用户自行配置: 
//时间限制(单位为毫秒):
const int TimeLimit=2000;
//g++ 编译器路径:
const std::string g__="C:\\Program Files (x86)\\Dev-Cpp\\MinGW64\\bin\\g++.exe";
//编译选项:
const string cp=" -O2 -std=c++14 -Wl,--stack=2147483647" ;
//造数据器名称:
const string maker="maker";
//std 名称:
const string mystd="std";
//SPJ 名称 :
const string checker="checker"; 

/*
Color:
    需要 16 进制 0x 前缀。 
    0 = 黑色       8 = 灰色
    1 = 蓝色       9 = 淡蓝色
    2 = 绿色       A = 淡绿色
    3 = 浅绿色       B = 淡浅绿色
    4 = 红色       C = 淡红色
    5 = 紫色       D = 淡紫色
    6 = 黄色       E = 淡黄色
    7 = 白色       F = 亮白色
*/
template<typename... Args>void COLOR_PRINT(const int front_color,const int back_color,const char*s,Args...x){
    HANDLE handle=GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleTextAttribute(handle, BACKGROUND_INTENSITY | back_color*16 | FOREGROUND_INTENSITY | front_color);
    std::printf(s,x...);
    SetConsoleTextAttribute(handle,FOREGROUND_INTENSITY|7);
}
bool fileExists(const string&filename){std::ifstream infile(filename);return infile.good();}//文件是否存在。
void SaveData(int &c){
    string s="data\\data"+std::to_string(c++);
    system(("copy data.in "+s+".in").c_str());
    system(("copy data.ans "+s+".out").c_str());
}
bool runcode(const string&username){
	std::ofstream outvbs("KillCode.vbs");
	outvbs<<"WSCript.Sleep "<<TimeLimit+200<<"\nSet WshShell = WScript.CreateObject(\"WScript.Shell\")\nWshShell.Run \"taskkill /im "<<username<<".exe /f\", 0, True\n";
	outvbs.close();//用来关闭超时的进程。
    system("start /B KillCode.vbs");
    double tm=clock();
    unsigned int ls=system(("src\\"+username+".exe < data.in > data.out").c_str());
    tm=clock()-tm;
    system("taskkill /f /im wscript.exe 1>nul 2>nul"),system("taskkill /f /im cscript.exe 1>nul 2>nul");
    printf("\n%s: \n",username.c_str());
    if(tm>TimeLimit) return COLOR_PRINT(0x7,0x1,"Time Limit Exceeded"),printf("! Time used %.0lf ms.\n",tm),1;
    else if(ls>0xc0000000u) return COLOR_PRINT(0x7,0x5,"Runtime Error"),printf("! Time was unavailable!\n"),1;//RE:3221225725
    else if(ls) COLOR_PRINT(0x7,0x0,"Unknown Error!\n"),system("pause");
    else{
        unsigned int rs=system((checker+".exe").c_str());
        if(rs>0xc0000000u) COLOR_PRINT(0x7,0x5,"Checker Runtime Error"),printf("! Time was unavailable!\n"),system("pause");
        else if(!rs) COLOR_PRINT(0x7,0xA,"Accepted"),printf("! Time used %.0lf ms.\n",tm);
        else if(rs==1) return COLOR_PRINT(0x7,0x4,"Wrong Answer"),printf("! Time used %.0lf ms.\n",tm),1;
        else COLOR_PRINT(0x7,0x0,"Unknown Error!\n"),system("pause");
    }
    return 0;
}
void comp(const string&filename){system(("start /B \"g++ compiler\" \""+g__+"\" "+filename+".cpp -o "+filename+".exe "+cp).c_str());}
std::vector<string> user;
int main(){
	if(!fileExists("src.txt")) return puts("Can\' t find file src.txt!"),0;
	puts("Read...");
	std::ifstream fin("src.txt");
	for(string us;!fin.eof();)getline(fin,us),user.push_back(us);
	fin.close();
    system("del /Q data 1>nul 2>nul");
	system("md data 1>nul 2>nul");
	puts("Compiling...");
	if(!fileExists(maker+".cpp")) return puts(("Can\' t find file "+maker+".cpp!").c_str()),0;
	system(("del /Q "+maker+".exe 1>nul 2>nul").c_str()),comp(maker);
	if(!fileExists(checker+".cpp")) return puts(("Can\' t find file "+checker+".cpp!").c_str()),0;
	system(("del /Q "+checker+".exe 1>nul 2>nul").c_str()),comp(checker);
	if(!fileExists(mystd+".cpp")) return puts(("Can\' t find file "+mystd+".cpp!").c_str()),0;
	system(("del /Q "+mystd+".exe 1>nul 2>nul").c_str()),comp(mystd);
	for(const string&us:user){
		if(!fileExists("src\\"+us+".cpp")) return printf("Can\' t find file src\\%s.cpp!\n",us.c_str()),0;
		string sys="del /Q src\\"+us+".exe 1>nul 2>nul";
		system(sys.c_str());
		comp("src\\"+us);
		Sleep(500);
	}
	while(system("wmic process where (name=\"g++.exe\") get ProcessId 2>nul | findstr /r \"[0-9]\" >nul"));
	int maxc;
	printf("Enter the total number of pairs of shots you wish to obtain:");
	scanf("%d",&maxc);//输入你想要的数据组数。
	char sys_make[100],sys_mystd[100];
	strcpy(sys_make,(maker+".exe > data.in").c_str()),strcpy(sys_mystd,(mystd+".exe < data.in > data.ans").c_str());
	for(int t=1,c=1;c<=maxc;t++){
		system(sys_make);system(sys_mystd);
		bool sd=0;
		std::printf("Test %d:\n",t);
		for(const string&us:user)sd|=runcode(us);
		if(sd) SaveData(c);
		putchar('\n');
	}
	return 0;
}
posted @   Chen_Jinhui  阅读(616)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)

一言

你将不再是道具,而是成为人如其名的人。
——紫罗兰的永恒花园
点击右上角即可分享
微信分享提示