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;
    }
}
文章分享二维码

微信扫码分享这篇文章

扫描二维码即可在手机中继续阅读,也方便转发给老师、家长或同学。

上一篇 2025年9月20日CSP-J1初赛试题 下一篇 好学好教发布OJ随机测试数据图形化生成工具(附使用说明)