三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Hydro SPJ 配置:告别答案唯一!

Hydro SPJ 配置:告别答案唯一!

Hydro SPJ 配置:告别答案唯一!

Hydro 自带的默认比较器(type: default)只做严格的文本比对——选手输出必须和标准答案 ans.ans 一模一样才能 AC。但现实中有很多题目答案不唯一,比如:

  • 输出浮点数允许一定误差;
  • 输出任意一种合法方案即可;
  • 需要根据输出质量给部分分;
  • 甚至像下面第一个例子那样,输出 114514 只给一半分(整活专用)。

这时候就需要 Special Judge(SPJ) 出场了。Hydro 支持多种 SPJ 格式,其中最常用的是基于 testlib 的方案。

话不多说,开干——


1. 基础篇:神秘的特判部分分

1.1. 题目与需求

题目链接:/hc_system/p/P0004

题目就是最经典的 A+B,但我们加了一个魔改需求:

  • 正常输出 a+b 得满分;
  • 如果输出 114514,只给 50% 的分数;
  • 其他情况判 WA。

这个需求虽然 屁用没有,但很好地展示了 SPJ 的部分分能力。

1.2. 准备测试数据

准备好 10 组正常的 A+B 测试数据,文件名格式为:

1.in  1.ans
2.in  2.ans
...
10.in 10.ans

每组数据都是一对整数 a b 和它们的和。

1.3. 编写 checker.cc

checker.cc 是 SPJ 的灵魂,它读取选手输出和标准答案,决定得分。

#include "testlib.h"int main(int argc, char* argv[]) {setName("compares two signed integers");registerTestlibCmd(argc, argv);int ja = ans.readInt();   // 标准答案int pa = ouf.readInt();   // 选手输出// 整活:输出 114514 只给 50% 分数if (pa == 114514)quitp(50, "You outputed 114514");// 正常比较if (ja != pa)quitf(_wa, "expected %d, found %d", ja, pa);quitf(_ok, "answer is %d", ja);
}

关键函数说明:

  • registerTestlibCmd(argc, argv) —— 初始化 testlib,必须调用。
  • ans.readInt() —— 从标准答案文件(.ans)读整数。
  • ouf.readInt() —— 从选手输出文件读整数。
  • quitp(score, message) —— 给部分分,score 是当前测试点的得分百分比(0~100)。
  • quitf(_wa, ...) —— 判为 Wrong Answer。
  • quitf(_ok, ...) —— 判为 Accepted。

注意:文件名后缀必须是 .cc,不能是 .cpp,否则 Hydro 可能不识别。

1.4. 配置 config.yaml

在测试数据文件夹里创建 config.yaml

type: default
score: 10          # 每个测试点 10 分,10 个点共 100 分
time: 100ms
memory: 64mchecker_type: testlib
checker: checker.cc

字段说明:

字段 含义
type 题目类型,default 表示传统题
score 每个测试点的分值
time / memory 资源限制
checker_type 比较器类型,填 testlib
checker 自定义比较器文件名

注意:quitp(50) 给的是当前测试点的 50%,而不是总分的 50%。所以每个点本来 10 分,输出 114514 就拿 5 分。

1.5. 上传与测试

1.in1.ans …… 10.in10.ans 连同 checker.ccconfig.yaml 打包成 zip,上传到 Hydro 的题目文件区。系统会自动解压并识别配置。

上传位置:

题目数据/
├── 1.in
├── 1.ans
├── 2.in
├── 2.ans
├── checker.cc
└── config.yaml

提交代码验证:

  • 正常输出 a+b → 每个点 10 分,AC;
  • 输出 114514 → 每个点 5 分,总分 50 分;
  • 其他错误 → WA。

2. 进阶篇:答案不唯一

2.1. 题目与需求

题目:https://hydro.ac/d/hc_system/p/P0005

这种题的难点在于 答案不唯一,不能依赖固定的 .ans 文件,必须自己写逻辑验证输出是否合法。

2.2. 编写 checker.cc

这次我们不读标准答案(.ans),而是从输入文件.in)中读取 \(n\),然后验证选手输出是否为 1..n 的排列。

#include "testlib.h"
#include <vector>int main(int argc, char* argv[]) {setName("check if output is a permutation of 1..n");registerTestlibCmd(argc, argv);// 1. 从输入文件读取 nint n = inf.readInt();// 2. 读选手输出的 n 个整数std::vector<int> a;for (int i = 0; i < n; i++) {a.push_back(ouf.readInt());}// 3. 检查是否有多余输出if (!ouf.seekEof()) {quitf(_wa, "Extra output after %d numbers", n);}// 4. 验证是否为 1~n 的排列std::vector<bool> vis(n + 1, false);for (int i = 0; i < n; i++) {int x = a[i];if (x < 1 || x > n) {quitf(_wa, "Number %d is out of range [1, %d]", x, n);}if (vis[x]) {quitf(_wa, "Duplicate number %d", x);}vis[x] = true;}// 5. 全部通过quitf(_ok, "Valid permutation of 1..%d", n);
}

因为判断答案不依赖.ans文件,所以这里面瞎写一个就行。但是一定要有.ans文件,不然Hydro会慷慨地赠送所有人一个灰色的Format Error

2.3. 与基础 SPJ 的核心差异

对比项 A+B SPJ 排列 SPJ
读取来源 ans(标准答案) inf(输入文件)
判断逻辑 比较选手输出与标准答案 验证输出是否满足排列条件
标准答案文件 必须存在(提供参考值) 存在但不被读取
部分分 支持(quitp 本例中只用 AC/WA

关键点:对于答案不唯一的题目,我们通过 inf 读取题目给定的输入参数(如 n),然后自行验证输出的合法性,完全不依赖 .ans 文件。

2.4. config.yaml 配置

和基础篇一样:

type: default
score: 10         # 每个点 10 分,按实际测试点数量
time: 100ms
memory: 64mchecker_type: testlib
checker: checker.cc

checker_type: testlibchecker: checker.cc 是 SPJ 的标配,无论哪种写法都一样。


3. 对比总结

场景 读取来源 判断方式 典型用途
答案唯一(如 A+B) ans 比较选手输出与标准答案 大多数传统题
答案不唯一(如排列) inf 自行验证输出是否满足条件 构造题、方案题
需要部分分 quitp 根据输出质量给分 多解、近似答案

一些实用 Tips

  1. 文件后缀checker.cc 必须是 .cc,Hydro 只认这个。
  2. testlib 内置:Hydro 已内置 testlib,直接 #include "testlib.h" 即可,无需额外上传。
  3. 本地调试:可以用命令 ./checker 1.in 1.ans user.ans 测试 SPJ 逻辑是否正确(需要先编译)。
  4. 检查多余输出:用 ouf.seekEof() 可以防止选手输出额外内容,让判题更严谨。
  5. 输入文件:对于答案不唯一的题,.ans 文件可以随便放个占位内容(例如 0),但 .in 文件必须正确,因为 SPJ 要从中读数据。

4. 结语

回顾一下完整流程:

准备测试数据(.in / .ans 或只有 .in)↓
编写 checker.cc(基于 testlib,选择合适的读取源)↓
创建 config.yaml(指定 checker_type 和 checker)↓
打包上传到 Hydro

希望这篇教程能帮你少走弯路。有问题欢迎评论区交流,记得收藏备用,下次写SPJ直接拿来改就行

← 返回列表