【C语言】对拍【保姆级教程】

对拍是一种验证代码正确性的方法,通过编写两份代码,一份是待验证的代码,另一份是已知正确但效率不高的暴力解,以及生成随机数据的代码。通过对随机数据的处理,比较两份代码的输出结果来判断待验证代码的正确性。这种方法常用于比赛和日常编程中找出代码错误。文章通过三个实例详细介绍了对拍的使用过程和技术要点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是对拍?

对拍是写完代码后,通过再另写一篇代码来判断自己的代码是否正确,是否能够实现预期功能。

一般是写完一篇代码后,再另写一篇绝对正确的代码(但在时间空间复杂度上不一定是最优的),以及一篇随机生成数据的代码,然后将随机生成的数据给到绝对正确的代码以及需要验证的代码。如果两个代码跑出来的输出数据一致,那么就能够在一定程度上说明这个需要验证代码是正确的,而如果跑出来的结果,不一样,那就说明这个需要验证的代码是错误的或者说是有漏洞的。接下来我们的任务就是去debug了,有需要的可以回顾我前面写的有关“调试”的文章。

同时,也因此,我们必须保证我们写的用来“对拍”的代码是绝对正确的,如果这个代码写的都是错误的话,那后面自然也就没有什么好说的了,一般情况下我们是写一篇暴力的代码,也就是不考虑时间复杂度与空间复杂度,只要在小范围数据内能跑对就行。

【C语言】调试【保姆级教程】 visual stdio 2022 + Dev-c++ + vscode三种编译器的调试方法_Autumn_goose的博客-CSDN博客

为什么要去对拍?

对拍一般有两种用途:

一、在一定程度上验证自己写的代码的正确性。一般是用在考场或者oi赛制的比赛上,也就是只能提交一份代码的时候,那么这个时候保证自己提交的代码的正确性就尤为重要了。

二、通过找出自己代码的错误数据来寻找代码中的错误。这种常用于平时的做题中,我们做题写了一篇代码,结果提交上去却是错的,然后又一下子不知道错在哪里。这个时候我们就可以去查看题解,然后用题解的代码进行对拍,找到那组(或者好多组)让你代码过不了的测试数据,然后针对测试数据的情况来有针对性的分析或快速调试自己的代码究竟错在哪里。

如何去对拍?

我们将从下面的三个样例来讲解如何进行对拍。

一、求和公式的对拍。

我现在想要时间复杂度\(O(1)\)的求解\(\sum_{i = 1}^{n}i\),但是我只会用一个暴力的for循环时间复杂度\(O(n)\)的去计算,然后我脑子灵光一闪,想起了以前还给数学老师的知识,依稀记得求和公式好像是这样\(\sum_{i = 1}^{n}i = \frac{n \times (n +1)}{2}\),但是我不太肯定,于是决定写一个“对拍”程序试一试。

1、于是我们先写下求和公式的代码,并且命名为 test.cpp ,表示这个是需要验证的代码。

#include<bits/stdc++.h>
using namespace std;

int main() {
    int n; cin >> n;
    cout << n * (n + 1) / 2 << endl;
    return 0;
}

2、接下来再写下暴力循环的代码,并且命名为 bf.cpp ,bf是 brute force 也就是“暴力”的英文,表示这份代码是用暴力求解答案的,并且要保证这篇代码一定是要正确的。

#include<bits/stdc++.h>
using namespace std;

int main() {
    int n; cin >> n;
    int sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    cout << sum << endl;
    return 0;
}

3、接下来再写一篇生成随机数据的代码,并且命名为 data.cpp ,表示是用来生成数据的代码。

关于这篇生成随机数据的代码,将会在文章的最后一部分进行大致的讲解。

#include<bits/stdc++.h>
using namespace std;

int main() {
	srand(time(0) + (unsigned long long)(new char));
	
    int n = rand() % 100 + 1;
    cout << n << endl;
	
	return 0;
}

4、然后写如下的脚本,并且命名为 run.txt ,这个脚本是用来运行并且比较暴力代码和需要验证代码的。写完后再将其后缀名给改成 run.bat 了。然后关于这个脚本的讲解也将放在本篇文章的最后一部分。

@echo off

g++ bf.cpp -o bf.exe
g++ test.cpp -o test.exe
g++ data.cpp -o data.exe

:loop
data.exe > data.txt
test.exe < data.txt > test.txt
bf.exe < data.txt > bf.txt

fc test.txt bf.txt

if not errorlevel 1 goto loop

pause

 

 5、这里说明三个点。

一、个是上面所写的三篇代码和一个脚本都是要放在同一个文件夹下面的。

二、不用管我上面截图中的后缀名为“.bin””的文件以及后缀名为“.cph”的文件夹,那个是我的 vscode 的插件弄出来的,如果大家没有下载或使用这个插件的话,是不会出现这些文件的。同时这些文件的有无不会影响“对拍”的正常使用。

三、在运行脚本之前要把上面三篇代码全部先提前编译运行一遍,也就是说你的文件夹中要有三篇代码对应的“.exe”文件。

 6、然后我们在文件夹中运行“run.bat”这个脚本,在 vscode 中如果安装了 code runner 这个插件的话,可以直接点击右上角的一个按钮来运行脚本,如果没有安装这个插件(比如雁雁我)或者是使用其他编译器如 dev-c 或 visual stdio 的话,那么我们就直接在文件夹中运行。

 7、运行了之后,文件夹中会多出来几个“.txt”文件,也就是分别对应的我们的随机生成的输入数据,暴力代码的输出数据以及我们需要验证的代码的输出数据。然后脚本运行出来的效果就会是下面这样子的,在遇到两份代码输出不同之前会一直跑。

 8.然后我们给代码加点错误的东西,看看如果对拍出错误的东西的话会是一种什么样的情况。我们给test.cpp里面的代码加点料。然后再运行看看效果。

#include<bits/stdc++.h>
using namespace std;

int main() {
    int n; cin >> n;
    if (n <= 10) cout << "qiuyan666";
    cout << n * (n + 1) / 2 << endl;
    return 0;
}

 9、这个时候我们就可以很直观的看到,两份代码中的差异了,正确的输出和实际输出之间的差别,然后我们再在文件夹中打开data.txt这个,我们就能够看到输入数据是什么。


二、高精度加法\((a+ b)\)的对拍。

原题指路:791. 高精度加法 - AcWing题库

这里介绍另一种对拍方式,其实也差不多,就是把“run.bat”这个脚本写成代码“run.cpp”来运行。

1、首先是我们自己写高精度代码,这里用的是y总的vector的板子。

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5;

vector<int> add (vector<int> &A, vector<int> &B) {
    vector<int> C;
    if(A.size() < B.size() ) return add(B, A);

    int t = 0;
    for (int i = 0; i < A.size(); i++) {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }

    if (t) C.push_back(1);
    return C;
}

void solve() {
    string a, b; cin >> a >> b;
    vector<int> A, B;
    for (int i = a.size() - 1; i >= 0; i--) {
        A.push_back(a[i] - '0');
    }
    for (int i = b.size() - 1; i >= 0; i--) {
        B.push_back(b[i] - '0');
    }

    auto C = add(A, B);

    for (int i = C.size() - 1; i >= 0; i--) {
        cout << C[i];
    }
    cout << endl;
}

int main() {
    solve();
    return 0;
}

2、然后是暴力的代码:

#include<bits/stdc++.h>
using namespace std;

int main() {
    int a, b;
    cin >> a >> b;
    int c = a + b;
    cout << c << endl;
    return 0;
}

3、然后是生成数据的代码:

#include<bits/stdc++.h>
using namespace std;

int main() {
    srand(time(0) + (unsigned long long)(new char));
	
    int a = rand() % 100 + 1;
    int b = rand() % 100 + 1;
    cout << a << " " << b << endl;
    return 0;
}

4、最后是运行比较的代码:

#include<bits/stdc++.h>
using namespace std;

void solve() {
    while(1) {
        system("data.exe > data.txt");
        system("bf.exe < data.txt > bf.txt");
        system("test.exe < data.txt > test.txt");
        if (system("fc test.txt bf.txt")) {
            cout << "wrong!" << endl;
            break;
        }
    }
    system("pause");
}

int main() {
    solve();
    return 0;
}

5、最后的运行结果:

6、加了点料之后的运行结果:

 这个可能就对用vscode的人来说友好很多,不用去打开文件夹去运行“run.bat”脚本了,所有的操作都可以在vscode编译器里面的界面中操作。其实对于其他编译器整体来说都会方便很多,因为不用把“run”文件的后缀名改来改去。


三、dijkstra算法(最短路)的对拍。

原题指路:https://www.acwing.com/problem/content/851/

这里再介绍另一种对拍方式,和前面的也都差不多。区别就在于,在暴力和需要验证的代码中就已经完成了从随机数据的输入以及从将输出数据输出到对应的“.txt”文件中。

1、还是先给出自己写的正解算法(dijkstra),这里用的仍然是y总大大的板子。

#include<bits/stdc++.h>
using namespace std;

//朴素dijkstra算法求最短路,适用于,边权都是正的,稠密图,复杂度n^2
//一开始 dis[1]=0, dis[其他]=INF
//首先遍历1~n个点,
//每次确定一个点到起点的最短路
//剩下的还没有确定的点中最小的一个点
//基于贪心,每次都找最短的路
//找到之后,就用这个点去更新其他点的最短路
//然后就一直迭代。
//这样一直迭代n次,就找到每个点的最短路了。

const int N = 520;
int n, m;
int g[N][N]; //邻接矩阵
int dist[N]; //表示从i号点走到起点的当前的最短路的距离是多少
bool st[N]; //表示每个点的最短路的距离是不是已经确定了

int dijkstra() {
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;

    for (int i = 0; i < n; i++) {
        int t = - 1;
        for (int j = 1; j <= n; j++) { //在所有最短距离还没有确定的点中找到最短距离的点(所有st[]=false的点中的最短的那个)
            if (!st[j] && (t == -1 ||  dist[t] > dist[j])) { //如果当前这个点没有遍历过,或者是这是第一个遍历或者是这个点的最短路更近。
                t = j;
            }
        }

        st[t] = true;

        for (int j = 1; j <= n; j++) {
            dist[j] = min(dist[j], dist[t] + g[t][j]); //用这个最短路的点去更新其他的点
        }

    }

    if (dist[n] == 0x3f3f3f3f) return -1; //假设不连通的话,就返回-1
    return dist[n]; //不然的话,就返回最短路的距离
}

void solve() {
    cin >> n >> m;

    memset(g, 0x3f, sizeof(g)); //初始化邻接矩阵

    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = min(g[a][b], c); //处理重边,只保留长度最短的那条边
    }

    int t = dijkstra();
    cout << t << endl;
}

int main() {
    solve();
    return 0;
}

2、然后再写出暴力算法(Floyd)。

#include<bits/stdc++.h>
using namespace std;

//处理多源汇最短路,时间复杂度o(n^3)
//直接就是dp思想然后暴力就完事了

const int INF = 1e7;
const int N = 520;
int n, m;
int d[N][N];

void floyd() {
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
            }
        }
    }
}

void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;
        }
    }

    while (m--) {
        int a, b, w;
        cin >> a >> b >> w;
        d[a][b] = min(d[a][b], w);
    }

    floyd();

    if (d[1][n] >= INF) cout << -1 << endl;
    else cout << d[1][n] << endl;
}

int main() {
    solve();
    return 0;
}

3、接着是写生成随机数据的代码:

#include<bits/stdc++.h>
using namespace std;

void solve() {
    srand(time(0) + (unsigned long long)(new char));
    int n = rand() % 500 + 1;
    int m = ( rand() + rand() + rand() +rand() ) % 100000 + 1;
    cout << n << " " << m << endl;
    for (int i = 1; i <= m; i++) {
        int a = rand() % n + 1;
        int b = rand() % n + 1;
        int w = rand() % 10000 + 1;
        cout << a << " " << b << " " << w << endl;
    }
}

int main() {
    freopen("data.in", "w", stdout);
    solve();
    return 0;
}

4、写一个运行比较的脚本:

@echo off

:loop
data.exe
test.exe
bf.exe

fc test.out bf.out

if not errorlevel 1 goto loop

pause
:end

5、然后在文件夹中运行脚本,最后面对拍出来的结果如下:

 6、加了料以后的运行情况:


一些说明(叮嘱)

①上面的三个案例其实是对应“对拍”中三种比较常见和经典的应用场景的。第一种是验证我们的某个猜想是否正确,比如一些需要推公式或者构造或者模拟的题目,我们猜或者推了一个表达式出来,然后再小范围数据内进行大暴力或者大模拟去对拍表达式。第二种是常见于写高精度算法。第三种是写完一些比较长,容易错的代码后,再写一个暴力而简单的代码去检查有没有错误。

②暴力(bf.cpp)代码一定要完全正确,可以很冗长或者很简短,可以很暴力,可以不剪枝,不需要考虑时间复杂度空间复杂度。唯一要求就是绝对正确。

③对拍出错误后,可以去data.txt中查看这一组让你代码错误的数据是什么,然后针对这一组数据可以有针对性的分析,自己是不是边缘数据,特殊数据,数组越界,特判,少考虑情况等等什么地方错了。然后查看暴力出来的正确答案的输出在bf.txt中,你的代码跑出来的结果在test.txt中。

④所有对拍的文件都要存放在一个文件夹下!!!

⑤三份代码 bf.cpp,test.cpp,data.cpp 都要先运行,或者说在同一个文件夹下要生成对应的 bf.exe,test.exe,data.exe 文件,之后,再去运行脚本 run.bat。不然的话是没有结果的。(第一种写法的话可以不用,直接保存就好,因为在脚本中写了运行.cpp的代码,也就是 g++ bf.cpp -o bf.exe 以及接下来的两行代码)。

⑥三份txt文件 data.txt,bf.txt,test.txt是不需要我们提前在文件夹中创建的,我们运行完代码(或脚本)之后,会自动在文件夹中创建这三份txt文件,同时将数据放进这三份文件中。

⑦比如对拍发现了错误,我们给代码进行了修改之后,要重新运行一遍修改完后的代码,不能只是保存,不然的话脚本运行对拍的还是原来的代码。

⑧对拍不出错误并不能保证你的代码一定是完全正确的,可能大数据(超出暴力代码的数据范围)的时候,你的代码就会出现问题,也可能会出现TLE,MLE等其他情况。

⑨用来写脚本的语言是 Bash。


有关脚本的介绍

我们以第一个版本的脚本进行介绍。

@echo off

g++ bf.cpp -o bf.exe
g++ test.cpp -o test.exe
g++ data.cpp -o data.exe

:loop
data.exe > data.txt
test.exe < data.txt > test.txt
bf.exe < data.txt > bf.txt

fc test.txt bf.txt

if not errorlevel 1 goto loop

pause

一、@echo off 的作用是关闭回显,也就是说,如果在脚本中加入了这一行代码的话,那么运行脚本的时候就不会把每一步操作都显示出来,对于只查看结果的我们来说更加简洁。

下面是删除这一句代码后运行的结果

二、g++ bf.cpp -o bf.exe表示的就是运行一下bf.cpp这个文件。

三、test.exe < data.txt > test.txt 的意思是,将data.txt中的内容输入到test.exe中并运行,然后将test.exe的输出结果返回到test.txt中。

四、fc text.txt bf.txt 表示将 text.txt 和 bf.txt 中的内容进行对比,看看是否一致。

五、loop 是循环,if not errorlevel 1 goto loop 是说,如果没有出现错误(即text.txt和bf.txt中的内容一致)的话,那么就继续循环。


进阶脚本

从上面的解释中其实我们是可以发现的,就是如果对拍不出错误的话,那么这个循环就会一直跑下去,一直循环。那么我们可不可以设定一个循环次数呢。比如只跑一百次就差不多得了,没必要一直跑下去。

那我们就可以把脚本写成这样:

@echo off

g++ bf.cpp -o bf.exe
g++ test.cpp -o test.exe
g++ data.cpp -o data.exe

set /a i = 0
set /a T = 100

:loop
data.exe > data.txt
test.exe < data.txt > test.txt
bf.exe < data.txt > bf.txt

fc test.txt bf.txt

echo running on test %i%
set /a i += 1
if %i% == %T% goto end

if not errorlevel 1 goto loop

:end
pause

set /a T = 100 这一行代码大家可以自行设定循环次数,比如我上面设定的就是一百次。

然后下面给出没跑出问题和跑出问题了的两种情况。


有关生成随机数据的介绍

    srand(time(0) + (unsigned long long)(new char));

这一句话是用来生成随机数的。如果我们不加这句话,直接用下面的代码的话

#include<bits/stdc++.h>
using namespace std;

int main() {
    int n = rand();
    cout << n << endl;
}

我们每一次运行出来的结果都会是41,这里面就涉及到伪随机数的知识了,大家可以去自行搜索相关资料,我就不再此赘述,大家只要记住加了上面的那一行代码后,出来的随机数会比较均匀。(当然还有别的高级方法出来的随机数会更加均匀,我这篇文章的终点在于介绍如何使用对拍,重点不在生成伪随机数上面,所以就不在这边写了)

另外,rand() 的范围是 0 ~ 32767 之间,取模运算的意思是生成一个某个范围内的数。比如 a = rand() % 100 的意思就是生成一个 0 ~ 99 之间的数。如果我们要生成 1 ~100 之间的数,那我们就在后面加一个1, a = rand() % 100 + 1。然后如果我们要随机生成的数字会超出范围,比如 50000,那我们就生成两个随机数,然后再取模,比如 a = ( rand() + rand() ) % 50000 + 1。


有关重定向输入输出的介绍

    freopen("data.in", "r", stdin);
    freopen("bf.out", "w", stdout);

freopen("文件名", "r", stdin);表示从这个文件中读取输入。

freopen("文件名", "r", stdout);表示将输出放到这个文件中。


$$\color{blue}{绝}\color{yellow}{域}\color{red}{殊}\color{purple}{方}\color{green}{画}\color{gold}{秋}\color{cyan}{雁}\color{lavender}{,}\color{magenta}{笑}\color{orangered}{看}\color{springgreen}{春}\color{gray}{风}\color{aquamarine}{阅}\color{violet}{千}\color{greenyellow}{帆}\color{thistle}{。}$$

如果大家文章中存在疑问或者解释不清楚额地方,可以在下方评论区中提问。如果我的文章中出现了什么错误,也烦请各位大佬指出,我马上修改。谢谢各位!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值