C++ 對拍詳解


對拍是什么

​對拍,是一個比較實用的工具。它能夠非常方便地對於兩個程序的輸出文件進行比較,可以幫助我們實現一些自動化的比較輸出結果的問題。

​眾所周知,每一道編程題目,都會有某種正解能拿到滿分;當我們想不出正解時,我們往往可以打暴力代碼來獲取部分分數。

​但是,當我們覺得有思路寫正解,但又擔心自己正解寫的不對,而恰好,我們又有一個能夠暴力騙分的代碼。這個時候就可以用到對拍。 暴力騙分代碼必須保證正確性,最多只是超時,不能出現答案錯誤的情況。

​這樣,我們可以造多組數據,讓暴力騙分的程序跑一遍,再讓我們自己寫的正解跑一遍,二者進行多次對比。如果多組數據都顯示二者的輸出結果一樣,那么這個正解大概率沒問題。相反地,如果兩組數據不同,我們就找到了一組錯誤數據,方便調試,找到正解哪里出了問題。

​這便是對拍。其作用也在上文提出。


對拍的實現

准備基本代碼

​首先,我們要有2份代碼,一份是這一道題“你寫的正解”代碼,另一份是同一道題“你打的暴力”代碼。

​為了方便,我們先用 A+B problem 來演示對拍。

​自己的代碼: std.cpp

#include <cstdio>
using namespace std;
int main()
{
    int a, b;
    scanf("%d%d", &a, &b);
    printf("%d\n", 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\n", ans);
    return 0;
}

​兩份代碼有了,我們把它放在同一個文件夾里。這樣算是做好了對拍的准備。

制作數據生成器

​我們制作的數據要求格式和上面兩份代碼的輸入格式一樣。

​根據上面,我們可以知道輸入的數據為2個數,中間有空格分隔。那么,我們的數據生成器就要輸出2個數,中間也要用空格分隔。

#include <cstdio>
#include <cstdlib>
#include <ctime>
int main()
{
    srand(time(0));
    //這是一個生成隨機數隨機種子,需要用到 ctime 庫
    printf("%d %d\n", rand(), rand());
    //這樣就生成了2個隨機數
    return 0;
}

​運行一下,確實生成了2個隨機數。

​注:如果不加那個隨機種子,生成的隨機數每次都是一樣的數。

Extra:數據范圍

​如果我們對於數據范圍有要求,那怎么辦呢?

​要讓隨機數限定在一個范圍,可以采用 “模除加加法” 的方式。

​對於任意數,\(0\leq rand()\%(a+1) \leq a\)

​於是 \(0+k\leq rand()\%(a+1) +k\leq 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;
}

看一下輸出結果

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

對拍代碼

標准輸入輸出代碼

​標准輸入輸出指的是:兩份基本代碼和數據生成代碼不含文件輸入輸出操作,如 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\n", 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\n", 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\n", 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份代碼。為了實現對拍,我們要把這些代碼放在同一個文件夾的同一層里。

​並且打開每一份代碼,讓每一份代碼都生成一個同名的 .exe 程序。如下:

​然后,打開 duipai.exe ,我們可以看到程序正在對兩個輸出文件進行比較

​找不到差異,說明這兩份代碼輸出的兩個文件是一樣的。

​那么我們可以一直拍着,如果長時間都是找不到差異,那么你寫的正解就可能是對的了。

​如果找到差異,它會分別返回兩個文件的數據,這樣我們就有了一組錯誤數據,方便我們 debug 。

這是存在差異的情況。

程序的優化

節約對拍次數

​在對拍時,你有沒有發現在 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\n", i);
        }
        else if (t > 1000) //1秒
        {
            printf("測試點#%d Time Limited Exceeded 用時 %.0lfms\n", i, t);
        }
        else
        {
            printf("測試點#%d Accepted 用時%.0lfms\n", i, t);
            ok++; //AC數量+1
        }
    }
    printf("\n");
    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 重大修改


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM