zoukankan      html  css  js  c++  java
  • 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 重大修改

  • 相关阅读:
    HDU Problem 1811 Rank of Tetris【拓扑排序+并查集】
    POJ Problem 2367 Genealogical tree【拓扑排序】
    HDU Problem 2647 Reward【拓扑排序】
    HDU Problem 1285 确定比赛名次【拓扑排序】
    HDU Problem HDU Today 【最短路】
    HDU Problem 3665 Seaside【最短路】
    HDU Problem 一个人的旅行 【最短路dijkstra】
    HDU Problem 1596 find the safest road【最短路dijkstra】
    Beyond Compare文本合并进行内容替换要注意什么
    用这些工具都可以比较代码的差异
  • 原文地址:https://www.cnblogs.com/EdisonBa/p/13509379.html
Copyright © 2011-2022 走看看