Hydro SPJ 进阶:评测机会说人话了!
0. 故事背景
众所周知,一场比赛需要一张头图

(如果你觉得这张图中二得要命那就尽管骂吧)
但是,注意到这张图实际上是Hydro的评测结果页面P出来的。有没有感觉Wrong Answer后面那些黑字过于自然了?——没错,我的评测机成精了(bushi
Hydro SPJ 进阶:评测机会说人话了!
上次我们在【告别答案唯一!】中讲了SPJ的基本用法。但是,你有没有想过——让评测机在 WA 的时候,给选手一句带中文的提示?
比如选手输出错了,你可以告诉他:
- “第 3 个测试点:输出格式有误,请检查是否有多余空格”
- “第 5 个测试点:数值超出范围 [1, 10]”
- “第 7 个测试点:你的程序跑得太慢了,但还没超时——再优化一下吧”
这不光能让选手更快定位问题,还能让评测体验变得更有“人情味”。今天就来聊聊怎么在 Hydro 的 SPJ 里输出中文消息。
1. 一个简单的开始
其实在 testlib 里输出中文,比你想象的要简单。直接往 quitf() 里写中文就行:
#include "testlib.h"
int main(int argc, char* argv[]) {
registerTestlibCmd(argc, argv);
int user = ouf.readInt();
int answer = ans.readInt();
if (user != answer) {
quitf(_wa, "答案错误:期望 %d,实际 %d", answer, user);
}
quitf(_ok, "回答正确!答案是 %d", answer);
}
就这么简单。quitf() 的字符串参数支持 UTF-8 中文,Hydro 的评测页面会原样显示出来。
但是,如果只是这样,每个 WA 点显示的消息都一样,还是不够好玩。能不能让不同的测试点显示不同的提示?
2. 核心技巧:获取当前测试点编号
Hydro 在运行 SPJ 时,会通过环境变量 HYDRO_TESTCASE 把当前测试点的编号传进来。我们可以在 checker.cc 里读取这个变量:
#include "testlib.h"
#include <cstdlib> // for getenv
#include <string>
int main(int argc, char* argv[]) {
registerTestlibCmd(argc, argv);
// 获取当前测试点编号
const char* testcase_str = std::getenv("HYDRO_TESTCASE");
int testcase_id = 0;
if (testcase_str != nullptr) {
testcase_id = std::atoi(testcase_str);
}
// 读取选手输出和标准答案
int user = ouf.readInt();
int answer = ans.readInt();
if (user != answer) {
// 根据测试点编号输出不同的中文提示
switch (testcase_id) {
case 1:
quitf(_wa, "第 1 个点 WA:检查一下是不是忘了处理负数?");
break;
case 2:
quitf(_wa, "第 2 个点 WA:输入的两个数可能很大,用 int 够吗?");
break;
case 3:
quitf(_wa, "第 3 个点 WA:输出格式有误,检查是否有多余空格或换行");
break;
case 4:
quitf(_wa, "第 4 个点 WA:你的程序好像把加法算成了减法");
break;
case 5:
quitf(_wa, "第 5 个点 WA:边界情况没处理好,试试 n=0 或 n=1");
break;
default:
quitf(_wa, "答案错误:期望 %d,实际 %d", answer, user);
}
}
quitf(_ok, "第 %d 个点通过 ✅", testcase_id);
}
这样,选手在评测结果页面看到的就是带中文的、针对性的错误提示,而不是千篇一律的 Wrong Answer。
3. 更实用的场景
除了“整活”,这种技巧在实际出题中也有很多用处:
场景一:分阶段提示
如果你的题目有多个 Subtask,可以在不同的测试点给出不同的反馈:
if (testcase_id <= 3) {
quitf(_wa, "小数据点 WA,检查基本逻辑");
} else if (testcase_id <= 6) {
quitf(_wa, "中数据点 WA,可能是时间复杂度太高");
} else {
quitf(_wa, "大数据点 WA,检查是否溢出或超时");
}
场景二:输出格式检查
// 假设选手应该输出 n 个数
for (int i = 0; i < n; i++) {
if (ouf.seekEof()) {
quitf(_wa, "第 %d 个测试点:输出数量不足,期望 %d 个数", testcase_id, n);
}
// ... 读取并检查
}
场景三:部分分提示
结合 quitp() 给部分分时,也可以加上中文说明:
if ( /*部分正确*/ ) {
quitp(50, "部分正确:输出格式没问题,但数值有误,给一半分");
}
(等会儿?做慈善呢?数值有误还判对?)
4. 注意事项
① 文件编码
checker.cc 文件本身要保存为 UTF-8 编码,否则中文会变成乱码。大多数现代编辑器(VS Code、Sublime等)默认都是 UTF-8,直接用就行。
② 环境变量是否存在
HYDRO_TESTCASE 是 Hydro 特有的环境变量,在本地调试时可能不存在。所以代码里要判断指针是否为空:
const char* testcase_str = std::getenv("HYDRO_TESTCASE");
if (testcase_str != nullptr) {
testcase_id = std::atoi(testcase_str);
}
③ 不要用 cout / printf
在 SPJ 里不能用 cout 或 printf 直接输出。所有给选手看的消息都必须通过 quitf()、quitp() 或 quit() 系列函数返回。
④ 消息长度
虽然理论上没有严格限制,但评测页面显示区域有限,建议消息不要太长,控制在 50 个字以内效果最好。
5. 完整示例
下面是一个完整的 checker.cc,它结合了之前文章里 A+B 的例子,给每个测试点定制了不同的中文 WA 提示:
#include "testlib.h"
#include <cstdlib>
#include <string>
int main(int argc, char* argv[]) {
setName("A+B with Chinese hints");
registerTestlibCmd(argc, argv);
// 获取测试点编号
const char* testcase_str = std::getenv("HYDRO_TESTCASE");
int testcase_id = 0;
if (testcase_str != nullptr) {
testcase_id = std::atoi(testcase_str);
}
int answer = ans.readInt();
int user = ouf.readInt();
// 特殊整活:输出 114514 给 50% 分数
if (user == 114514) {
quitp(50, "你输出了 114514,太搞了,给你一半分吧 🤣");
}
if (user != answer) {
// 根据测试点给出不同的中文提示
const char* hints[] = {
"检查一下是不是忘了处理负数?",
"输入的两个数可能很大,用 int 够吗?",
"输出格式有误,检查是否有多余空格",
"你的程序好像把加法算成了减法",
"边界情况没处理好,试试 n=0 或 n=1",
"这个点专门测大数,用 long long 了吗?",
"你的程序输出为空,检查一下读入",
"这个点数据比较极限,再想想算法",
"离 AC 就差一步了,检查一下符号",
"最后一个点了,冲鸭!检查所有细节"
};
int idx = (testcase_id - 1) % 10;
quitf(_wa, "第 %d 个点 WA:%s", testcase_id, hints[idx]);
}
quitf(_ok, "第 %d 个点通过!答案是 %d ✅", testcase_id, answer);
}
6. 结语
让评测机“说人话”其实就这么简单——核心就两步:
- 用
getenv("HYDRO_TESTCASE")拿到当前测试点编号; - 用
quitf()输出带中文的提示信息。
这个小技巧能让 评测机成精 你的题目评测体验上升一个档次。选手不再面对冷冰冰的 WA,而是能收到有针对性的、带中文的提示——对新手尤其友好。
下次出题的时候,不妨试试给你的 SPJ 加点“人话”吧

浙公网安备 33010602011771号