stdio 型交互题本地测评指南
私以为 Testlib 的 Interactor 部分写得比较依托,且现有大部分 Interactor 都直接译自 Mike 版本,无法获取有效信息,尤其是 运行指令。其实可能是我比较菜无法理解人类语言,已严肃红温。 故经过研究给出相对简单好写的对拍方法,不用依赖 Testlib, 但要求会一点基础的 Bash。如果你不会 Bash,文末也会给出 C++ 简单移植版本,希望能帮到大家。
以 QOJ14726 Restore the Array 为例,下面给出 cur.cpp、data.cpp:
/**
* 年挽红枫,溪傍芦荻
*
* @file H.cpp
* @date 2025-11-25 11:01:47
*/
#include <iostream>
#include <vector>
#include <unordered_map>
int n;
std::unordered_map<int, int> mp;
std::vector<int> res;
int query(int x) {
if (mp.count(x)) return mp[x];
std::cout << "? " << x << std::endl;
int y;
return std::cin >> y, mp[x] = y ^ x;
}
void solve(int x, int t, int up) {
if (!t || res.size() == n - 1) return res.push_back(x);
int y = query(up | x & (1 << t) - 1);
if (x == y) return res.push_back(x);
int o = std::__lg(x ^ y);
y = query(up | x & ((1 << t) - 1 ^ (1 << o) - 1));
solve(y, o, up | x & ((1 << t) - 1 ^ (1 << o) - 1));
solve(x, o, up);
}
void work() {
std::cin >> n, mp.clear(), res.clear();
solve(query(0), 30, 0);
std::cout << "! ";
for (int x : res) std::cout << x << ' ';
std::cout << std::endl;
}
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
// int _T;
// for (std::cin >> _T; _T--; ) work();
work();
return 0;
}
/**
* 年挽红枫,溪傍芦荻
*
* @file data.cpp
* @date 2025-11-25 15:30:28
*/
#include <iostream>
#include <random>
#include <sstream>
#include <set>
namespace DataMaker {
std::mt19937 rnd(std::random_device{}());
inline int rand_int(int l, int r) { return rnd() % (r - l + 1) + l; }
template <class T>
inline void read(char *s, T &x) { std::istringstream(s) >> x; }
} // namespace DataMaker
using namespace DataMaker;
int n, V;
std::set<int> s;
int main(int argc, char **argv) {
std::cin.tie(nullptr)->sync_with_stdio(false);
read(argv[1], n), read(argv[2], V);
std::cout << n << '\n';
while (s.size() < n) s.insert(rand_int(0, V));
for (int x : s) std::cout << x << ' ';
std::cout << '\n';
return 0;
}
这里预先放出普通对拍与 SPJ 对拍代码:
#! /bin/bash
# Usage: ./pat.sh [数据生成器参数]
g++ -o data data.cpp -O2
g++ -o cur cur.cpp -O2
g++ -o std std.cpp -O2
for ((i = 1; ; i++)); do
./data $* > cur.in
./cur < cur.in > cur.out
./std < cur.in > std.out
if diff cur.out std.out -Z; then
echo -e "\x1B[32;1mAC\x1B[0m on #$i."
else
echo -e "\x1B[31;1mWA\x1B[0m on #$i."
break
fi
done
#! /bin/bash
# Usage: ./pat.sh [数据生成器参数]
g++ -o data data.cpp -O2
g++ -o cur cur.cpp -O2
# g++ -o checker checker.cpp -O2
# 由于带 testlib.h 的 checker 编译奇慢无比,通常在外部完成
for ((i = 1; ; i++)); do
./data $* > cur.in
./cur < cur.in > cur.out
if ! ./checker cur.in cur.out cur.out; then
break
fi
done
解释:
- Generator 直接从 执行命令中 获取参数,故主函数声明为
int main(int argc, char **argv)。这样写的好处在于,修改数据范围 不需要修改、重新编译原代码,且 不用每次生成数据输入一遍数据范围(命令行有历史),这样可以方便对拍数据进行变更以及对拍程序的运行。 - 不过局限在于,调用 pat.sh 时后面 不能接受更多参数,无法进行更多的个性化设置(如程序名相对固定,checker 参数也只能预先确定)。理论上对输入进行特判可以 优化,但我们需要一个足以在考场上写完的东西,轻量级优先。(好吧 CNOI 考场不会出 stdio 型交互……)
- 使用 Testlib 编写 Checker 用来对拍时,可以通过
./checker的返回值快速判断是否通过数据点,不过 Bash 的if疑似比较抽风,return 0;判true,return 1;判false,需要注意。
现在考虑编写一个 Interactor。由于没有 Testlib 支持,我们需要手动维护 数据流、输入流和输出流。由于是 stdio 交互,输入输出流可以直接使用 std::cin std::cout,在外部重定向,那么使用 std::ifstream 实现数据流读入 cur.in 就能避免 std::freopen 占用 stdin。
/**
* 年挽红枫,溪傍芦荻
*
* @file checker.cpp
* @date 2025-11-25 15:35:21
*/
#include <fstream>
#include <iostream>
#include <vector>
int n, cnt;
std::vector<int> a, b;
void read() {
std::ifstream fin("cur.in");
fin >> n, a.resize(n);
for (int &x : a) fin >> x;
}
int main() {
read();
std::cout << n << std::endl;
while (true) {
char opt;
if (std::cin >> opt, opt ^ '?') break;
int v, ans = 0;
std::cin >> v, ++cnt;
for (int x : a) ans = std::max(ans, x ^ v);
std::cout << ans << std::endl;
}
b.resize(n);
for (int &x : b) std::cin >> x;
if (a != b) return std::cerr << "Wrong Elements!\n", 1;
if (cnt > n * 2 - 2)
return std::cerr << "Extra Queries with " << cnt << "!\n", 1;
return std::cerr << "Accepted!\n", 0;
}
这里为对拍考虑,Checker 也通过返回值反映数据通过情况,即非正常退出返回 \(1\)。那么只剩下最令人头大的 pat.sh 编写。主要问题在两点:一、我怎么让两个程序通讯;二、我怎么让两个程序 同时跑起来。
让一个程序输出到另一个程序中,可以使用 管道,如使用 ps -ef | grep firefox 找到火狐浏览器进程。但 Interactor 并非 单向输出,这就需要同时存在两个管道分别调控 ./cur | checker 和 ./checker | cur,显然不太现实。如果单独开两个空文件 tmp.in tmp.out 执行 ./cur < tmp.in > tmp.out,又发现程序不会理你自个儿读入 EOF 跑路了。于是引入 命名管道,它将管道变成一个实体文件用于交互,在 Linux 中调用 mkfifo [name1] [name2] [...] 即可创建。其特点在于类似 std::queue 的先进先出模式,以及需要 同时接入 输入端和输出端才会启用,这就符合我们的交互需求。
命名管道创建后,调用方式与普通文件一致,即 mkfifo ii oo; ./cur < ii > oo。但光有一个程序在运行可不行,需要俩玩意儿一起运行才能交互。开两个终端分别执行 ./cur < ii > oo ./checker > ii < oo 固然可以完成单次交互,然而对拍程序只有一个,因此考虑 将其中一个程序挂入后台运行。Linux 中在命令后面添加 & 即可创建一个后台进程执行命令,原终端可以继续运行,如 evince down/statement.pdf & 可以直接打开 statement.pdf,并可以不关闭文档查看器继续在终端中执行命令。由于 Checker 返回值需要用来判断,可以考虑将 cur.cpp 先挂到后台再运行 Checker,这样就能实现一个 pat.sh:
#! /bin/bash
# Usage: ./pat.sh [数据生成器参数]
g++ -o data data.cpp -O2
g++ -o cur cur.cpp -O2
g++ -o checker checker.cpp -O2
mkfifo ii oo
for ((i = 1; ; i++)); do
./data $* > cur.in
./cur < ii > oo &
if ! ./checker > ii < oo; then
break
fi
done
rm ii oo
同时需要注意一个坑点:重定向命令顺序决定管道打开顺序,而由于命名管道的性质,若调用 ./cur < ii > oo; ./checker < oo > ii 将一根筋变成两头堵,ii 和 oo 都停在 阻塞状态,交互不了一点。这种情况的特征在于 主函数都不会执行,所以检查是容易的(比如在主函数第一行写上 std::cerr << "**** ***\n")。我才不会说这个东西我调了 1.5 h 才发现。
于是你就可以本地测试 stdio 型交互了!如果你不会使用 Bash 的话,可以采用下面的 C++ 方案实现 同时调用两个程序进行交互(注意这不是 pat.sh):
/**
* 年挽红枫,溪傍芦荻
*
* @file judger.cpp
* @date 2025-11-12 15:27:34
*/
#include <cstdio>
#include <unistd.h>
int fd[2][2];
bool fl;
int main(int argc, char **argv) {
pipe(fd[0]), pipe(fd[1]), fl = fork();
dup2(fd[fl][0], fileno(stdin)), dup2(fd[fl ^ 1][1], fileno(stdout));
execve(argv[1 + fl], nullptr, nullptr);
}
其中:
fork()用于创建子进程并返回进程 PID,若为子进程则返回 \(0\)。同时父子进程 完全一致,这保证了fd在两个进程中相同;pipe(int fd[2])用于创建管道;dup2(int oFd, int nFd)用于重定向输入输出;execve(const char *s, ?, ?)用于将当前进程用s程序完全代替。

浙公网安备 33010602011771号