對拍是什么
對拍,是一個比較實用的工具。它能夠非常方便地對於兩個程序的輸出文件進行比較,可以幫助我們實現一些自動化的比較輸出結果的問題。
眾所周知,每一道編程題目,都會有某種正解能拿到滿分;當我們想不出正解時,我們往往可以打暴力代碼來獲取部分分數。
但是,當我們覺得有思路寫正解,但又擔心自己正解寫的不對,而恰好,我們又有一個能夠暴力騙分的代碼。這個時候就可以用到對拍。 暴力騙分代碼必須保證正確性,最多只是超時,不能出現答案錯誤的情況。
這樣,我們可以造多組數據,讓暴力騙分的程序跑一遍,再讓我們自己寫的正解跑一遍,二者進行多次對比。如果多組數據都顯示二者的輸出結果一樣,那么這個正解大概率沒問題。相反地,如果兩組數據不同,我們就找到了一組錯誤數據,方便調試,找到正解哪里出了問題。
這便是對拍。其作用也在上文提出。
對拍的實現
准備基本代碼
首先,我們要有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\) 。
舉幾個簡單的例子:
-
當
a=rand()%2
時,a 的范圍:\(0 \leq a \leq 1\) 。 -
當
a=rand()%2+1
時,a 的范圍:\(1 \leq a \leq 2\) 。 -
要想讓 \(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。
那么,我們就可以執行這一操作來實現對拍。
- 先讓數據生成器輸出數據。
system("data.exe > in.txt")
- 然后用這個數據跑一遍暴力代碼,輸出結果。
system("baoli.exe < in.txt > baoli.txt")
- 再用這個數據跑一遍你寫的正解代碼,輸出結果。
system("std.exe < in.txt > std.txt")
- 把兩個結果相比較,判斷是不是一樣的。
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 。
-
數據生成代碼例子:
#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);
}
-
暴力代碼例子:
#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);
}
-
正解代碼例子:
#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);
}
-
對拍代碼
#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 重大修改