通信题配置指南

· · 科技·工程

众所周知,通信题很多代码好写而且还是黑题,只是在洛谷上没法测评。欢迎大家读完这篇文章之后,踊跃配置通信题,增加黑题通过数!

部分代码使用 AI 编写,本文可能存在时效性。

洛谷配置通信题大致原理

普通的通信题测评方法是 run-twice,即在评测机上运行两次。而洛谷平台由于历史原因并不支持这种玩法。而在一年前,一位洛谷用户 发现 洛谷支持如 forkpipeexecve 等的高级系统调用。而通过 fork 开辟的子进程内存不共享。于是我们可以通过一些手段使得父进程与子进程通信,然后在子进程中运行选手代码,这时候选手做出的修改都不会修改父进程的内存,以此实现通信题防作弊。

而洛谷配置通信题借助的是洛谷的交互题功能。

洛谷交互题配置概述

洛谷交互题需要额外实现 interactive_lib.cppchecker.cpp。其中,interactive_lib.cpp 会和选手程序一起编译,需要实现 main()checker.cpp 需要被注册为 Interaction,用于读入输入数据,将输入数据传给 interactive_lib.cpp,读取 interactive_lib.cpp 的输出,判断答案正确性。

具体来说:

配置交互题需要添加 交互题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 重新调用自己,擦除所有内存信息。