只需准备 3 个 .cpp 文件,编译后运行 duipai.exe 即可。
步骤一:准备 3 个程序
题目示例:输入 n 和 n 个整数,输出最大值。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
int ans = 0; // bug:全负数时输出 0
for (int i = 0; i < n; i++) {
int x; cin >> x;
ans = max(ans, x);
}
cout << ans << endl;
return 0;
}
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
int ans = a[0];
for (int i = 1; i < n; i++) ans = max(ans, a[i]);
cout << ans << endl;
return 0;
}
#include <bits/stdc++.h>
using namespace std;
int main() {
srand(time(0));
int n = rand() % 3 + 1; // 小数据,方便 std 快速跑完
cout << n << endl;
for (int i = 0; i < n; i++) {
cout << rand() % 20 - 15 // 大概率出负数,方便触发 bug << " \n"[i == n - 1];
}
return 0;
}
步骤二:编写对拍总控程序
#include <cstdlib>
#include <cstdio>
int main() {
int T = 0;
while (true) {
T++;
system("gen.exe > data.in");
system("std.exe < data.in > std.out");
system("my.exe < data.in > my.out");
// fc 返回 0 表示相同,非 0 表示不同
if (system("fc my.out std.out > nul") != 0) {
printf("===== WA at test #%d =====\n", T);
return 0;
} else {
printf("Case #%d AC\n", T);
}
}
}
步骤三:编译并运行
g++ 命令!只需在 IDE 里依次打开 my.cpp、std.cpp、gen.cpp、duipai.cpp 四个文件,分别点击一次"编译"(Dev-C++ 按 F9,Code::Blocks 按 F9 或 F11),生成好对应的 .exe 文件后,最后单独运行 duipai.cpp(或双击 duipai.exe)即可,效果完全一样。
g++ my.cpp -o my.exe -O2 -std=c++14
g++ std.cpp -o std.exe -O2 -std=c++14
g++ gen.cpp -o gen.exe -O2 -std=c++14
g++ duipai.cpp -o duipai.exe -O2 -std=c++14
duipai.exe
对拍涉及几个"角色",先用比喻理解它们,后面所有操作都会清晰很多。
你写的可能翻车的程序。速度快但可能有 bug。对拍的目的就是找出它的错误。
暴力但绝对正确的程序。速度慢但答案一定对。是对拍的"裁判"。
数据生成器。负责源源不断地出"卷子"(随机输入数据),让两个程序分别作答。
| 术语 | 含义 | 备注 |
|---|---|---|
| 对拍 / Stress Testing | 通过大量随机数据验证程序正确性 | 名称来源于"两两比对输出" |
| 暴力程序 / BF | 时间复杂度高但逻辑简单的正确程序 | 通常指 std.cpp |
| 正解 / 优化解 | 你写的最终提交程序 | 通常指 my.cpp |
| 数据生成器 / Generator | 按题目格式生成合法随机输入的程序 | 对拍质量的关键 |
| Special Judge / SPJ | 答案不唯一时,用来判断输出是否合法的程序 | 进阶内容,见附录D |
| Testlib | Codeforces 出题库,含随机数、SPJ 等工具 | 进阶内容,见附录F |
对拍自动化循环流程
详细步骤:
为什么主推 C++ 对拍?因为你已经在用 C++ 写算法了,不需要额外学批处理或 Shell 语法。在 Dev-C++ 或 VS Code 里一键编译运行即可,和平时写题完全一样。
每个程序的作用、写法、注意事项,逐一说明:
my.cpp(待测程序)—— 写你平时的解法即可
唯一要注意:不要写 freopen。用对拍时,输入/输出通过命令行重定向完成,代码中无需文件操作。
// ❌ 不要这样写(对拍时)
freopen("data.in", "r", stdin);
freopen("my.out", "w", stdout);
// ✅ 这样写,平时和对拍都能用
int main() {
// 正常读入即可,无需 freopen
}
std.cpp(暴力程序)—— 核心是保证正确
暴力程序不要求效率,但逻辑必须简单、直接、无优化。最好是另一种思路实现,避免和 my 犯同样的错误。
| 题目类型 | std 写法建议 |
|---|---|
| 最值问题 | O(n²) 枚举所有子区间 / 子序列 |
| 图论最短路 | Floyd O(n³) 或 暴力 BFS |
| 数论问题 | 按定义暴力枚举 |
| DP 问题 | 暴力搜索 / 回溯(指数级但正确) |
gen.cpp(数据生成器)—— 对拍质量的关键
gen 的唯一要求是:生成的数据必须符合题目输入格式。格式不合法,my 和 std 都可能 RE。
下面是完整版 duipai.cpp,每行都有注释:
#include <cstdlib> // system() 需要
#include <cstdio> // printf() 需要
#include <ctime> // clock() 计时需要
int main() {
int T = 0; // 测试用例计数器
while (true) { // 无限循环,直到发现错误
T++;
// 【1】生成数据:gen.exe 的输出重定向到 data.in 文件
system("gen.exe > data.in");
// 【2】运行 std(暴力程序):从 data.in 读,输出到 std.out
system("std.exe < data.in > std.out");
// 【3】运行 my(待测程序):同样的输入,输出到 my.out
double beg = clock(); // 开始计时
system("my.exe < data.in > my.out");
double end = clock(); // 结束计时
// 【4】比较输出
// fc 命令:比较两个文件;返回 0 表示相同,非 0 表示不同
// > nul:隐藏 fc 的详细输出,只保留返回值
if (system("fc my.out std.out > nul") != 0) {
printf("========== WA at test #%d ==========\n", T);
printf("Input saved in: data.in\n");
printf("Your output: my.out\n");
printf("Correct output: std.out\n");
return 0; // 停止对拍
} else {
// 显示通过,顺带显示 my 的运行时间
printf("Case #%d AC (%.0lf ms)\n",
T, (end - beg) * 1000.0 / CLOCKS_PER_SEC);
}
}
return 0;
}
system("cmd"):执行一条系统命令(Windows 下是 cmd,Linux 下是 bash)>:输出重定向,把程序输出写到文件<:输入重定向,从文件读入数据fc a b:Windows 比较文件命令(Linux 对应 diff a b)⏱️ 关于测速的说明:上面代码用 clock() 测量 my.exe 的运行时间,但这个时间包含了操作系统启动进程和磁盘 I/O 的开销,不是纯算法运行时间,仅供参考。实际评测时间通常会更短。
system("my.exe ...") 调用的是 my.exe 这个文件。
如果你没有用 g++ 把 my.cpp 编译成 my.exe,system() 会执行失败(找不到文件)。
完整编译命令(Windows,在 Dev-C++ 或 VS Code 终端里运行):
.cpp 文件,分别点击一次"编译"(Dev-C++ 按 F9),生成好 .exe 后,最后单独运行 duipai.cpp 即可。
g++ my.cpp -o my.exe -O2 -std=c++14
g++ std.cpp -o std.exe -O2 -std=c++14
g++ gen.cpp -o gen.exe -O2 -std=c++14
g++ duipai.cpp -o duipai.exe -O2 -std=c++14
每条命令执行后,如果没看到报错,说明编译成功。编译完成后,直接运行:
duipai.exe
正常运行效果:
Case #1 AC (0 ms)
Case #2 AC (0 ms)
Case #3 AC (0 ms)
...
========== WA at test #27 ==========
Input saved in: data.in
Your output: my.out
Correct output: std.out
此时可以打开 data.in 查看触发 bug 的输入数据。
对拍停在了第 27 组(举例),data.in 里是一组 n=18 的随机数据,肉眼很难直接看出 bug。这时不要试图直接调试大数据,而要先把数据缩小。
具体操作:
① 修改 gen.cpp,把 rand() % 20 + 1 改成 rand() % 5 + 1,重新编译 gen.exe
g++ gen.cpp -o gen.exe -O2 -std=c++14
duipai.exe
② 此时对拍会在很小的 n 下触发错误,data.in 里的数据可以直接手算:
3 <- n=3,手算完全可行
-5 -2 -10 <- 最大值应该是 -2,但 my 会输出 0
③ 找到 bug:ans 初始化为 0 在全负数时不成立。修复:
int ans = -2e9; // 或者:int ans = a[0]; (先读第一个元素)
④ 修复后,把 gen.cpp 的 n 改回正常范围和边界值,再跑 10000 组确认无误。
第3章的 gen.cpp 用了 srand(time(0)) 和 rand(),这在简单场景下够用,但有两个严重问题:
| 问题 | 详情 |
|---|---|
| 范围太小 | Windows 下 RAND_MAX = 32767,无法生成 >32767 的随机数 |
| 种子重复 | srand(time(0)) 以秒为单位,1 秒内多次运行生成相同数据 |
| 质量差 | 线性同余生成器,随机性不够好 |
C++11 引入的 std::mt19937(梅森旋转算法)质量远优于 rand():
#include <bits/stdc++.h>
using namespace std;
// 用硬件随机源初始化(Linux 下效果更好)
mt19937 rng(random_device{}());
// 生成 [a, b] 范围内的随机整数
int rand_int(int a, int b) {
return uniform_int_distribution<int>(a, b)(rng);
}
int main() {
int n = rand_int(1, 100000);
cout << n << endl;
for (int i = 0; i < n; i++) {
cout << rand_int(-1000000000, 1000000000);
if (i < n - 1) cout << " ";
}
cout << endl;
return 0;
}
| 误区 | 正确做法 |
|---|---|
rng() % n(直接取模) | 用 uniform_int_distribution,避免模偏差 |
在循环内定义 mt19937 rng(...) | rng 应定义为全局变量或只在 main 开头初始化一次 |
用 time(0) 做种子(秒级) | 用 random_device{}() 或 chrono::steady_clock |
好的数据生成器是对拍成功的一半。下面给出常见题型的数据生成模板。
int n = rand_int(1, 100000);
cout << n << endl;
for (int i = 0; i < n; i++) {
cout << rand_int(1, 1000000000);
if (i < n - 1) cout << " ";
}
cout << endl;
int n = rand_int(1, 100000);
cout << n << endl;
vector<int> p(n);
iota(p.begin(), p.end(), 1);
shuffle(p.begin(), p.end(), rng);
for (int i = 0; i < n; i++) {
cout << p[i] << " \n"[i == n - 1];
}
int n = rand_int(2, 100000);
cout << n << endl;
// 点 i 的父亲从 [1, i-1] 随机选,保证连通且无环
for (int i = 2; i <= n; i++) {
cout << rand_int(1, i - 1) << " " << i << "\n";
}
int n = rand_int(2, 1000);
// m 在 [n-1, n*(n-1)/2] 之间,保证连通
int m = rand_int(n - 1, min(n * (n - 1) / 2, 2000));
cout << n << " " << m << "\n";
// 先生成树(保证连通)
for (int i = 2; i <= n; i++) {
cout << rand_int(1, i - 1) << " " << i << "\n";
}
// 补上剩余边
for (int i = n; i <= m; i++) {
cout << rand_int(1, n) << " " << rand_int(1, n) << "\n";
}
int len = rand_int(1, 100000);
string s;
for (int i = 0; i < len; i++) {
s += char('a' + rand_int(0, 25));
}
cout << s << endl;
对拍不是跑一次就完了。高效的测试需要分层进行,从易到难,全面覆盖。
数据规模控制在暴力可承受范围内(如 n≤20),大量循环测试。快速发现逻辑 bug。
✅ 必做
针对特殊边界:全 0、全同、升序、降序、n=1、极端大值、图论中的菊花图/链等。
✅ 必做
用正解本身跑大数据(无暴力对比),检查 TLE/MLE/RE。观察运行时间是否在预期内。
⚠️ 对拍的补充
编写多个 gen.cpp,分别侧重不同边界(如一个专出随机数据,一个专出有序数据)。
💡 进阶
很多新手在对拍 AC 后产生"万事大吉"的错觉。以下是对拍无法有效覆盖的场景:
| 局限 | 说明 | 应对方法 |
|---|---|---|
| MLE(内存超限) | 对拍只比较输出,不检测内存使用 | 用大数据手动压测,观察任务管理器/代码内存 |
| 极端大数据 bug | 暴力程序跑不了大数据,无法对比 | 手工构造最大数据验证正解 |
| 随机数据未触发的边界 bug | 纯随机可能漏掉特定边界 | 手工构造边界数据 + 多个生成器 |
| 时间复杂度分析 | 对拍 AC 不代表算法复杂度正确 | 人工分析复杂度,确保正解能在时限内通过 |
| 浮点精度问题 | 微小误差可能导致 WA 但随机数据未触发 | SPJ 中用 eps 容差;手工测边界浮点值 |
原因:数据规模不够大未触发 bug;std 和 my 有相同 bug;gen 未覆盖特定边界。
解决:增大测试量;用多个不同思路的 std 交叉验证;手工构造边界数据。
解决:减小 gen 的数据规模;使用更优的暴力(如 O(n²) 而非 O(n³));限制对拍轮数。
解决:编写 SPJ(见附录D),用 fabs(a-b) < eps 判断;或输出时控制精度(如 printf("%.6f"))。
解决:在 gen 中控制数据范围,避免触发 RE;在 duipai.cpp 中加入超时检测。
推荐:不用。用 shell 重定向(< data.in > my.out),代码中无需 freopen,避免提交时忘记删除导致 CE。
原因:srand(time(0)) 以秒为单位,编译运行太快导致种子相同。
解决:改用 mt19937 + random_device(见第4章)。
duipai/
├── my.cpp ← 待测程序
├── std.cpp ← 暴力程序(保证正确)
├── gen.cpp ← 数据生成器
└── duipai.cpp ← 对拍总控程序(本文件)
g++ my.cpp -o my.exe -O2 -std=c++14
g++ std.cpp -o std.exe -O2 -std=c++14
g++ gen.cpp -o gen.exe -O2 -std=c++14
g++ duipai.cpp -o duipai.exe -O2 -std=c++14
duipai.exe
#include <bits/stdc++.h>
using namespace std;
mt19937 rng(random_device{}());
int rand_int(int a, int b) {
return uniform_int_distribution<int>(a, b)(rng);
}
int main() {
// 根据题目格式生成数据,n 范围控制在暴力可接受范围内
int n = rand_int(1, 20);
cout << n << endl;
for (int i = 0; i < n; i++) {
cout << rand_int(1, 1000000000) << " \n"[i == n - 1];
}
return 0;
}
── 主线内容结束,以下为进阶附录 ──
如果你更习惯用批处理脚本而非 C++ 程序来对拍,可以用 .bat 文件。核心命令是 fc(文件比较)。
@echo off
:loop
gen.exe > data.in
std.exe < data.in > std.out
my.exe < data.in > my.out
fc std.out my.out > nul
if errorlevel 1 pause
goto loop
@echo off
echo Compiling...
g++ gen.cpp -o gen.exe -O2 -std=c++14
g++ std.cpp -o std.exe -O2 -std=c++14
g++ my.cpp -o my.exe -O2 -std=c++14
if errorlevel 1 (echo Compile failed & pause & exit)
set /a cnt=0
:loop
set /a cnt+=1
echo Case %cnt%...
gen.exe > data.in
std.exe < data.in > std.out
my.exe < data.in > my.out
fc std.out my.out > nul
if errorlevel 1 (
echo ===== WA at Case %cnt% =====
pause & exit
) else (
echo Case %cnt% AC
)
goto loop
| 要点 | 说明 |
|---|---|
fc 返回值 | 0 = 文件相同,非 0 = 不同(与 diff 相反!) |
if errorlevel 1 | 返回值 ≥1 时触发(即文件不同) |
> nul | 隐藏 fc 的详细输出,只保留返回值 |
Linux 或 macOS 下使用 .sh 脚本,核心命令是 diff。
#!/bin/bash
# 编译(每次运行前重新编译,确保代码最新)
g++ gen.cpp -o gen -O2 -std=c++14
g++ std.cpp -o std -O2 -std=c++14
g++ my.cpp -o my -O2 -std=c++14
cnt=0
while true; do
cnt=$((cnt + 1))
echo "Case $cnt..."
./gen > data.in
./std < data.in > std.out
./my < data.in > my.out
if ! diff -q std.out my.out > /dev/null; then
echo "===== WA at Case $cnt ====="
echo "Input: data.in"
exit 0
else
echo "Case $cnt AC"
fi
done
使用方法:
chmod +x duipai.sh # 首次运行前添加执行权限
./duipai.sh # 运行
| 要点 | 说明 |
|---|---|
diff 返回值 | 0 = 文件相同,非 0 = 不同(与 fc 相反!) |
diff -q | 只报告是否相同,不输出差异内容 |
/dev/null | 丢弃输出,只保留返回值 |
Python 版本跨平台性最好,无需区分 Windows/Linux 的命令差异。
import os, sys
cnt = 0
while True:
cnt += 1
print(f"Case {cnt}...", end=" ")
# 生成数据
os.system("gen.exe > data.in")
# 运行两个程序
os.system("std.exe < data.in > std.out")
os.system("my.exe < data.in > my.out")
# 比较输出(忽略空白字符差异)
with open("std.out") as f: s1 = f.read().split()
with open("my.out") as f: s2 = f.read().split()
if s1 != s2:
print(f"\n===== WA at Case {cnt} =====")
print("Input: data.in")
sys.exit(0)
else:
print("AC")
上面的 Python 版本用 split() 读取文件,自动忽略多余空格和换行,比直接用 fc/diff 更宽容。
当题目答案不唯一时(如输出任意一组合法方案、输出精度范围内的值),直接比对输出文件无法判定正确性,需要编写 SPJ(Special Judge)。
SPJ 是一个独立程序,它读取「输入文件 + 选手输出文件」,判断选手输出是否合法且正确。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char* argv[]) {
// argv[1] = 输入文件, argv[2] = 标准输出, argv[3] = 选手输出
ifstream fin(argv[1]), fstd(argv[2]), fmy(argv[3]);
int n; fin >> n;
vector<int> a(n);
for (int& x : a) fin >> x;
// 读取选手输出,验证合法性
int l, r;
fmy >> l >> r;
if (l < 1 || r > n || l > r) return 1; // 不合法
long long sum = 0;
for (int i = l - 1; i < r; i++) sum += a[i];
// 与标准答案比较
long long std_ans;
fstd >> std_ans;
if (sum != std_ans) return 1;
return 0; // 正确
}
把 duipai.cpp 中的 system("fc ...") 替换为调用 SPJ:
// duipai.cpp 中替换比较部分
system("spj.exe data.in std.out my.out");
// 检查 SPJ 的返回值:0 = AC,非 0 = WA
if (system("spj.exe data.in std.out my.out") != 0) {
printf("WA at test #%d\n", T);
return 0;
}
Testlib 是行业标准,被 Codeforces、洛谷等平台采用:
#include "testlib.h"
int main(int argc, char* argv[]) {
registerTestlibCmd(argc, argv);
double pans = ouf.readDouble();
double jans = ans.readDouble();
if (fabs(pans - jans) < 1e-6)
quitf(_ok, "Accepted\n");
else
quitf(_wa, "Expected %.6f, found %.6f\n", jans, pans);
}
交互题的对拍比传统题复杂,因为程序之间存在"对话"。主要分为两种实现方式。
对于下发 grader.cpp 的题,本地调试方法:
g++ my_solution.cpp grader.cpp -o test -O2 -std=c++14
./test
自对拍技巧:复制一份 grader.cpp,把交互函数实现放在文件末尾,选手代码放在文件开头,这样可以直接编译调试。提交时注意改回原始格式。
需要用管道(pipe)连接选手程序和交互器:
mkfifo pipe # 创建命名管道
./interactor < pipe | ./my_solution > pipe # 连接
更简单的方式是将交互器逻辑内置到 C++ 程序中,用文件 I/O 模拟交互过程。
最权威的出题辅助库(仅 testlib.h 一个头文件)。用于编写 Generator、Validator、Interactor、Checker。与 Codeforces Polygon 完全兼容。
GitHub: MikeMirzayanov/testlib
洛谷开发的 Python 库,内置随机图、树、字符串等数据结构生成器,可自动调用标程生成 .out 文件。
安装: pip install cyaron
基于 C++ 的出题工具,提供 caryon.h 头文件,内置 cyrand()、inint() 等函数简化数据生成。
Codeforces 开发的在线出题平台,集成 Testlib 全部功能,支持可视化编辑测试数据。
网址: polygon.codeforces.com