好学好教OJ系统新增特判(Special Judge,SPJ)功能

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,并传入以下三个参数:

  1. <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函数,我们还提供了常用的一些函数,老师们可以选择使用。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. // 定义返回码 - 请勿修改
  4. #define AC 0 // Accepted - 答案正确
  5. #define WA 1 // Wrong Answer - 答案错误
  6. // 全局变量用于输出信息
  7. string judge_message = "";
  8. /**
  9. * 读取文件全部内容
  10. * @param filename 文件路径
  11. * @return 文件内容,失败时返回空字符串
  12. */
  13. string read_file_content(const string& filename) {
  14. ifstream file(filename);
  15. if (!file.is_open()) {
  16. cerr << "ERROR: Cannot open file " << filename << endl;
  17. return "";
  18. }
  19. string content, line;
  20. while (getline(file, line)) {
  21. if (!content.empty()) content += "
  22. ";
  23. content += line;
  24. }
  25. file.close();
  26. // 去除末尾空白字符
  27. while (!content.empty() && isspace(content.back())) {
  28. content.pop_back();
  29. }
  30. return content;
  31. }
  32. /**
  33. * 从文件读取所有数字
  34. * @param filename 文件路径
  35. * @return 数字向量,失败时返回空向量
  36. */
  37. vector<double> read_numbers_from_file(const string& filename) {
  38. ifstream file(filename);
  39. vector<double> numbers;
  40. if (!file.is_open()) {
  41. cerr << "ERROR: Cannot open file " << filename << endl;
  42. return numbers;
  43. }
  44. double num;
  45. while (file >> num) {
  46. numbers.push_back(num);
  47. }
  48. file.close();
  49. return numbers;
  50. }
  51. /**
  52. * 去除字符串首尾空白字符
  53. */
  54. string trim(const string& str) {
  55. size_t start = str.find_first_not_of("
  56. ");
  57. if (start == string::npos) return "";
  58. size_t end = str.find_last_not_of("
  59. ");
  60. return str.substr(start, end - start + 1);
  61. }
  62. /**
  63. * 分割字符串
  64. * @param str 要分割的字符串
  65. * @param delimiter 分隔符
  66. * @return 分割后的字符串向量
  67. */
  68. vector<string> split(const string& str, char delimiter = ' ') {
  69. vector<string> result;
  70. stringstream ss(str);
  71. string item;
  72. while (getline(ss, item, delimiter)) {
  73. string trimmed = trim(item);
  74. if (!trimmed.empty()) {
  75. result.push_back(trimmed);
  76. }
  77. }
  78. return result;
  79. }
  80. /**
  81. * 特判逻辑 - 请在这里实现你的判题逻辑
  82. *
  83. * @param input_file 输入文件路径
  84. * @param std_output_file 标准输出文件路径
  85. * @param user_output_file 用户输出文件路径
  86. * @return AC 或 WA
  87. */
  88. int special_judge(const string& input_file, const string& std_output_file, const string& user_output_file) {
  89. // ==================== 请在下方编写特判逻辑,根据需要返回AC或WA ====================
  90. // ==================== 特判逻辑结束 ====================
  91. }
  92. /**
  93. * 主函数 - 请勿修改
  94. */
  95. int main(int argc, char* argv[]) {
  96. // 检查参数数量
  97. if (argc != 4) {
  98. cerr << "用法: " << argv[0] << " <input_file> <std_output_file> <user_output_file>" << endl;
  99. return WA;
  100. }
  101. string input_file = argv[1];
  102. string std_output_file = argv[2];
  103. string user_output_file = argv[3];
  104. // 检查文件是否存在
  105. vector<string> files = {input_file, std_output_file, user_output_file};
  106. for (const string& filename : files) {
  107. ifstream file(filename);
  108. if (!file.is_open()) {
  109. cerr << "ERROR: 文件不存在 " << filename << endl;
  110. return WA;
  111. }
  112. file.close();
  113. }
  114. try {
  115. // 调用特判逻辑
  116. int result = special_judge(input_file, std_output_file, user_output_file);
  117. // 输出判题信息
  118. if (!judge_message.empty()) {
  119. cout << judge_message << endl;
  120. }
  121. return result;
  122. } catch (const exception& e) {
  123. cerr << "ERROR: 特判程序运行出错: " << e.what() << endl;
  124. return WA;
  125. }
  126. }

注意:除了special_judge函数内容,请勿修改模版代码的其他部分。

比如,下面是计算2个浮点数平均数的特判程序。这道题的要求是:
给定两个实数 a 和 b,请你输出它们的平均数,精确到小数点后 6 位。考虑到浮点数运算可能会有误差,所以需要用特判程序来判题。
下面是用C++实现的special_judge函数:

  1. /**
  2. * 特判逻辑 - 基于你原有的程序逻辑
  3. *
  4. * @param input_file 输入文件路径
  5. * @param std_output_file 标准输出文件路径
  6. * @param user_output_file 用户输出文件路径
  7. * @return AC 或 WA
  8. */
  9. int special_judge(const string& input_file, const string& std_output_file, const string& user_output_file) {
  10. // 读取标准答案文件
  11. string std_content = read_file_content(std_output_file);
  12. if (std_content.empty()) {
  13. judge_message = "Failed to read standard output file";
  14. return WA;
  15. }
  16. // 读取用户输出文件
  17. string user_content = read_file_content(user_output_file);
  18. if (user_content.empty()) {
  19. judge_message = "Failed to read user output file";
  20. return WA;
  21. }
  22. try {
  23. // 解析标准答案
  24. double expected = stod(trim(std_content));
  25. // 解析用户输出
  26. double actual = stod(trim(user_content));
  27. // 比较两个浮点数,允许 1e-6 的误差
  28. if (compare_floats(expected, actual, 1e-6)) {
  29. // 答案正确,不设置 judge_message(静默通过)
  30. return AC;
  31. } else {
  32. // 答案错误,输出详细信息
  33. return WA;
  34. }
  35. } catch (const exception& e) {
  36. judge_message = "Invalid number format in output";
  37. return WA;
  38. }
  39. }

微信扫一扫,分享此文章

少儿编程教学平台

联系我们

微信

aguibo002

邮箱

haoxuehaojiao在163.com

Loading
我们已经收到您的信息,将尽快联系您!