当前位置: 首页 > news >正文

【详细教程】对拍 0 基础学习小课堂 [内附例题演示]

以下文章专门面对 c++ & Windos 选手,使用其他语言或其他环境请另寻高明。

笔者是万能头文件党,非特殊情况文章里不会介绍头文件使用(默认万能头)。

以下使用函数默认为 std 库标准函数,即直接使用不加 std:: 的前缀。


有相当一部分题目的样例十分寒碜,有时样例全对但交上去不到 50 分。

这时候我们需要自己造数据,但一组组数据手动输入等输出肯定是不现实的。

解决这种问题需要用到对拍。


0.简述

对拍,是一种进行检验或调试的方法,通过对比两个程序的输出来检验程序的正确性。

可以将自己程序的输出与其他程序的输出进行对比,从而判断自己的程序是否正确。

一般我们对拍,需要四个程序:

(1)待测 / 出现错误的程序:sol.cpp  (solution)

         这是准备交上去的程序,已经尽量保证速度,但还未测验正确性

(2)暴力 / 标准的程序:bru.cpp (brute)

         这是你找的标程或是自己打的暴力,速度可能不尽人意,但输出保证绝对正确

(3)数据生成器:gen.cpp (generator)

         这是你自己打的数据生成器,通常用随机数函数生成。

         保证这个程序输出的都是符合题意,且完美覆盖到所有边界情况的数据。

(4)对拍脚本:st.cpp (stress test)

         这是一个自动循环程序,将执行以下操作:

  1. 调用 gen.cpp 生成输入
  2. 将输入分别喂给 sol.cpp 和 bru.cpp
  3. 比较两个程序的输出是否一致
  4. 若不一致,保存当前输入并终止,便于调试

1.高质量随机数生成

rand 家族

首先我们把目光放在随机数生成函数上,相信大家都听过 rand() 函数。

这个函数在 Linux 环境下的随机数生成范围是 [0,2^{31} -1]

而在 Windos 环境下却只有 [0,32767],这太小了,完全不够我们日常使用

不但如此,rand() 即使有好种子作为生成基础,但分布和周期仍不佳,大量使用环境还是不推荐

srand(time(0));           // 用当前时间作为种子
int x = rand() % 100;     // 生成 [0, 99] 的随机整数

(如上为用每秒都变化(不易重复)的时间函数作为种子,种子可以理解为每次玩 mc 输入的那个数字,凭借这个种子生成地图(随机数)。一样的种子生成的东西就一样)

如果你做过随机乱搞题,你应该对 random_shuffle() 也不陌生。

它用于随机打乱一个数组,同样以 rand() 为基底,现已被时代淘汰

如今我们要达到同样的效果,会使用 shuffle 函数配上高质量随机引擎(见下文代码)。

高级货 MT 19937

这个高质量随机引擎名为梅森旋转算法(Mersenne Twister)

是一种广泛使用的伪随机数生成器,周期为 2^{19937}-1,是一个梅森素数,因此得名。

具体使用如下:

#include<bits/stdc++.h>
using namespace std;#define uid uniform_int_distribution<int>
// 这是一个 "一致分布" 映射函数,后面经常跟着一段区间
// 用于把随机数生成器生成出大的数映射到这段区间里
// 大的数取模会有某个数字出现频率更高的情况,而 uid 能保证所有数分布概率一样 // 当然,你如果要 long long 的直接改类型就好,就像这样
// #define uid uniform_int_distribution<long long>
// 这样后面跟着的区间就可以到 long long 的范围
// 不过,区间常数也要是 long long 类型
// 比如这样:dist(1, 1000000000000LL)int main() {// 我个人习惯关同步流,不关也行,关了快一点 ios::sync_with_stdio(false);cin.tie(0); // 创建一个 mt19937 类型的随机数引擎 rngmt19937 rng(chrono::steady_clock::now().time_since_epoch().count());// 这一大长串东西是一个时间戳,和 time(0) 差不多,但是纳秒级精细度// time(0) 是以每秒的时间做种子,在快速运行时可能连续几次生成完全相同的数据// 可能无法快速测到一些边界情况/* chrono::steady_clock::now()   获取当前时间点(time_point).time_since_epoch()   从时间起点到当前时间的时长.count()            将该时长转换为一个整数(通常是纳秒或微秒数)*/uid dist(1, 100);    // 生成 [1, 100] 的随机整数(严格闭区间) cout << dist(rng) << "\n";// 以后想生成 [1, 100] 的随机整数,直接调用 dist(rng) 就好// 还需要更多范围的数 uid dist1(1, 10000);      // [1, 10000] uid dist2(1, 1000000);    // [1, 1000000]cout << dist1(rng) << "\n";   // 直接调用 cout << dist2(rng) << "\n";vector<int> v = {1, 2, 3, 4, 5};shuffle(v.begin(), v.end(), rng);   // 用 rng 打乱 vectorfor (int x: v) {cout << x << " ";} cout << "\n";return 0;
}

看起来多,真正用的时候,只需要记得这四句:

#define uid uniform_int_distribution<int>
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
uid dist(1, 100);  
cout << dist(rng) << "\n";

多打几遍,直到完全熟练。

例题实战

接下来尝试实战下,以这道题为例:P1141 01迷宫 - 洛谷

大家可以自己先试试,然后用文件输出看看生成的怎么样。

这里提供标程:

// gen.cpp
// 生成 P1141 01迷宫 的合法测试数据
// Windows 下可用 g++ 或 MinGW 编译运行#include <bits/stdc++.h>
using namespace std;#define uid uniform_int_distribution<int>int main() {ios::sync_with_stdio(false);cin.tie(0);mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());uid n_dist(1, 1000);   // n 范围 [1, 1000]uid m_dist(1, 100000);  // m 范围 [1, 100000]// ****真正使用时可以适当缩小数据范围,防止暴力程序美美卡住 ***int n = n_dist(rng);int m = m_dist(rng);cout << n << " " << m << "\n";// 生成 n x n 的 01 矩阵for (int i = 1; i <= n; i ++) { for (int j = 1; j <= n; j ++) {cout << (rng() & 1);  // 快速生成 0 或 1}cout << "\n";}// 生成 m 个查询uid x_dist(1, n);uid y_dist(1, n);for (int i = 1; i <= m; i ++) {cout << x_dist(rng) << " " << y_dist(rng) << "\n";}return 0;
}

每道题都有不同的数据生成程序 gen.cpp,不过大部分都是换汤不换药。

有一些特殊的,比如说生成无自环、无重边的二叉树,

就得在 gen 里面加上一些小智慧或者算法(如 vector 记录可作为父亲的点,再随机选取)。

这些我先暂时不讲,后面有时间补上。

2.对拍脚本

这是很关键的一步,要保证你的程序能自个儿循环 or 找错,对拍脚本是不能少的。

你需要先将四个程序(待测,暴力,数据生成,对拍脚本)全部放在同一个文件夹里。

然后运行待测和暴力程序以及数据生成器,保存三个 exe 在上文的相同文件夹里。

接下来运行这个程序:

// stress test/*x < y:将 x 作为 y 的输入x > y:将 x 的输出写入 y其中 a 是程序,b 是文本 可以理解为尖尖朝哪边,哪边就是被放入数据的 
*//*system 函数是一个系统调用函数成功了会返回 0,非成功返回非 0 (通常是 1) 
*/
#include<bits/stdc++.h>
using namespace std; int main() {// 这里可不能关同步流!!不然没缓冲看不到文字输出 int tot = 0;    // 计数的 // 循环持续生成测试数据并进行对拍,直到发现错误 while (1) {tot ++;cout << "Generating test case " << tot << "\n";   // 这里用英文,有些环境下中文会乱码 // 1. 调用数据生成器 gen.exe// 将生成的测试数据放到文件 test.in(输入数据) 里 system("gen > test.in");// 2. 运行第一个程序(sol.exe)// 待测 sol 程序从 test.in 读取输入,将输出放到 a.outsystem("sol.exe < test.in > a.out");// 3. 运行第二个程序(bru.exe)// 暴力 bru 程序从 test.in 读取输入,将输出放到 b.outsystem("bru.exe < test.in > b.out");// 4. 比较两个输出文件 a.out 和 b.out 是否一致// fc 是 Windos 自带文件比较函数  // 如果文件完全相同,fc 返回 0// 如果文件不同,fc 返回非 0 值(通常为 1)// system("fc a.out b.out") != 0,说明输出不一致。if (system("fc a.out b.out > nul")) {// fc 会自动输出一些东西,我们这里用不到就把它放到 nul 空文件里cout << "The test case " << tot << " is wrong!" << "\n";// 要把上面这句话放在 pause 上面才能看到 system("pause");   // pause 暂停程序,方便你查看控制台输出的差异信息// 立即退出对拍器// 此时 test.in 中保存的就是导致错误的输入数据// 可以用该数据单独调试 sol 和 bru return 0;}// 如果输出一致,循环继续,生成下一组测试数据if (tot > 10000) {    // 拍了一万组 cout << "A possible solution." << "\n";   // 可能也许大概是正确的? system("pause");return 0;} }
}

这个 Windows 脚本对于所有题都是适用的,只要你保证在同一个文件夹、有 exe、无同名程序

3.待测程序与暴力程序

这个放到后面来讲,是因为比起前面两个,这俩玩意是因题而异

就拿之前那道例题来说:P1141 01迷宫 - 洛谷

虽然聪明的同学们一看就知道是广搜 + 带权并查集,但假设我们还没有那么聪明。

傻傻的把每一个询问点都拿去广搜,也就有了我们的 70 分 TLE 暴力 bru 程序

时间复杂度 O(N^2+MN)

// bru.cpp
#include <bits/stdc++.h>
using namespace std;const int N = 1010;
const int dx[4] = {0, 0, 1, -1};
const int dy[4] = {1, -1, 0, 0};char s[N];
int a[N][N];
bool v[N][N];
int n, m;int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n >> m;for (int i = 1; i <= n; i++) {cin >> (s + 1);for (int j = 1; j <= n; j++) {a[i][j] = s[j] - '0';}}for (int i = 1; i <= m; i++) {int x, y;cin >> x >> y;// 每次查询清空 vismemset(v, 0, sizeof(v));queue<int> Q;Q.push((x - 1) * n + (y - 1)); // 0-indexed 每个点唯一编码v[x][y] = 1;int cnt = 0;while (!Q.empty()) {int head = Q.front(); Q.pop();int x = head / n + 1;   // 转回 1-indexed 行int y = head % n + 1;   // 转回 1-indexed 列cnt ++;for (int d = 0; d < 4; d++) {int xx = x + dx[d];int yy = y + dy[d];if (xx >= 1 && xx <= n && yy >= 1 && yy <= n && !v[xx][yy]) {if (a[x][y] != a[xx][yy]) {v[xx][yy] = 1;Q.push((xx - 1) * n + (yy - 1));}}}}cout << cnt << "\n";}return 0;
}

然后我们再打一个待测程序,故意出错,比如说点转编号时错误。

以下程序通过样例,但交上去 10 分,时间复杂度 O(N^2 + M)

// sol.cpp
#include <bits/stdc++.h>
using namespace std;const int N = 1010;
char s[N];
int a[N][N], mp[N][N];
int n, m;
int siz[N * N], fa[N * N];int findfa(int x) {if (fa[x] == x) return fa[x];return fa[x] = findfa(fa[x]);
}queue<int> Q;
bool v[N * N];
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};void bfs() {while (!Q.empty()) {int head = Q.front(); Q.pop();if (v[head]) continue;v[head] = true;int x = head / n + 1, y = head % n;// 如果 head 本来的点 y = n,那这里转编号就会错 for (int i = 0; i < 4; ++i) {int xx = x + dx[i];int yy = y + dy[i];if (xx < 1 || xx > n || yy < 1 || yy > n) continue;if (a[xx][yy] == a[x][y]) continue;int np = mp[xx][yy];Q.push(np);int tx = findfa(head);int ty = findfa(np);if (tx != ty) {fa[tx] = ty;siz[ty] += siz[tx];}}}
}int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n >> m;for (int i = 1; i <= n; ++i) {cin >> (s + 1);for (int j = 1; j <= n; ++j) {a[i][j] = s[j] - '0';mp[i][j] = (i - 1) * n + j;   // 这里 j 没有 - 1 fa[mp[i][j]] = mp[i][j];siz[mp[i][j]] = 1;}}memset(v, 0, sizeof(v));for (int i = 1; i <= n; ++i) {for (int j = 1; j <= n; ++j) {int id = mp[i][j];if (!v[id]) {Q.push(id);bfs();}}}while (m--) {int x, y;cin >> x >> y;int pfa = findfa(mp[x][y]);cout << siz[pfa] << '\n';}return 0;
}

4.实操

好啦,美美开工。

注意注意!!!这里要先把 gen.cpp 的数据生成范围改小,这样才方便调试!

    uid n_dist(1, 30);   // n 范围 [1, 30]uid m_dist(1, 500);  // m 范围 [1, 500]

不然暴力程序就会一直运行(毕竟时间复杂度高),出不来结果!

还有请事先保证: sol.cppbru.cpp 的输出格式完全一致!

(包括换行符、空格、无多余空行),否则 fc 可能误报错误。

首先先建一个文件夹,为了模仿比赛,这里建在 D 盘。

然后把所有程序都放进去,命好名,除了 st 都在 dev 里运行得到 exe(点运行然后关掉黑框)。

搞好了就长这样:

现在开始运行 st.cpp,会出现这样:

第一组就错了,回到文件夹点开三个文本看看。

很好,找到问题!

修改完后,再次运行出 exe,运行下 st.cpp 看看。

(就是改了这两句)

27            int x = head / n + 1, y = head % n + 1;
57            mp[i][j] = (i - 1) * n + j - 1;

然后你就会看到:

嘛,如果你愿意等并且不怕电脑烧坏的话,就可以拿到 "A possible solution." 成就!。

参考资料

常见技巧 - OI Wiki

Don't use rand(): a guide to random number generators in C++ - Codeforces

后记

对拍是很重要的技能,需要多练习。

但不要每道题都盲目使用对拍,进一步调试的前提是自己仔细检查每一行代码。

这算是干了一件一直想干的事情,希望能帮助到你。

后面会在这里给一些链接,作为对拍例子博客。

http://www.dtcms.com/a/545009.html

相关文章:

  • 在 Windows 系统中安装 Oracle、SQL Server(MSSQL)和 MySQL
  • 企业网站导航代码国外代码开源网站
  • 深圳网站开发公司哪家好平面设计岗位职责
  • mooc网站开发案例ip138域名查询
  • 黑白图片智能上色API技术文档 - 让你的老照片重获新生
  • 【Android】Dalvik 对比 ART
  • 【游戏设计】如何建立个人的游戏创意库
  • 手表电商网站湖南人文科技学院官网教务系统
  • 【软件可维护性测试:构建可持续演进更新的软件系统】
  • 【小白笔记】 while 与 for + break 的比较分析
  • STM32中死机 Crash dump 打印出函数调用关系
  • STM32的GPIOx_IDR 与 GPIOx_ODR
  • Rust 借用检查器(Borrow Checker)的工作原理:编译期内存安全的守护者
  • 仓颉语言核心技术深度解析:面向全场景智能时代的现代编程语言
  • 漳州住房和城乡建设部网站简单的页面
  • 架构论文《论负载均衡的设计与应用》
  • Linux frameworks 音视频架构音频部分
  • 【AI论文】PICABench:我们在实现物理逼真图像编辑的道路上究竟走了多远?
  • 设计模式之抽象工厂模式:最复杂的工厂模式变种
  • 设计模式>原型模式大白话讲解:就像复印机,拿个原件一复印,就得到一模一样的新东西
  • 网站数据库大小石家庄发布最新消息
  • 本地运行Tomcat项目
  • 大模型如何变身金融风控专家
  • 台州网站建设维护网页设计与制作教程杨选辉
  • 动力网站移动端模板网站建设价格
  • Windows 10终止服务支持:企业IT安全迎来重大考验
  • Mac os安装Easyconnect卡在正在验证软件包
  • 手机网站免费模板下载门户网站 销售
  • 学习和掌握RabbitMQ及其与springboot的整合实践(篇二)
  • Flink、Storm、Spark 区别