• C++ 对拍详解


    对拍是什么

    ​ 对拍,是一个比较实用的工具。它能够非常方便地对于两个程序的输出文件进行比较,可以帮助我们实现一些自动化的比较输出结果的问题。

    ​ 众所周知,每一道编程题目,都会有某种正解能拿到满分;当我们想不出正解时,我们往往可以打暴力代码来获取部分分数。

    ​ 但是,当我们觉得有思路写正解,但又担心自己正解写的不对,而恰好,我们又有一个能够暴力骗分的代码。这个时候就可以用到对拍。 暴力骗分代码必须保证正确性,最多只是超时,不能出现答案错误的情况。

    ​ 这样,我们可以造多组数据,让暴力骗分的程序跑一遍,再让我们自己写的正解跑一遍,二者进行多次对比。如果多组数据都显示二者的输出结果一样,那么这个正解大概率没问题。相反地,如果两组数据不同,我们就找到了一组错误数据,方便调试,找到正解哪里出了问题。

    ​ 这便是对拍。其作用也在上文提出。


    对拍的实现

    1.准备基本代码

    ​ 首先,我们要有2份代码,一份是这一道题“你写的正解”代码,另一份是同一道题“你打的暴力”代码。

    ​ 为了方便,我们先用 A+B problem 来演示对拍。

    ​ 自己的代码: std.cpp

    #include<cstdio>
    using namespace std;
    int main()
    {
    	int a,b;
    	scanf("%d%d",&a,&b);
    	printf("%d
    ",a+b);
    	return 0;
    }
    

    ​ 暴力代码:baoli.cpp

    #include<cstdio>
    using namespace std;
    int main()
    {
    	int a,b;
    	scanf("%d%d",&a,&b);
    	int ans=0;
    	int i;
    	for(i=1;i<=a;i++)  ans++;
    	for(i=1;i<=b;i++)  ans++;
    	printf("%d
    ",ans);
    	return 0;
    }
    

    ​ 2份代码有了,我们把它放在同一个文件夹里。这样算是做好了对拍的准备。

    2.制作数据生成器

    ​ 我们制作的数据要求格式和上面两份代码的输入格式一样。
    ​ 根据上面,我们可以知道输入的数据为2个数,中间有空格分隔。那么,我们的数据生成器就要输出2个数,中间也要用空格分隔。

    #include<cstdio>
    #include<cstdlib>
    #include<ctime>
    int main()
    {
    	srand(time(0));
        //这是一个生成随机数随机种子,需要用到 ctime 库
    	printf("%d %d
    ",rand(),rand());
        //这样就生成了2个随机数
    	return 0;
    }
    

    ​ 运行一下,确实生成了2个随机数。

    ​ 注:如果不加那个随机种子,生成的随机数每次都是一样的数。

    Extra:数据范围

    ​ 如果我们对于数据范围有要求,那怎么办呢?

    ​ 要让随机数限定在一个范围,可以采用 “模除加加法” 的方式。

    ​ 对于任意数,(0leq rand()\%(a+1) leq a)

    ​ 于是 (0+kleq rand()\%(a+1) +kleq a+k)

    举几个简单的例子:

    1. a=rand()%2 时,a 的范围:(0 leq a leq 1)

    2. a=rand()%2+1 时,a 的范围:(1 leq a leq 2)

    3. 要想让 (1 leq a leq 30000) ,则 a=rand()%30000+1

    但是,这里有个小问题。Windows 系统下 rand() 生成的随机数的范围在0~32767之间。如果我们想要得到比32767更大的随机数怎么办呢?除了换 Unix 系统外,我还有一个小办法,很实用。

    比如让 (1 leq a leq 1,000,000)

    ll random(ll mod)
    {
        ll n1,n2,n3,n4,ans;
        n1=rand();n2=rand();
        n3=rand();n4=rand();
        ans=n1*n2%mod;
        ans=ans*n3%mod;
        ans=ans*n4%mod;
        return ans;
    }
    
    int main()
    {
        srand((unsigned)time(0));
        ll n;
        while(1)
        {
            n=random(1000000);
            cout<<n<<endl;
        }
        return 0;
    }
    

    看一下输出结果

    这种 “暴力” 的方法是不是感到很神奇呢?

    3.对拍代码

    ①标准输入输出代码

    ​ 标准输入输出指的是:两份基本代码和数据生成代码不含文件输入输出操作,如 freopen 等。

    ​ 在这里,我们需要用到一些文件的读写符号。(需用到 <cstdlib> 库)

    system("A.exe > A.txt") 指的是运行 A.exe,把结果输出(>)到 A.txt 中。

    system("B.exe < A.txt > C.txt") 指的是运行 B.exe,从 A.txt 中读入(<)数据,把结果输出(>)到 C.txt 中。

    system("fc A.txt B.txt") 指的是比较 A.txt 和 B.txt ,如果两个文件里的数据相同返回0,不同返回1。

    ​ 那么,我们就可以执行这一操作来实现对拍。

    1. 先让数据生成器输出数据。 system("data.exe > in.txt")
    2. 然后用这个数据跑一遍暴力代码,输出结果。 system("baoli.exe < in.txt > baoli.txt")
    3. 再用这个数据跑一遍你写的正解代码,输出结果。 system("std.exe < in.txt > std.txt")
    4. 把两个结果相比较,判断是不是一样的。 system("fc std.txt baoli.txt")
    #include<cstdio>
    #include<cstdlib>
    #include<ctime>
    using namespace std;
    int main()
    {
    	while(1) //一直循环,直到找到不一样的数据
    	{
    		system("data.exe > in.txt");
    		system("baoli.exe < in.txt > baoli.txt");
    		system("std.exe < in.txt > std.txt");
    		if(system("fc std.txt baoli.txt")) //当 fc 返回1时,说明这时数据不一样
    			break; //不一样就跳出循环
    	}
    	return 0;
    }
    
    

    ②文件输入输出

    ​ 标准输入输出指的是:两份基本代码和数据生成代码含有文件输入输出操作,如 freopen 等。

    ​ 因为基本代码中有文件输入输出,所以我们在对拍代码中不必使用 ' < ' 、' > ' 等符号对文件进行操作。只需运行一下两个程序,程序会自己输出文件。

    ​ 这种文件输入输出的模式适合各种大型线下比赛使用。优点在于对拍的时候不用删除 freopen 。

    1. 数据生成代码例子:
    #include<bits/stdc++.h>
    int main()
    {
        srand(time(0));
        freopen("in.in","w",stdout); //生成 使两份基本代码 将要读入的数据
        int a=rand(),b=rand();
        printf("%d %d
    ",a,b);
    }
    
    1. 暴力代码例子:
    #include<bits/stdc++.h>
    int main()
    {
        freopen("in.in","r",stdin);//读入数据生成器造出来的数据
        freopen("baoli.txt","w",stdout);//输出答案
        int a,b,ans=0;
        scanf("%d %d",&a,&b);
        for(int i=1;i<=a;++i) ans++;
        for(int i=1;i<=b;++i) ans++;
        printf("%d
    ",ans);
    }
    
    1. 正解代码例子:
    #include<bits/stdc++.h>
    int main()
    {
        freopen("in.in","r",stdin);
        freopen("std.txt","w",stdout);
        scanf("%d %d",&a,&b);
        printf("%d
    ",a+b);
    }
    
    1. 对拍代码
    #include<cstdio>
    #include<cstdlib>
    #include<ctime>
    using namespace std;
    int main()
    {
    	while(1) //一直循环,直到找到不一样的数据
    	{
    		system("data.exe");
    		system("baoli.exe");
    		system("std.exe");
    		if(system("fc std.txt baoli.txt")) //当 fc 返回1时,说明这时数据不一样
    			break; //不一样就跳出循环
    	}
    	return 0;
    }
    

    4.运行对拍程序

    ​ 目前,我们有了4份代码。为了实现对拍,我们要把这些代码放在同一个文件夹的同一层里。

    ​ 并且打开每一份代码,让每一份代码都生成一个同名的 .exe 程序。如下:

    ​ 然后,打开 duipai.exe ,我们可以看到程序正在对两个输出文件进行比较

    ​ 找不到差异,说明这两份代码输出的两个文件是一样的。

    ​ 那么我们可以一直拍着,如果长时间都是找不到差异,那么你写的正解就可能是对的了。

    ​ 如果找到差异,它会分别返回两个文件的数据,这样我们就有了一组错误数据,方便我们 debug 。

    这是存在差异的情况。

    5.程序的优化

    ①节约对拍次数

    ​ 在对拍时,你有没有发现在 cmd 的黑色框框里面,“找不到差异” 这几行输出的很快,看起来对拍的频率好像很高的样子。实际上,这样浪费了很多次对拍,数据生成需要一定的时间,而文件的读取输出等都需要一定时间。但是两个输出文件的对比却在不停地运行着,数据生成器生成的文件在一定的时间内是相同的,这样就浪费了许多次对拍。

    ​ 为此,我们可以使每次对拍完毕后休眠1秒,给四个程序留给一定的缓冲时间,使得每次对拍时,数据生成器生成的数据都不同。

    ​ 那么,我们可以使用 <windows.h> 库里的 Sleep(t)(t) 为时间,单位是毫秒。它可以使程序休眠 (t) 毫秒。我们可以在每次对拍之后加上 Sleep(1000) ,这样每次对拍之后休眠1秒,就不会出现浪费对拍的情况了。详见下面代码部分。

    ②美化对拍程序

    ​ 众所周知,每一道编写程序题都有时间限制。那么我们可以用一个计时函数"clock()",来计算我们写的正解用的时间,判断它是否超时(当然,本地测出的时间和评测机测的时间一般不同),并把所用时间在对拍程序上体现出来。

    ​ 我们还可以给把一个通过的数据当作一个测试点,还可以给他赋予编号,这些都能在对拍程序直观地体现出来,像下面这样:

    #include<iostream>
    #include<cstdio>
    #include<windows.h>
    #include<cstdlib>
    #include<ctime>
    using namespace std;
    int main()
    {
        int ok=0;
        int n=50;
        for(int i=1; i<=n; ++i)
        {
            system("make.exe > make.txt");
            system("std.exe < make.txt > std.txt");
            double begin=clock();
            system("baoli.exe < make.txt > baoli.txt");
            double end=clock();
    
            double t=(end-begin);
            if(system("fc std.txt baoli.txt"))
            {
                printf("测试点#%d Wrong Answer
    ",i);
            }
            else if(t>1000) //1秒
            {
                printf("测试点#%d Time Limited Enough 用时 %.0lfms
    ",i,t);
            }
            else
            {
                printf("测试点#%d Accepted 用时%.0lfms
    ",i,t);
                ok++; //AC数量+1
        	}
        }
        pritf("
    ");
        double res=100.0*ok/n;
        printf("共 %d 组测试数据,AC数据 %d 组。 得分%.1lf。",n,ok,res);
        
        Sleep(1000);//休眠1秒,为了节约对拍次数。
    }
    

    ​ 上面造了50个测试点,我们还可以计算程序 AC 多少个点来评个总分。这样可以让我们大致地了解一下编出的程序的正确性。

    ​ 这样子,对拍的作用就发挥到了极致。


    总结

    ​ 经过上面的一番讲解,大家一定对 “对拍” 已经有了一些了解。相信大家跟着上面的步骤,也能用对拍来解决一些实际的问题。

    ​ 在考场上,对于一些 比较容易写出暴力代码 而 写正解又担心自己写不对 的情况,我们可以用自己的暴力代码和写的正解比较一下。(毕竟暴力代码肯定不会WA掉,输出的答案只是慢了些,但答案肯定不会错) 这么比较,就可以检查出自己写的正解有没有大问题。

    ​ 而且,对拍还能方便地计算出任意随机数据所跑的时间,我们可以知道这个程序大约用的时间,我们可以自己再去调试优化。这避免了我们考试时写完代码,但是不知道自己的程序跑大数据非常慢,考试结束交程序评测的时候全是TLE。(悲)

    ​ 但是,对拍仅仅能确保自己写的正解能跑过一些比较小的数据。如果数据范围太大,一是暴力的程序跑不出来,二是数据生成的程序需要承受更多的压力。所以,如果想要确保能过大数据,需要自己手动去看一下代码里面是否隐藏着问题,比如中间过程要强转为 long long 等等。

    ​ 总之,对拍是个比较实用的工具,它非常方便地对两个文件进行了比较操作。这是编程的必备神器,大家一定要好好掌握!

    希望大家在2020NOIP中发挥超常,RP++!

    EdisonBa

    2020.8.15 首次发布

    2020.11.4 重大修改

  • 相关阅读:
    014 要区分好slice,splice和split,方法如下
    108 shutil模块(了解)
    107 pathlib模块(了解)
    106 collections模块
    105 typing模块
    104 re模块
    103_01 matplotlib模块
    102 pandas模块
    101 numpy模块
    SQLserver找出执行慢的SQL语句
  • 原文地址:https://www.cnblogs.com/EdisonBa/p/13509379.html
Copyright © 2020-2023  润新知