交互造题指南
写在前面
这篇文章写在联合省选 2022 两天后。笔者此前造交互题时发现全网都搜不到好上手的教程。笔者一想,自己进集训队的可能性为 ,打算写一篇博客,在退役之前留下真正有用的东西。
NOI Style
NOI 风格的交互题就是通过函数调用实现交互。我们假设题目的名字叫做 "test",那么通常一个交互系统中需要这样的几个文件:
test.cpp
:选手的源代码。grader.cpp
:和test.cpp
交互并判断是否正确的程序。这个程序将下发一份示例版本给选手供其测试自己的程序。当然,为了避免选手从下发的示例版本中发现漏洞并攻击评测,评测时使用的版本应与下发的示例版本(例如在变量名或实现方法上)有所不同。test.h
:是grader.cpp
和test.cpp
需包含的头文件,以实现交互。这个程序直接下发给选手,无需改动。checker.cpp
:在一些评测环境下,需要这个程序将grader.cpp
的输出转化为分数。这个程序不需要下发给选手。
under Lemon
-
test.h
在这个头文件里需要定义所有同时出现在
grader.cpp
和test.cpp
中的函数(即test.cpp
可调用grader.cpp
的函数,和grader.cpp
可调用test.cpp
的函数)。一个好的test.h
通常需要包含尽量少的头文件和命名空间,以尊重不同选手的代码习惯。举例:#include <vector> #include <string> void solve(int n); std::string query(std::vector<int> v); void answer(int x);
-
grader.cpp
在 Lemon 下采用这样的评测机制:设置若干个对应程序正确情况的密钥字符串,由
grader.cpp
评定出选手程序情况后按照对应状态,向输出文件(通常就是test.out
)先输出这个密钥字符串,再输出分数。checker.cpp
根据密钥解密状态得到分数,反馈给 Lemon。如果读取到非密钥的字符串,则判定为答案错误。这样就防止了选手攻击交互库,向输出文件自行输出分数的可能性。同样为了安全,
grader.cpp
中的所有全局变量应被包装在一个namespace
中。下发给选手的 grader 则不需要文件输入输出、密钥。
举例:
#include <bits/stdc++.h> using namespace std; namespace GLOBAL{ const string key = "KEY"; int T, flg, n; double scr, res; } std::string query(std::vector<int> v){ /* ... */ } void answer(int x){ /* ... */ } int main(){ freopen("test.in", "r", stdin); freopen("test.out", "w", stdout); GLOBAL::scr = 100; for(scanf("%d", &T); T--; ){ scanf("%d", &GLOBAL::n); GLOBAL::res = 100; GLOBAL::flg = false; solve(); if(!flg) GLOBAL::scr = 0; GLOBAL::scr = min(GLOBAL::scr, GLOBAL::res); } printf("%s\n", key); printf("%Lf\n", scr); return 0; }
-
checker.cpp
在 Lemon 下评测时首先需要包含头文件
testlib.h
(Lemon 版的,帮助 - 使用手册 告诉我们 testlib for lemon 从 Lemon 的源代码仓库中获得)并把testlib.h
放到和checker.cpp
同一目录下,并调用registerLemonChecker(argc, argv);
这句话完成初始化。在 testlib - OI Wiki 这个页面有 testlib 详细的使用方法。概括地说,只需要从
inf
(输入文件),ouf
(选手输出),ans
(参考输出)这几个对象中读取评判正确所需的信息,然后调用quitf(verdict, message, ...)
,quitp(score, message, ...)
(两个用法和printf( ... )
大致相同的函数)返回 Lemon 可识别的状态。更有针对性地说,对于交互题而言,采用上述的方法,通常先从
ouf
中读取grader
输出的密钥,只有当密钥正确时才继续读取分数(或其它信息);否则直接quitf(_wa, ...)
。根据ouf
中的后续信息,通常判断出正确、错误、部分正确这几种状态。正确则quitf(_ok, ...)
;错误则quitf(_wa, ...)
;部分正确则quitp(score, ...)
(这里score
需要对该测试点的满分作标准化,即对读取到的 间的数乘 的系数,注意:使用_pc(score)
的写法是错误的!)。举例:
#include "testlib.h" #include <bits/stdc++.h> using namespace std; const string key = "KEY"; int main(int argc, char* argv[]) { registerLemonChecker(argc, argv); std::string sout = ouf.readLine(), sans = ans.readLine(); if (sout != key) quitf(_wa, "%s", sout.c_str()); int score = ouf.readInt(); if(score == 100) quitf(_ok, "ok"); else quitp(score / 100 * perfectScore, "partially correct"); return 0; }
-
Lemon 中的配置
如图所示。
under LOJ(旧版)
旧版 LOJ 处理 NOI 风格交互题,笔者只会一种鸡贼的办法。思路是将这道题设置为传统题 - special judge,并通过 special judge 充当评分的角色。而 grader.cpp
中的内容将会被封装到 test.h
中。(根据合理的推测,将题目设置为交互题,并同样将 grader.cpp
中的内容封装到 test.h
中,且使用下一节中的方法编写 data.yml
,也可能成功实现。但笔者此时缺少验证的条件。)下面详细介绍所需要的每一个文件。
-
test.h
事实上,只需要将原先评测用的
grader.cpp
中的内容全部粘贴在test.h
的末尾。如果在设置时选择标准输入输出,则这里也不需要文件输入输出。此时为了保证安全,
grader.cpp
中复制过来的内容也应包含尽量少的头文件和命名空间。举例:
#include <vector> #include <string> void solve(int n); std::string query(std::vector<int> v); void answer(int x); namespace GLOBAL{ const string key = "KEY"; int T, flg, n; double scr, res; } std::string query(std::vector<int> v){ /* ... */ } void answer(int x){ /* ... */ } int main(){ GLOBAL::scr = 100; for(scanf("%d", &T); T--; ){ scanf("%d", &GLOBAL::n); GLOBAL::res = 100; GLOBAL::flg = false; solve(); if(!flg) GLOBAL::scr = 0; GLOBAL::scr = min(GLOBAL::scr, GLOBAL::res); } printf("%s\n", key); printf("%Lf\n", scr); return 0; }
-
checker.cpp
和上一节中
checker.cpp
的写法是几乎一致的,除了testlib.h
的使用方法略有差异。返回部分正确的分数时需要quitf(_pc(score), ...)
。举例:
#include "testlib.h" #include <bits/stdc++.h> using namespace std; const string key = "KEY"; int main(int argc, char* argv[]) { registerTestlibCmd(argc, argv); std::string sout = ouf.readLine(), sans = ans.readLine(); if (sout != key) quitf(_wa, "%s", sout.c_str()); int score = ouf.readInt(); if(score == 100) quitf(_ok, "ok"); else quitp(_pc(score), "partially correct"); return 0; }
-
data.yml
这是专门为 LOJ 下评测而生的最关键的文件。通过该文件彻底设置所有评测细节。需要写好
data.yml
并上传到评测数据当中。第一部分是子任务设置:
subtasks: - score: 50 type: min cases: ['1-1','1-2','1-3','1-4'] - score: 50 type: min cases: ['2-1','2-2','2-3','2-4']
第二部分是上传的测试数据的后缀:注意交互题不需要参考输出,但是为了正确识别,必须创建空的输出文件(例如
1-1.out
)上传。inputFile: '#.in' outputFile: '#.out'
第三部分描述头文件:
extraSourceFiles: - language: cpp files: - name: test.h dest: test.h - language: cpp11 files: - name: test.h dest: test.h - language: cpp17 files: - name: test.h dest: test.h - language: cpp11-clang files: - name: test.h dest: test.h - language: cpp17-clang files: - name: test.h dest: test.h - language: c files: - name: test.h dest: test.h - language: cpp14-noi files: - name: test.h dest: test.h
第四部分是 SPJ:
specialJudge: language: cpp11 fileName: checker.cpp
把这些拼起来即可。
-
LOJ 配置
题目类型选择传统,评测方式选择 Special Judge,输入输出可以选择标准输入输出或者文件输入输出,但必须和
test.h
中的实现相符。将checker.cpp
、test.h
和所有的输入输出文件(.in
,.out
)上传到测试数据中。即可。
under LOJ(新版)
鉴于笔者没有这样的经验,这里提供链接 评测设置 #6 - LibreOJ,相信读者一定可以参考该题目学会。
CF Style
CF 风格的交互题就是通过标准输入输出流实现交互。很遗憾 Lemon Lime 并不支持这样的交互方式,因此以下仅针对旧版 LOJ 进行说明。(Polygon?没听说过) 在这套系统中需要 test.cpp
(选手提交),grader.cpp
和 data.yml
。
-
grader.cpp
旧版的 LOJ 的交互从
input
文件(没有后缀名) 中读取输入数据,输出到score.txt
中(由于文件名是公开不变的,密钥就变得格外重要。)输出的分数是 间的。
举例:
#include <bits/stdc++.h> using namespace std; namespace GLOBAL{ const string key = "KEY"; int T, flg, n; double scr, res; } std::string query(std::vector<int> v){ /* ... */ } void answer(int x){ /* ... */ } int main(){ freopen("input", "r", stdin); freopen("score.txt", "w", stdout); GLOBAL::scr = 100; for(scanf("%d", &T); T--; ){ scanf("%d", &GLOBAL::n); GLOBAL::res = 100; GLOBAL::flg = false; solve(); if(!flg) GLOBAL::scr = 0; GLOBAL::scr = min(GLOBAL::scr, GLOBAL::res); } printf("%s\n", key); printf("%Lf\n", scr); return 0; }
-
data.yml
和上一节中没有太大的区别。
举例:
interactor: language: cpp11 fileName: grader.cpp subtasks: - score: 50 type: min cases: ['1-1', '1-2'] - score: 50 type: min cases: ['2-1', '2-2'] inputFile: '#.in'
-
LOJ 设置
设置为交互题,把这两个文件传上去就行了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探