1. 概述
在线判题系统(Online Judge,简称 OJ)用于自动化评测用户提交的程序。其基本判题方式是将用户程序的输出与标准答案逐字节对比,从而给出评测结果。然而,在部分题目中,答案存在多解性、浮点数误差、顺序不确定等情况,单纯的逐字节比较无法满足需求。
为解决上述问题,OJ 系统支持 特判程序(Special Judge,简称 SPJ)。SPJ 是由命题人编写的独立判题程序,能够根据题目特点实现灵活的判题逻辑。
2. 判题的基本实现方法
OJ 系统的标准判题流程包括以下步骤:
编译:对用户提交的源代码进行编译,生成可执行文件。
运行:将测试输入文件提供给用户程序,得到输出文件。
比较:将用户输出与标准答案进行逐字节比较。
判定:根据比较结果输出判题结果,如 AC(Accepted)、WA(Wrong Answer)、TLE(Time Limit Exceeded)、RE(Runtime Error)等。
该流程在绝大多数题目中有效,但在某些特殊场景下会产生误判,从而需要 SPJ 来替代或补充标准比较逻辑。
3. 为什么需要特判
以下场景常常需要使用特判程序:
3.1 浮点数比较
浮点运算不可避免存在精度误差,例如标准答案为 3.141593,用户输出 3.1415926 实际上是正确的。此时需要通过设定误差范围(如 1e-6)进行比较。
3.2 输出顺序无关
某些题目允许解的顺序不同,例如输出集合 {1, 2, 3} 与 {3, 1, 2} 都是合法答案。特判可实现顺序无关的等价判断。
3.3 多解题目
题目本身存在多种合法解法,如最短路径题目中可能存在多条长度相同的路径。标准答案仅给出一种解,必须通过 SPJ 验证用户解的合法性。
3.4 宽松格式要求
部分题目允许输出中包含额外的空格、换行,甚至大小写不敏感。此类情况也需通过 SPJ 处理。
综上,SPJ 的引入保证了评测结果的科学性与公正性。
4. 特判的实现机制
OJ 系统调用 SPJ 的方式如下:
独立编译:SPJ 程序由命题人提供,独立编译生成可执行文件。
运行参数:OJ 在评测时调用 SPJ,并传入以下三个参数:
<input_file> <std_output_file> <user_output_file>
input_file:输入数据文件
std_output_file:标准输出文件
user_output_file:用户程序输出文件
返回码:SPJ 程序返回整数作为判题结果:
0:Accepted (AC)
1:Wrong Answer (WA)
其他返回值也可扩展定义。
输出信息:SPJ 可向标准输出打印判题说明,用于提示用户错误原因。
好学好教OJ系统中SPJ的使用
在好学好教OJ系统中,我们在每道编程题的操作菜单中,提供了编辑SPJ程序的菜单,如下图:
点击上图中的“编辑特判(SPJ)程序”,即可打开一个对话框,在对话框中可以输入SPJ程序,这个程序将会接收用户程序的输出结果并进行判断,为了方便用户,我们提供了C++语言和Python语言的SPJ模板程序,命题老师只需要修改special_judge这个函数,根据情况让它返回常量WA(错误)或者(AC)即可。除了special_judge函数,我们还提供了常用的一些函数,老师们可以选择使用。
#include <bits/stdc++.h>
using namespace std;
// 定义返回码 - 请勿修改
#define AC 0 // Accepted - 答案正确
#define WA 1 // Wrong Answer - 答案错误
// 全局变量用于输出信息
string judge_message = "";
/**
* 读取文件全部内容
* @param filename 文件路径
* @return 文件内容,失败时返回空字符串
*/
string read_file_content(const string& filename) {
ifstream file(filename);
if (!file.is_open()) {
cerr << "ERROR: Cannot open file " << filename << endl;
return "";
}
string content, line;
while (getline(file, line)) {
if (!content.empty()) content += "
";
content += line;
}
file.close();
// 去除末尾空白字符
while (!content.empty() && isspace(content.back())) {
content.pop_back();
}
return content;
}
/**
* 从文件读取所有数字
* @param filename 文件路径
* @return 数字向量,失败时返回空向量
*/
vector<double> read_numbers_from_file(const string& filename) {
ifstream file(filename);
vector<double> numbers;
if (!file.is_open()) {
cerr << "ERROR: Cannot open file " << filename << endl;
return numbers;
}
double num;
while (file >> num) {
numbers.push_back(num);
}
file.close();
return numbers;
}
/**
* 去除字符串首尾空白字符
*/
string trim(const string& str) {
size_t start = str.find_first_not_of("
");
if (start == string::npos) return "";
size_t end = str.find_last_not_of("
");
return str.substr(start, end - start + 1);
}
/**
* 分割字符串
* @param str 要分割的字符串
* @param delimiter 分隔符
* @return 分割后的字符串向量
*/
vector<string> split(const string& str, char delimiter = ' ') {
vector<string> result;
stringstream ss(str);
string item;
while (getline(ss, item, delimiter)) {
string trimmed = trim(item);
if (!trimmed.empty()) {
result.push_back(trimmed);
}
}
return result;
}
/**
* 特判逻辑 - 请在这里实现你的判题逻辑
*
* @param input_file 输入文件路径
* @param std_output_file 标准输出文件路径
* @param user_output_file 用户输出文件路径
* @return AC 或 WA
*/
int special_judge(const string& input_file, const string& std_output_file, const string& user_output_file) {
// ==================== 请在下方编写特判逻辑,根据需要返回AC或WA ====================
// ==================== 特判逻辑结束 ====================
}
/**
* 主函数 - 请勿修改
*/
int main(int argc, char* argv[]) {
// 检查参数数量
if (argc != 4) {
cerr << "用法: " << argv[0] << " <input_file> <std_output_file> <user_output_file>" << endl;
return WA;
}
string input_file = argv[1];
string std_output_file = argv[2];
string user_output_file = argv[3];
// 检查文件是否存在
vector<string> files = {input_file, std_output_file, user_output_file};
for (const string& filename : files) {
ifstream file(filename);
if (!file.is_open()) {
cerr << "ERROR: 文件不存在 " << filename << endl;
return WA;
}
file.close();
}
try {
// 调用特判逻辑
int result = special_judge(input_file, std_output_file, user_output_file);
// 输出判题信息
if (!judge_message.empty()) {
cout << judge_message << endl;
}
return result;
} catch (const exception& e) {
cerr << "ERROR: 特判程序运行出错: " << e.what() << endl;
return WA;
}
}
注意:除了special_judge函数内容,请勿修改模版代码的其他部分。
比如,下面是计算2个浮点数平均数的特判程序。这道题的要求是:
给定两个实数 a 和 b,请你输出它们的平均数,精确到小数点后 6 位。考虑到浮点数运算可能会有误差,所以需要用特判程序来判题。
下面是用C++实现的special_judge函数:
/**
* 特判逻辑 - 基于你原有的程序逻辑
*
* @param input_file 输入文件路径
* @param std_output_file 标准输出文件路径
* @param user_output_file 用户输出文件路径
* @return AC 或 WA
*/
int special_judge(const string& input_file, const string& std_output_file, const string& user_output_file) {
// 读取标准答案文件
string std_content = read_file_content(std_output_file);
if (std_content.empty()) {
judge_message = "Failed to read standard output file";
return WA;
}
// 读取用户输出文件
string user_content = read_file_content(user_output_file);
if (user_content.empty()) {
judge_message = "Failed to read user output file";
return WA;
}
try {
// 解析标准答案
double expected = stod(trim(std_content));
// 解析用户输出
double actual = stod(trim(user_content));
// 比较两个浮点数,允许 1e-6 的误差
if (compare_floats(expected, actual, 1e-6)) {
// 答案正确,不设置 judge_message(静默通过)
return AC;
} else {
// 答案错误,输出详细信息
return WA;
}
} catch (const exception& e) {
judge_message = "Invalid number format in output";
return WA;
}
}