如何在 UOJ 评测系统内配置通信题(详细揭秘)

前言

前几天搬了个远古 IOI 的通信丢到联测去了,vfk 的文档基本啥都没说,然后研究了两天 judger.h 差不多搞清楚怎么在 UOJ 上实现通信评测了。结果数据边数开小被暴力踩了然后赛时改的数据范围于是被 down 爆了

过两天 gcz 告诉我说有几个人在 U 裙里问怎么配通信题,我也想顺便记录一下配置过程免得下次配通信的时候又走弯路,当然也有可能没有下次了,故作此博客。

正文前的提示

在 UOJ,通信题的配置需要自行编写 judger.cpp。这意味着你需要拥有超级 root,也就是创建网站时第一个注册的账号,仅有这个账号有不使用内置 judger 的权利。

通信题的正确评测姿势

今年省选前集训的时候我和 iotang 已经分别搬了一个通信题了。在 lemon 里 iotang 支持的是一个 grader 同时加多个文件一起编译的评测方式,也是 JOISC 中的本地评测方式。但这样的评测方式有一个致命的缺陷是可以在一个文件里实现两个函数,在另一个文件里啥都不干……如果两个函数可以实现在同一个文件里的话那么显然可以把传入 encode 的信息存全局变量里然后 decode 的时候再拿出来就行了,通信题就没有意义了。

当时竟然没有人这么弄,说明选手素质很高

正确的评测方式应该是弄一个 encode_grader 和一个 decode_grader,先把 encode_graderencode 绑定在一起编译得到 encode 的输出,再把这个输出喂到 decodedecode_grader 编译的文件里得到 decode 的输出。

当然在 custom test 中用一个 grader 编译多个文件还是方便一些,可以直接用 judger.h 中的内置评测。

Makefile 的配置

export INCLUDE_PATH
CXXFLAGS = -I$(INCLUDE_PATH) -O2

all: chk judger

% : %.cpp
    $(CXX) $(CXXFLAGS) $< -o $@

这个 Makefile 的意义大概是将上传的文件中必要的 cpp 文件进行编译。对于通信题配置,有必要修改的只有 CXXFLAGSall 这两行。

CXXFLAGS 表示的是编译这些文件的开关,比如要开 C++17 就在这一行的最后面加上 -std=c++17

all 表示的是上传的包中需要编译的文件的名字,不需要包含 cpp 后缀名。举例:如果上传了 chk.cpp,judger.cpp 那么这一行就是 all: chk judger;如果使用内置 checker 那么这一行就是 all: judger;如果有校验器那么这一行就是 all: val judger

你需要注意的是 judger.cpp 总是要编译的。

judger.h 中的重要接口

如果你只想要一个模板 judger 的话可以跳过这一个部分。如果你想要 DIY 的话可以看一下。

这是本文的一个大头。judger.h 包含了评测时用到的一些重要的接口。下文会按照它们在 judger.h 中的出现顺序将有用的一些函数进行简单介绍。如果你不知道 judger.h 在哪里,可在 此处 查看。

下面将一些函数的定义删掉了,把里面的管道交互部分删掉了(因为我不会弄),然后把一些没用的都给删掉了,加了一些函数意义的注释。然后 RS_XXX 是在 uoj_env.h 里面的定义的一个宏,对应的是评测结果是什么,反正就是 RS_AC,RC_WA,RC_OLE,RC_JGF 之类的。

当然下面的还是很长,因为把 test_point 之类的单点测试参考编写都保留了。

#include "uoj_env.h"
//这个 .h 包含了所有可能的评测信息如 RS_AC/RS_WA 对应的宏定义。
using namespace std;

/*========================== execute ====================== */
//没啥用
/*======================== execute End ==================== */

/*========================= file ====================== */

string file_preview(const string &name, const size_t &len = 100);
//读取 name 文件的前 len 个字符,如果超出则在后面会加上三个省略号。
void file_hide_token(const string &name, const string &token);
//在 name 文件中尝试读取 token 并把它删掉。如果读取失败则将结果变为 "Unauthorized output"。
/*======================= file End ==================== */

/*====================== parameter ==================== */

struct RunLimit {//时空限制对应的结构体,名字很容易懂。
	int time;
	int real_time;
	int memory;
	int output;
};

const RunLimit RL_DEFAULT = RunLimit(1, 256, 64);
...
//这里有一些默认的限制

struct PointInfo  {//测试点结果信息
	int num;//测试点编号
	int scr;//分数
	int ust, usm;//使用的时间和空间
	string info, in, out, res;//评测信息/输入文件名/输出文件名/评测结果文件名(又好像)

	PointInfo(const int &_num, const int &_scr,
			const int &_ust, const int &_usm, const string &_info,
			const string &_in, const string &_out, const string &_res)
			: num(_num), scr(_scr),
			ust(_ust), usm(_usm), info(_info),
			in(_in), out(_out), res(_res) {
		if (info == "default") {
			if (scr == 0) {
				info = "Wrong Answer";
			} else if (scr == 100) {
				info = "Accepted";
			} else {
				info = "Acceptable Answer";
			}
		}//如果 info 是 "default",则会按照分数给出对应结果
	}
};

struct CustomTestInfo  {//自定义测试结果信息
	int ust, usm;//使用的时间和空间
	string info, exp, out;//信息/(可能)输入文件名/输出文件名

	CustomTestInfo(const int &_ust, const int &_usm, const string &_info,
			const string &_exp, const string &_out)
			: ust(_ust), usm(_usm), info(_info),
			exp(_exp), out(_out) {
	}
};

struct RunResult {//评测结果信息
	int type;//评测结果种类,这里填 RS_XXX
	int ust, usm;
	int exit_code;//程序的返回值,如果不为 0 说明 RE 了

	static RunResult failed_result(); //构造一个 "Judgement Failed" 的评测状态信息
	static RunResult from_file(const string &file_name); //从 file_name 文件中读取评测结果,如果读取失败返回 "Judgement Failed"
};
struct RunCheckerResult {
	//checker 的评测信息
	int type;//评测结果种类,这里填 RS_XXX
	int ust, usm;
	int scr;//分数
	string info;//信息

	static RunCheckerResult from_file(const string &file_name, const RunResult &rres); //通过 file_name 的 checker 输出和 rres 中提交程序的评测结果得到合并后的评测结果
	static RunCheckerResult failed_result(); //返回一个 "Checker Judgement Failed" 的评测结果
};
struct RunValidatorResult {//校验器结果信息
	int type;
	int ust, usm;
	bool succeeded;//是否校验成功
	string info;
	
	static RunValidatorResult failed_result(); //返回一个 "Validator Judgment Failed" 的校验器结果信息
};
struct RunCompilerResult {//评测状态信息
	int type;
	int ust, usm;
	bool succeeded;
	string info;

	static RunCompilerResult failed_result(); //返回一个 "Compile Failed"(不同于 CE)的评测信息
};

int problem_id;//字面意思
string main_path;//主目录
string work_path;//工作目录(放程序的目录)
string data_path;//数据存放目录
string result_path;//存放所有评测结果的目录,包括编译、val、chk

int tot_time   = 0;//最后显示的总时间,可以把这里改了变成最大时间之类的
int max_memory = 0;//最大空间
int tot_score  = 0;//总分
ostringstream details_out;//这又是啥
//vector<PointInfo> points_info;
map<string, string> config;//存 problem.conf 中所有的 key->val 的映射表

/*==================== parameter End ================== */

/*====================== config set =================== */

void print_config(); //输出 problem.conf 中的内容到 stderr 里
void load_config(const string &filename); //从 filename 里读入 problem.conf
string conf_str(const string &key, int num, const string &val);
string conf_str(const string &key, const string &val);
string conf_str(const string &key);
//conf_str(key,(num),(val)):读取 problem.conf 中 key 对应的结果,在带有 num 的情况中 key 会转为 key + "_" + to_string(num) 的形式,如果找不到返回 val,如果没有 val 返回空串
int conf_int(const string &key, const int &val);
int conf_int(const string &key, int num, const int &val);
int conf_int(const string &key);
//conf_int(key,(num),(val)):和上面一样,只是会将结果转成 int 返回,空串返回 0。

/*
key = 提交文件名+"_lang" 可以返回该提交文件的编译语言
在提交文件配置里面有一个 name 选项,那就是这里的这个提交文件名。
key = checker 可以得到 checker 的文件名
*/

string conf_input_file_name(int num);
string conf_output_file_name(int num);
//conf_(input/output)_file_name (num):通过 problem.conf 得到第 num 个数据的输入输出文件名。num<0 时会在开头加上 ex_ 表示 hack 数据,后面用的是 abs(num) 所以不会出现 ex_test-1 的情况。
RunLimit conf_run_limit(string pre, const int &num, const RunLimit &val);
RunLimit conf_run_limit(const int &num, const RunLimit &val);
//得到运行限制。其中 pre 表示前缀名,比如 checker 的限制就需要加入 "checker" 前缀。无前缀情况就是程序的运行限制。
void conf_add(const string &key, const string &val); //往 conf 里面加东西
bool conf_has(const string &key); //conf 里有没有这个 key
bool conf_is(const string &key, const string &val); //conf 里 key 对应的取值是不是 val

/*==================== config set End ================= */

/*====================== info print =================== */

template <class T>
inline string vtos(const T &v); //任意类型转换为 string
inline string htmlspecialchars(const string &s);//输出 HTML 格式的字符串,如将 < 换为 &:lt
inline string info_str(int id);//将 RS_XXX 格式换为对应信息字符串
inline string info_str(const RunResult &p);//输出结果的对应信息

void add_point_info(const PointInfo &info, bool update_tot_score = true); //输出一个测试点的信息,info 表示对应信息,update_tot_score 表示是否将该测试点的分数加入总分里,如 subtask 情况下就不需要加入。
void add_custom_test_info(const CustomTestInfo &info); //输出自定义测试的结果,info 表示对应信息。
void add_subtask_info(const int &num, const int &scr, const string &info, const vector<PointInfo> &points); //输出一个 subtask 进去。num 是编号、scr 是分数、info 是整个的评测信息、points 是每个点的评测信息
void end_judge_ok(); //正常结束
void end_judge_judgement_failed(const string &info); //评测挂了导致的结束
void end_judge_compile_error(const RunCompilerResult &res); //CE结束

void report_judge_status(const char *status); //往评测界面输出信息(如 compiling,judging ... status 就是直接输出的内容)
bool report_judge_status_f(const char *fmt, ...);//格式化输出
/*==================== info print End ================= */

/*========================== run ====================== */

struct RunProgramConfig {
};//这大概是一个用于运行提交的代码程序的信息库,用处不大

// @deprecated
// will be removed in the future
RunResult vrun_program(
		const char *run_program_result_file_name,
		const char *input_file_name,
		const char *output_file_name,
		const char *error_file_name,
		const RunLimit &limit,
		const vector<string> &rest);

RunResult run_program(const RunProgramConfig &rpc);//直接通过程序信息运行程序

RunResult run_program(
		const char *run_program_result_file_name,
		const char *input_file_name,
		const char *output_file_name,
		const char *error_file_name,
		const RunLimit &limit, ...);//运行任一程序,程序运行中状态放在 result_file,ioe 就是stdin/out/err,变长参数是 argv 内容,第一个一定放运行程序的名字。

RunValidatorResult run_validator(
		const string &input_file_name,
		const RunLimit &limit,
		const string &program_name);//跑 validator 检测 input_file_name 文件,validator 限制为 limit,validator 名字是 program_name。
		
RunCheckerResult run_checker(
		const RunLimit &limit,
		const string &program_name,
		const string &input_file_name,
		const string &output_file_name,
		const string &answer_file_name);//跑 checker,文件格式很容易懂。

RunCompilerResult run_compiler(const char *path, ...);
/*
编译程序。path 是编译文件的路径,变长参数是 argv。
举例:下文中 compile_cpp11 对该函数的调用方式为
run_compiler(path.c_str(),
	"/usr/bin/g++-4.8", "-o", name.c_str(), "-x", "c++", (name + ".code").c_str(), "-lm", "-O2", "-DONLINE_JUDGE", "-std=c++11", NULL);
*/

RunResult run_submission_program(
		const string &input_file_name,
		const string &output_file_name,
		const RunLimit &limit,
		const string &name,
		RunProgramConfig rpc = RunProgramConfig()); //运行提交的代码。注意前面的参数会覆盖 rpc 中的参数。

void prepare_run_standard_program();//把 data 里的 std 拉到 work 里

// @deprecated
// will be removed in the future
RunResult run_standard_program(
		const string &input_file_name,
		const string &output_file_name,
		const RunLimit &limit,
		RunProgramConfig rpc = RunProgramConfig()); //跑一遍标算

/*======================== run End ==================== */

/*======================== compile ==================== */

bool is_illegal_keyword(const string &name);//判断是不是 asm 系列

bool has_illegal_keywords_in_file(const string &name);//检查文本里有没有 asm 系列

RunCompilerResult compile_c(const string &name, const string &path = work_path);
...
//懂的都懂
RunCompilerResult compile(const char *name);//就一个 switch
RunCompilerResult compile_c_with_implementer(const string &name, const string &path = work_path);
...
RunCompilerResult compile_with_implementer(const char *name);
//和上面一样

/*====================== compile End ================== */

/*======================    test     ================== */

struct TestPointConfig {//单点测试信息
	int submit_answer;//是不是提答 -1->不知道 0->不是 1-> 是
	
	int validate_input_before_test;//测试前是否先跑val
	string input_file_name;//输入文件名
	string output_file_name;//输出文件名
	string answer_file_name;//答案文件名
	
	TestPointConfig()
			: submit_answer(-1), validate_input_before_test(-1) {
	}

	void auto_complete(int num);
	//如果上面没有设置是否是提交答案/输入输出文件名/是否提前跑一遍val,这里就会自动补全。num 是测试点编号。
};

PointInfo test_point(const string &name, const int &num, TestPointConfig tpc = TestPointConfig()) {
	//测试一个测试点。其中 name 是文件名,num 是测试点编号,tpc 是测试点信息
	tpc.auto_complete(num);

	if (tpc.validate_input_before_test) {
	//这里跑了一遍 val
		RunValidatorResult val_ret = run_validator(
				tpc.input_file_name,
				conf_run_limit("validator", 0, RL_VALIDATOR_DEFAULT),
				conf_str("validator"));
		if (val_ret.type != RS_AC) {
			return PointInfo(num, 0, -1, -1,
					"Validator " + info_str(val_ret.type),
					file_preview(tpc.input_file_name), "",
					"");
		} else if (!val_ret.succeeded) {
			return PointInfo(num, 0, -1, -1,
					"Invalid Input",
					file_preview(tpc.input_file_name), "",
					val_ret.info);
		}
	}

	RunResult pro_ret;
	//运行程序
	if (!tpc.submit_answer) {
		pro_ret = run_submission_program(
				tpc.input_file_name.c_str(),
				tpc.output_file_name.c_str(),
				conf_run_limit(num, RL_DEFAULT),
				name);
		if (conf_has("token")) {//读token
			file_hide_token(tpc.output_file_name, conf_str("token", ""));
		}
		if (pro_ret.type != RS_AC) {
			return PointInfo(num, 0, -1, -1,
					info_str(pro_ret.type),
					file_preview(tpc.input_file_name), file_preview(tpc.output_file_name),
					"");
		}
	} else {
		pro_ret.type = RS_AC;
		pro_ret.ust = -1;
		pro_ret.usm = -1;
		pro_ret.exit_code = 0;
	}
		//用 checker 检查结果
	RunCheckerResult chk_ret = run_checker(
			conf_run_limit("checker", num, RL_CHECKER_DEFAULT),
			conf_str("checker"),
			tpc.input_file_name,
			tpc.output_file_name,
			tpc.answer_file_name);
	if (chk_ret.type != RS_AC) {
		return PointInfo(num, 0, -1, -1,
				"Checker " + info_str(chk_ret.type),
				file_preview(tpc.input_file_name), file_preview(tpc.output_file_name),
				"");
	}
	return PointInfo(num, chk_ret.scr, pro_ret.ust, pro_ret.usm, 
		"default",
		file_preview(tpc.input_file_name), file_preview(tpc.output_file_name),
		chk_ret.info);
}

PointInfo test_hack_point(const string &name, TestPointConfig tpc) {
//在 Hack 模式下的评测
	tpc.submit_answer = false;
	tpc.validate_input_before_test = false;
	tpc.auto_complete(0);
	RunValidatorResult val_ret = run_validator(
			tpc.input_file_name,
			conf_run_limit("validator", 0, RL_VALIDATOR_DEFAULT),
			conf_str("validator"));
	if (val_ret.type != RS_AC) {
		return PointInfo(0, 0, -1, -1,
				"Validator " + info_str(val_ret.type),
				file_preview(tpc.input_file_name), "",
				"");
	} else if (!val_ret.succeeded) {
		return PointInfo(0, 0, -1, -1,
				"Invalid Input",
				file_preview(tpc.input_file_name), "",
				val_ret.info);
	}

	RunLimit default_std_run_limit = conf_run_limit(0, RL_DEFAULT);

	prepare_run_standard_program();
	RunProgramConfig rpc;
	rpc.result_file_name = result_path + "/run_standard_program.txt";
	RunResult std_ret = run_submission_program(
			tpc.input_file_name,
			tpc.answer_file_name,
			conf_run_limit("standard", 0, default_std_run_limit),
			"std",
			rpc);
	if (std_ret.type != RS_AC) {
		return PointInfo(0, 0, -1, -1,
				"Standard Program " + info_str(std_ret.type),
				file_preview(tpc.input_file_name), "",
				"");
	}
	if (conf_has("token")) {
		file_hide_token(tpc.answer_file_name, conf_str("token", ""));
	}
	PointInfo po = test_point(name, 0, tpc);
	po.scr = po.scr != 100;
	return po;
}

CustomTestInfo ordinary_custom_test(const string &name) {
//一般的 custom test 测试
	RunLimit lim = conf_run_limit(0, RL_DEFAULT);
	lim.time += 2;

	string input_file_name = work_path + "/input.txt";
	string output_file_name = work_path + "/output.txt";

	RunResult pro_ret = run_submission_program(
			input_file_name,
			output_file_name,
			lim,
			name);
	if (conf_has("token")) {
		file_hide_token(output_file_name, conf_str("token", ""));
	}
	string info;
	if (pro_ret.type == RS_AC) {
		info = "Success";
	} else {
		info = info_str(pro_ret.type);
	}
	string exp;
	if (pro_ret.type == RS_TLE) {
		exp = "<p>[<strong>time limit:</strong> " + vtos(lim.time) + "s]</p>";
	}
	return CustomTestInfo(pro_ret.ust, pro_ret.usm, 
			info, exp, file_preview(output_file_name, 2048));
}

/*======================  test End   ================== */

/*======================= conf init =================== */

void main_judger_init(int argc, char **argv);
void judger_init(int argc, char **argv);//初始化。程序开始先运行这一句。

/*===================== conf init End ================= */

judger.cpp(可能是)模板

直接丢我自己配 A+B+C 写的 judger.cpp 算了,如果有问题可以直接来喷我。

如果你真的很想知道怎么写,把这个和 judger.h 看完应该是没有问题的了。

具体使用是 programme_name_Aprogramme_name_B 填入对应的两个文件名字(第一个是 encode,第二个是 decode),然后在 require 底下丢 (programme_name_A).h,(programme_name_B).h,compile_(programme_name_A).cpp,compile_(programme_name_B).cpp,grader.cpp,最后这个 grader 就是一次把两个文件编译在一起运行的下发 grader,然后就能用了。

子任务依赖和拓扑排序是从南外 OJ 白嫖来的(

关于编译语言,因为初始的 UOJ 只有 C++C++11 两个选项,所以除了 C++ 选项以外其他的 C++ 选项一律都是 C++11。当然你也可以在 compile()custom_test() 里自行修改一下。

然后 token 这个东西 encodedecode 里都要输出。

没有写 Hack 的原因是自己试了一下发现没法重测和加 extests,有没有老哥教教我 QAQ

里面实现的时候有两个小的变化是:子任务部分分评测是百分比取 (min),有依赖则与前面的依赖分数取 (min);最终的评测时间是所有测试点时间的 (max)

#include<bits/stdc++.h>
#include"uoj_judger.h"
using namespace std;

string programme_name_A = "A" , programme_name_B = "B";

vector<vector<int> > get_subtask_dependencies(int n){
	vector<vector<int> > dependencies(n + 1, vector<int>());
	for (int t = 1; t <= n; t++){
		if(conf_str("subtask_dependence", t, "none") == "many"){
			string cur = "subtask_dependence_" + vtos(t);
			int p = 1;
			while(conf_int(cur, p, 0) != 0){
				dependencies[t].push_back(conf_int(cur, p, 0));
				p++;
			}
		}else if (conf_int("subtask_dependence", t, 0) != 0)
			dependencies[t].push_back(conf_int("subtask_dependence", t, 0));
	}
	return dependencies;
}

vector<int> subtask_topo_sort(int n, const vector<vector<int> > &dependencies)
{
	priority_queue<int> Queue;
	vector<int> degree(n + 1, 0), sequence;
	for (int t = 1; t <= n; t++) {
		for (int x : dependencies[t])
			degree[x]++;
	}
	for (int t = 1; t <= n; t++) {
		if (!degree[t])
			Queue.push(t);
	}
	while (!Queue.empty()) {
		int u = Queue.top();
		Queue.pop();
		sequence.push_back(u);
		for (int v : dependencies[u]) {
			degree[v]--;
			if (!degree[v])
				Queue.push(v);
		}
	}
	reverse(sequence.begin(), sequence.end());
	return sequence;
}

void compile(string name){
	report_judge_status_f("compiling %s" , name.c_str());
	string lang = conf_str(name + "_language");
	if(lang.size() < 3 || string(lang , 0 , 3) != "C++")
		end_judge_judgement_failed("Only C++ has been supported. Sorry for the inconvenience");
	string ver = lang == "C++" ? "-std=c++98" : "-std=c++11";
	RunCompilerResult comp
		= run_compiler(work_path.c_str() , "/usr/bin/g++" , "-o" , name.c_str() , "-x" , "c++" , ("compile_" + name + ".cpp").c_str() ,
		   (name + ".code").c_str() , "-lm" , "-O2" , "-DONLINE_JUDGE" , ver.c_str() , NULL);
	if(!comp.succeeded) end_judge_compile_error(comp);
}

vector < vector < int > > subtask_dependencies; vector < int > minscale;
int new_tot_time = 0;

PointInfo Judge_point(int id){
	TestPointConfig tpc; tpc.auto_complete(id);
	string tempoutput = work_path + "/temp_output.txt";
	
	RunLimit lim = conf_run_limit(id , RL_DEFAULT);
	RunResult runA = run_submission_program(
		tpc.input_file_name , 
		tempoutput , 
		lim, programme_name_A);
	if(runA.type != RS_AC)
		return PointInfo(id, 0, -1, -1,
			"Programme A " + info_str(runA.type) , 
			file_preview(tpc.input_file_name) ,
			file_preview(tempoutput) , "");
	if(conf_has("token")) file_hide_token(tempoutput , conf_str("token" , ""));
	
	RunResult runB = run_submission_program(
		tempoutput ,
		tpc.output_file_name, 
		lim, programme_name_B);
	if(runB.type != RS_AC)
		return PointInfo(id, 0, -1, -1,
			"Programme B " + info_str(runB.type) , 
			file_preview(tempoutput) ,
			file_preview(tpc.output_file_name) , "");
	if(runA.ust + runB.ust > lim.time * 1000)
		return PointInfo(id , 0 , -1 , -1 ,
			"Overall Time Limit Exceeded." ,
			"" , "" , "");
	
	if(conf_has("token")) file_hide_token(tpc.output_file_name , conf_str("token" , ""));
	RunCheckerResult chk_ret = run_checker(
				conf_run_limit("checker", id, RL_CHECKER_DEFAULT),
				conf_str("checker"),
				tpc.input_file_name,
				tpc.output_file_name,
				tpc.answer_file_name);
		if (chk_ret.type != RS_AC) {
			return PointInfo(id, 0, -1, -1,
					"Checker " + info_str(chk_ret.type),
					file_preview(tpc.input_file_name), file_preview(tpc.output_file_name),
					"");
		}

	return PointInfo(id, chk_ret.scr, runA.ust+runB.ust, max(runA.usm,runB.usm), 
			"default",
			file_preview(tpc.input_file_name), file_preview(tpc.output_file_name),
				chk_ret.info);
}

void Judge_subtask(int id){
	vector<PointInfo> subtask_testinfo;
	int mn = 100; for(auto t : subtask_dependencies[id]) mn = min(mn , minscale[t]);
	if(!mn){minscale[id] = 0; add_subtask_info(id , 0 , "Skipped" , subtask_testinfo); return;}
	int from = conf_int("subtask_end" , id - 1 , 0) , to = conf_int("subtask_end" , id , 0) , total = conf_int("subtask_score" , id , 0);
	string statestr = "default";
	RunLimit currentlimit = conf_run_limit(id , RL_DEFAULT);
	for(int i = from + 1 ; i <= to ; ++i){
		report_judge_status_f(("Running on test "+to_string(i)+" on subtask "+to_string(id)).c_str());
		PointInfo res = Judge_point(i);
		subtask_testinfo.push_back(res); mn = min(mn , res.scr); new_tot_time = max(new_tot_time , res.ust);
		if(!mn){statestr = res.info; break;}
	}
	minscale[id] = mn;
	add_subtask_info(id , 1.0 * mn / 100 * total , (statestr == "default" ? (mn == 100 ? "Accepted" : "Acceptable Answer") : statestr) , subtask_testinfo);
}

void ordinary_test(){
	compile(programme_name_A); compile(programme_name_B);
	int num = conf_int("n_subtasks");
	subtask_dependencies = get_subtask_dependencies(num); minscale.resize(num + 1);
	vector < int > seq = subtask_topo_sort(num , subtask_dependencies);
	for(auto t : seq) Judge_subtask(t);
	tot_time = new_tot_time; bool alright = 1; for(int i = 1 ; i <= num ; ++i) alright &= minscale[i] == 100; 
	if(alright){
		int m = conf_int("n_ex_tests");
		for (int i = 1; i <= m; i++) {
			report_judge_status_f("Judging Extra Test #%d", i);
			PointInfo po = Judge_point(-i);
			if (po.scr != 100) {
				po.num = -1;
				po.info = "Extra Test Failed : " + po.info + " on " + vtos(i);
				po.scr = -3;
				add_point_info(po);
				end_judge_ok();
			}
		}
		if (m != 0) {
			PointInfo po(-1, 0, -1, -1, "Extra Test Passed", "", "", "");
			add_point_info(po);
		}
		end_judge_ok();
	}
	end_judge_ok();
}

void custom_test(){
	report_judge_status_f("Compiling...");
	string langA = conf_str(programme_name_A + "_language") , langB = conf_str(programme_name_B + "_language");
	if(langA.size() < 3 || langB.size() < 3 || string(langA , 0 , 3) != "C++" || string(langB , 0 , 3) != "C++")
		end_judge_judgement_failed("Only C++ has been supported. Sorry for the inconvenience");
	langA = langA == "C++" ? "-std=c++98" : "-std=c++11";
	RunCompilerResult comp
		= run_compiler(work_path.c_str() , "/usr/bin/g++" , "-o" , "grader" , "-x" , "c++" , "grader.cpp" ,
		   (programme_name_A + ".code").c_str() , (programme_name_B + ".code").c_str() , "-lm" , "-O2" , "-DONLINE_JUDGE" , langA.c_str() , NULL);
	if(!comp.succeeded) end_judge_compile_error(comp);
	report_judge_status_f("Judging...");
	CustomTestInfo res = ordinary_custom_test("grader");
	add_custom_test_info(res);
	end_judge_ok();
}

int main(int argc , char** argv){
	judger_init(argc , argv);
	
	if (conf_is("custom_test", "on")) custom_test(); else ordinary_test();
	
	return 0;
}

problem.conf

相比一般的 problem.conf 只需要修改 use_builtin_judgeroff 就行了。

最后一步:提交文件配置

把提交文件配置和其他配置一栏修改成下面两行

[{"name":"A","type":"source code","file_name":"A.code"},{"name":"B","type":"source code","file_name":"B.code"}]

{"custom_test_requirement":[{"name":"A","type":"source code","file_name":"A.code"},{"name":"B","type":"source code","file_name":"B.code"},{"name":"input","type":"text","file_name":"input.txt"}],"view_content_type":"ALL","view_details_type":"ALL"}

里面的 AB 对应上面的 programme_name_Aprogramme_name_B。搞完之后大概长这样

如何在 UOJ 评测系统内配置通信题(详细揭秘)

这样提交的时候就可以提交两个文件了。类似的也可以进行推广,比如可以弄一些类似可以提交一个打表的文件然后用程序去读取它的题目之类的(