通信题配置指南
众所周知,通信题很多代码好写而且还是黑题,只是在洛谷上没法测评。欢迎大家读完这篇文章之后,踊跃配置通信题,增加黑题通过数!
部分代码使用 AI 编写,本文可能存在时效性。
洛谷配置通信题大致原理
普通的通信题测评方法是 run-twice,即在评测机上运行两次。而洛谷平台由于历史原因并不支持这种玩法。而在一年前,一位洛谷用户 发现 洛谷支持如 fork、pipe、execve 等的高级系统调用。而通过 fork 开辟的子进程内存不共享。于是我们可以通过一些手段使得父进程与子进程通信,然后在子进程中运行选手代码,这时候选手做出的修改都不会修改父进程的内存,以此实现通信题防作弊。
而洛谷配置通信题借助的是洛谷的交互题功能。
洛谷交互题配置概述
洛谷交互题需要额外实现 interactive_lib.cpp 和 checker.cpp。其中,interactive_lib.cpp 会和选手程序一起编译,需要实现 main()。checker.cpp 需要被注册为 Interaction,用于读入输入数据,将输入数据传给 interactive_lib.cpp,读取 interactive_lib.cpp 的输出,判断答案正确性。
具体来说:
checker.cpp:从inf读入输入数据,然后cout把需要交互库知道的信息通过 stdout 倒灌进interactive_lib.cpp的 stdin;interactive_lib.cpp:通过cin从 stdin 读入数据,运行交互逻辑,将最终结果通过cout输出;checker.cpp:从ouf读入交互库输出数据,判断结果正确性。
配置交互题需要添加 交互题、Special Judge 两个 tag。
洛谷通信题配置教程
手动实现 fork 实现通信题麻烦、不安全。Moeebius 编写的 https://github.com/Xiaohuba/luogu-communication-lib 则解决了这一困境。
下面,以这一问题作为例子:
Alice 读入一个
\le 10^9 的正整数,将其转换为长度不超过40 的\tt 01 字符串,Bob 接收到这个字符串,将其转换回原本的正整数。
数据目录如下:
1.in
1.ans
2.in
2.ans
...
interactive_lib.cpp
checker.cpp
打开仓库,复制其中的 luogu-communication-lib.hpp,将其粘贴至 interactive_lib.cpp 的开头(洛谷不允许在数据文件夹中加入其他文件,所以我们无法将其作为一个头文件放入数据文件夹)。
interactive_lib.cpp
接下来,我们编写 interactive_lib.cpp 的具体逻辑。
#include <iostream>
#include <string>
// 1. 声明选手的函数接口
std::string Alice(int target);
int Bob(std::string message);
// 2. 顶层 grader
void communication_grader() {
int N;
std::cin >> N;
// 创建 Alice 进程
auto proc_A = CommunicationLib::SubProcess::safe_invoke();
// 给 Alice 发送身份和数据
proc_A->fout << "ALICE\n" << N << std::endl;
// 拿到 Alice 返回的 01 字符串
std::string secret_msg;
proc_A->fin >> secret_msg;
// 判断字符串合法性
bool is_valid = true;
if (secret_msg.length() > 40) {
is_valid = false;
}
for (char c : secret_msg) {
if (c != '0' && c != '1') {
is_valid = false;
break;
}
}
if (!is_valid) {
std::cout << "BAD_PROTOCOL" << std::endl;
return;
}
// 等待 Alice 退出程序
proc_A->guard();
// 创建 Bob 进程
auto proc_B = CommunicationLib::SubProcess::safe_invoke();
// 给 Bob 发送身份和来自 Alice 的密文
proc_B->fout << "BOB\n" << secret_msg << std::endl;
// 拿到 Bob 解密出来的最终数字
int final_answer;
proc_B->fin >> final_answer;
// 等待 Bob 退出程序
proc_B->guard();
// 将 Bob 的答案返回给 checker(也可以直接当场判断然后输出 AC 或 WA)
// 实际使用时,最好输出一个隐藏 token 给 checker 检查,防止选手代码抢优先级
std::cout << final_answer << std::endl;
}
// 注册为 grader
COMMUNICATION_LIB_REGISTER_GRADER(communication_grader);
// 3. 子进程入口
int main() {
std::string role;
std::cin >> role;
if (role == "ALICE") {
int target;
std::cin >> target;
// 调用选手的 Alice 函数,并返回给顶层 grader
std::cout << Alice(target) << std::endl;
return 0;
} else if (role == "BOB") {
std::string msg;
std::cin >> msg;
// 调用选手的 Bob 函数,并返回给顶层 grader
std::cout << Bob(msg) << std::endl;
return 0;
}
return 0;
}
让我们来解读这段代码。
第一部分是洛谷对交互题的要求,在这里我们遵守。
第二部分,我们实现了一个顶层 grader,在这里我们需要创建子进程,执行 Alice 进程逻辑与 Bob 进程逻辑。
我们通过 CommunicationLib::SubProcess::safe_invoke() 创建子进程,然后类似 cout << 地使用 proc->fout << 给子进程的 stdin 灌入数据,类似 cin >> 地使用 proc->fin >> 从子进程的 stdout 读出数据。最后我们还需要使用 proc->guard() 等待子进程结束。
接下来,我们通过一个宏 COMMUNICATION_LIB_REGISTER_GRADER 注册了这个顶层 grader。这里的原理是,如果我们发现当前是顶层 grader,那么就会通过构造函数优先级大于 main() 的性质,运行 communication_grader(),然后立刻退出;否则,我们就会执行 main(),作为子进程逻辑。
第三部分,我们实现子进程的逻辑,使用 cin 读入当前角色,执行相关逻辑,将数据通过 cout 返回给顶层 grader。
实际使用时,可以使用随机字符串的 namespace 包裹我们的代码,防止定义变量与选手重名。
最好在最后输出一个隐藏 token 给 checker 检查,防止选手代码抢优先级。这里指的是,选手可能可以通过其他一些技巧比我们的顶层 grader 先执行,然后直接返回 AC,这时候输出一个隐藏 token 就可以避免这种情况的发生(或许可能还是可以绕过,总之这部分其实和交互题防 hack 是类似的,更多方法见 https://riteme.blog.uoj.ac/blog/2255)。
checker.cpp
接下来,我们编写 checker.cpp 的逻辑。
#include "testlib.h"
#include <iostream>
#include <string>
int main(int argc, char* argv[]) {
registerInteraction(argc, argv);
int S = inf.readInt();
std::cout << S << std::endl;
int user_ans = ouf.readInt();
if (S == user_ans) {
quitf(_ok, "Success!");
} else {
quitf(_wa, "Wrong Answer. Expected %d, but Bob restored %d", S, user_ans);
}
}
没什么好说的。如果在交互库输出了隐藏 token,这里记得检查。
标签方面,类似交互题地添加 交互题、Special Judge 两个 tag 即可,但是也可添加 通信题 tag。
Q & A
还是太难理解了,有没有例子
你可在 https://www.luogu.com.cn/problem/U648439 该题附件区下载 data.zip 自行研究。
如何调试 grader
随便点开一道题,打开洛谷 IDE,将 grader 和 std 粘到一起即可调试。
子进程会继承父进程的信息吗
不会。虽然单单使用 fork() 创建的进程确实会继承父进程的信息,但是如果使用了 luogu-communication-lib,就会在子进程中使用 exec 重新调用自己,擦除所有内存信息。