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.in、1.ans …… 10.in、10.ans 连同 checker.cc、config.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: testlib 和 checker: checker.cc 是 SPJ 的标配,无论哪种写法都一样。
3. 对比总结
| 场景 | 读取来源 | 判断方式 | 典型用途 |
|---|---|---|---|
| 答案唯一(如 A+B) | ans |
比较选手输出与标准答案 | 大多数传统题 |
| 答案不唯一(如排列) | inf |
自行验证输出是否满足条件 | 构造题、方案题 |
| 需要部分分 | quitp |
根据输出质量给分 | 多解、近似答案 |
一些实用 Tips
- 文件后缀:
checker.cc必须是.cc,Hydro 只认这个。 - testlib 内置:Hydro 已内置 testlib,直接
#include "testlib.h"即可,无需额外上传。 - 本地调试:可以用命令
./checker 1.in 1.ans user.ans测试 SPJ 逻辑是否正确(需要先编译)。 - 检查多余输出:用
ouf.seekEof()可以防止选手输出额外内容,让判题更严谨。 - 输入文件:对于答案不唯一的题,
.ans文件可以随便放个占位内容(例如0),但.in文件必须正确,因为 SPJ 要从中读数据。
4. 结语
回顾一下完整流程:
准备测试数据(.in / .ans 或只有 .in)↓
编写 checker.cc(基于 testlib,选择合适的读取源)↓
创建 config.yaml(指定 checker_type 和 checker)↓
打包上传到 Hydro
希望这篇教程能帮你少走弯路。有问题欢迎评论区交流,记得收藏备用,下次写SPJ直接拿来改就行