【技巧】數據生成器&對拍

目錄

前言

關於對拍的引言

一、數據生成器

模板

如何構造數據

其他特殊類型數據的生成

二、對拍

模板

三、兩個必需程序

四、開始對拍

五、例題

(一)題目

(二)分析

(三)代碼實現

待測代碼cheat.cpp——暴搜代碼

正確代碼bf.cpp——官方標程(考場上就是隻有自己寫暴力什麼的了qwq)

數據生成器makedata.cpp

對拍器duipai.cpp

總結


前言

以前一直不會生成數據和對拍...這次花了一整晚終於大概懂了+練了一道例題

希望比賽時能助我一臂之力吧

參考博客&推薦:

https://blog.csdn.net/C20190102/article/details/60138907

https://blog.csdn.net/C20190102/article/details/82944384#_141

https://blog.csdn.net/CQBZLYTina/article/details/84852495

關於對拍的引言

考試時或比賽時,總會遇到一些題,我們不得不騙個分or打個暴力

但是打着打着可能會突然靈光乍現——想到了更優的做法or一個優化

可是我們有時不好手動造數據測試或者證明來保證它的正確性,甚至考慮錯了的話,得分還不如直接打暴力高,這該怎麼辦呢,一個技巧孕育而生——對拍

簡而言之,對拍就是將你對正確性沒有把握的程序和有把握的程序進行比較

怎麼比較?很簡單的想法:輸入相同數據,用一個程序比較它們的輸出,若一模一樣,則你優化後的程序就是正確的

這就是對拍的大致原理了

這個進行比較工作的程序就是“對拍器”

怎麼非手動生成輸入數據?那麼就需要“數據生成器”

綜上,進行對拍需要四個程序——你的需檢測正確性的程序,答案正確的程序,一個數據生成器,和一個對拍器

一、數據生成器

【概覽】生成數據,需要使用隨機數生成函數rand( )

其實rand(  )生成的是僞隨機數序列,
一般採用一次同餘方程來生成隨機數

例如:
X(0)=seed;
X(n+1) = (A * X(n) + C) % M;
顯然,如何隨機數種子seed相同,則產生的隨機數序列也相同
如果在數據生成器中這樣初始化隨機數種子 : srand(time(NULL))
在調用數據生成器的時間間隔非常小的情況下,生成的將會是相同的數據,導致對拍效率降低

形象來說,它就相當於一個腦子和一雙手,腦子構造出數據後,手再把數據輸出出來,以供你的程序讀入並運行

一般寫作“makedata#.cpp”

"#"處填1、2、3...表示不同的數據範圍or類型(一般一道題裏會有部分分,針對不同的部分分可寫不同的數據生成器)

寫數據生成器,你需要做以下兩件事:

1.背住模板

2.寫出能構造相應數據的代碼

3.按題目格式輸出

模板

#include<cstdio>
#include<ctime>//必寫
#include<cstdlib>//必寫
int main()
{
	srand(time(NULL));//必寫
	/*以下是你的構造方法*/
}

如何構造數據

【概覽】解決方法很簡單,定義一個局部變量,把它的地址(操作系統在爲本程序分配運行內存時,
會隨機分配一段空間,保證不與之前的程序完全重合)與當前的機器時間作異或運算,將
得到一個不重複的seed,這樣就可以連續調用數據生成器,產生不重複的數據了

#include<bits/stdc++.h>
int main()
{
    int a;
    int *aa = &a;
    freopen("data1.txt", "w", stdout);
    srand(time(NULL) ^ (unsigned long long)aa);
    for(int i = 1; i <= 10; i++)
        printf("%d ", rand() % 10);
    puts("");
}

例:構造兩個n以內的非負整數(n=100000)

#include<cstdio>
#include<ctime>//必寫
#include<cstdlib>//必寫
int main()
{
	srand(time(NULL));//必寫
        int n=100000;
	printf("%d %d\n",rand()%n,rand()%n);
}

若 2~n 呢?%( n-1 )+2即可

1.餘數最小爲0,0+2=2,符合>=2的要求

2.仍然%n的話,有可能餘數爲n-1,再+2就是n+1,不符合生成<=n的要求了,%( n-1 )餘數最大爲n-2,所以%( n-1 )剛好符合<=n的要求

#include<cstdio>
#include<ctime>//必寫
#include<cstdlib>//必寫
int main()
{
	srand(time(NULL));//必寫
        int n=100000;
	printf("%d %d\n",rand()%(n-1)+2,rand()%(n-1)+2);
}

其他特殊類型數據的生成

生成隨機排列:先生成1~n,再random_shuffle()
生成集合:用set
生成無根樹:隨機生成一條樹邊,並查集檢驗連通性,合法的加入,不合法的忽略
                        如果要限制每個結點的度,可加上deg[ ]數組,統計各點的度
生成無向圖:鄰接矩陣存圖,隨機生成邊,加在圖上,做至少n*(n-1)/2次
生成DAG:先生成無向圖,再DFS,輸出編號小的結點指向編號大的結點的邊

for(int i = 1; i <= n; i++)
{
    sort(G[i].begin(), G[i].end());
    for(int j = 0; j < G[i].size(); j++)
    {
        int k = G[i][j];
        if(k > i)
            printf("%d %d\n", i, k);
    }
}

二、對拍

【概覽】在windows操作中對拍需要寫批處理程序。

一個簡單的對拍批處理程序如下:

echo off
:loop
    makedata.exe >data.in
    bf.exe <data.in>data.out
    std.exe <data.in>data.txt
    fc data.out data.txt
    IF ERRORLEVEL 1 GOTO stop
    IF ERRORLEVEL 0 GOTO loop
:stop
pause

對拍就是幫你比對兩個程序的輸出結果是否相同

一般寫作“dp.cpp”or“duipai.cpp”

模板

限制對拍次數的版本:

#include<iostream>
#include<windows.h>
using namespace std;
int main()
{
	/*有對拍次數的版本*/
	int t=這裏是檢查(對拍)次數;
	while(--t)
    {
		system("數據生成器名稱.exe > 數據生成器名稱.txt");
		system("程序1名稱.exe < 數據生成器名稱.txt > 程序1名稱.txt");
		system("程序2名稱.exe < 數據生成器名稱.txt > 程序2名稱.txt");
		if(system("fc 程序2名稱.txt 程序1名稱.txt"))
			break;
	}
	if(t==0)
		cout<<"no error"<<endl;
	else
		cout<<"error"<<endl;
	return 0;
}

直到有錯才停止的版本:

#include<iostream>
#include<windows.h>
using namespace std;
int main()
{
        /*直到錯了才停止的版本*/
        while(1)
        {
	    system("數據生成器名稱.exe > 數據生成器名稱.txt");
	    system("程序1名稱.exe < 數據生成器名稱.txt > 程序1名稱.txt");
	    system("程序2名稱.exe < 數據生成器名稱.txt > 程序2名稱.txt");
	    if(system("fc 程序2名稱.txt 程序1名稱.txt"))
		break;
        }
	return 0;
}

三、兩個必需程序

一個你待檢測的程序,一個保證了正確性的程序(一般是暴力)


四、開始對拍

將所有東西放進一個文件夾(主要是需要exe),運行對拍程序即可

運行結果樣板圖:

如果對拍顯示有問題,就直接打開data.txt,裏面有數據,1.txt和2.txt中分別是你程序1的答案和程序2的答案

如果你的對拍程序一出錯就關閉了,就在return 0前加一個getchar(),讓程序停一下,頭文件:cstdio


五、例題

(一)題目

原題:http://codeforces.com/problemset/problem/932/F

NOIP/CSP-S考試 騙分對拍生成數據訓練

JUMP TO LEAF

給定一棵樹,有n個節點(從1到n編號),以節點1爲根。每個節點

都有兩個與之關聯的權值。 第i個節點的權值爲ai和bi。
你可以從一個節點跳到其子樹中的任何節點。
從節點x跳到節點y的成本是ax和by的乘積。 由一個或多個跳躍形成

的路徑的總成本是單個跳躍成本的總和。 對於每個節點,計算從該

節點到達任意一個葉結點的最小總成本。
請注意,根不是葉結點,不能從節點跳到自身。

Input
第1行:1個正整數n,表示樹的結點數
第2行:n個空格分開的整數,表示ai
第3行:n個空格分開的整數,表示bi
接下來n-1行:每行兩個整數ui, vi,表示樹上的一條邊

Output
第1行:n個空格分開的整數,表示每個結點跳到任意的葉結點的最

小總代價

Sample input 1
3
2 10 -1
7 -7 5
2 3
2 1

Sample output 1
10 50 0

sample 1 detail
節點3已經是葉子,因此成本爲0。對於節點2,跳轉到節點3的成本

爲a2×b3 = 50;對於節點1,直接跳轉到節點3的成本爲a1×b3 = 10。

Sample input 2
4
5 -10 5 7
-8 -80 -3 -10
2 1
2 4
1 3

Sample output 2
-300 100 0 0

數據規模
對於 30% 的數據,n ≤ 5*10^3。
對於另外 20% 的數據,Bi = 1。
對於另外 20% 的數據,樹是一條鏈,且除 1 號點外,i 號點的父親

是 i-1。
對於 100% 的數據,2 ≤ n ≤ 10^5,|Ai|,|Bi|≤ 10^5。

練習要求:
1. 按要求生成指定特徵的數據,共10組,數據生成器統一命名爲

makedata#.cpp(#表示1,2,3,4),對於極限規模的數據,要保證樹的深度

不小於n/10。
用你的暴力程序儘量跑出正確答案。對於極限數據,有可能跑不出

答案,可以放棄。
數據名稱爲:
leaf#.in
leaf#.out

2. 寫一個能得到50~70分的騙分程序,程序名稱爲cheat.cpp

3. 寫一個正解或者暴力程序,進行對拍。如果你寫的是正解,程序

名稱爲std.cpp。如果你寫的是暴力,程序名稱爲bf.cpp

4. 寫一個對拍的批處理程序, 文件名爲dp.bat,對你的std.cpp 與

bf.cpp或cheat.cpp進行對拍

5. 最後,按照你的CSP考號,建立文件夾,把上述的生成的數據放

在data子文件夾中。其餘的放在考號文件夾中。
提交給我。


附錄:
隨機生成一棵樹的代碼

(二)分析

這道題用來訓練對拍,於是就愉快暴搜吧=。=...

(以爲自己可以暴力+特殊數據=50~70分
後來發現特殊數據是那麼的難搞=。=  
所以出現了一個騙分代碼與暴力代碼沒啥區別的尷尬場面... )


1.第一遍DFS搜出各點的父親

這樣搜索的時候就直接知道該點的兒子是誰了

2.第二遍dfs搜出【該點到葉節點的最小距離】和更新【該節點的兒子往上變成該節點同輩】

Ps.搜到葉節點後直接返回“0”

解析:

求“最小距離”可以用遞歸的思想,即在其兒子中找出 “ 兒子到葉節點距離+兒子與自己的距離 ” 的最小值

再加上“記憶化”的技巧:

if(dis[u]!=INF)	
	dis[u]=min(dis[u],dis[i]+a[u]*b[i]);
else
	dis[u]=min(dis[u],dfs(i,u)+a[u]*b[i]);

接下來有個難題——怎麼知道當前點u的所有子孫?

全部存下來似乎不太現實,空間又大、又花時間...

所以可以——實時更新!也就是一邊搜索距離、一邊更新各點子孫:

我們在搜索到點u的時候,將點u的所有兒子的祖先設爲u的父親

for(int i=head[u];i;i=nxt[i])
	if(E[i].v!=father)
	{
		dfs(E[i].v,u);
		old[E[i].v]=father;
		//類似並查集,把u的兒子往上變成father的兒子(與u同級) 
		//這樣一邊算出dis[]一邊實時更新兒子數 
	}

然後更新距離時,就從點u的兒子(fa[ v ]=u)和孫子(old[ v ]=u)中選取

這樣做的意義在於:做到了類似“01搜索”,也就是“該點選或不選”

例如下圖:

已搜到點5,父親爲2,兒子分別爲6,3,8,繼續向下搜,回溯回來時將兒子上移一層:old[ 6,3,8 ] = 2,然後更新答案

已回溯到點2,父親爲1,兒子爲5,將兒子上移一層:old[ 5 ] = 1,

然後更新答案:dis[ 2 ]=min( dis[ 2 ] , dis[ 5,6,3,8 ] + a[ 2 ] * b[ 5,6,3,8 ] )

已回溯到點4,父親爲1,兒子爲7,將兒子上移一層:old[ 7 ] = 1,

然後更新答案:dis[ 4 ]=min( dis[ 4 ] , dis[ 7 ] + a[ 4 ] * b[ 7 ] )

已回溯到點1,父親爲0,兒子爲2,4,將兒子上移一層:old[ 2,4 ] = 0,

然後更新答案:dis[ 1 ]=min( dis[ 1 ] , dis[ 2,4,5,7 ] + a[ 1 ] * b[ 2,4,5,7 ] )

結束搜索,輸出答案

應該講解清楚了吧...連打個暴搜都不容易...累shi ...=。=

(三)代碼實現

待測代碼cheat.cpp——暴搜代碼

(這道題我只會暴力...所以就拿暴力來對拍了...本來因該是用有優化或者做法更優的代碼)

/*以爲自己可以暴力+特殊數據=50~70分 
後來發現特殊數據是那麼的難搞=。=  
所以出現了一個騙分代碼與暴力代碼沒啥區別的尷尬場面... */
#include<cstdio>
#include<cmath>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=1e5,MAXM=4e5,INF=0x3f3f3f3f;
int head[MAXN+5],nxt[MAXM+5],fa[MAXN+5],old[MAXN+5];
int a[MAXN+5],b[MAXN+5],dis[MAXN+5];
int n,ecnt; 
struct Edge
{
	int u,v;
	Edge(int _u=0,int _v=0){u=_u,v=_v;}
	Edge(Edge &e){u=e.u,v=e.v;} 
}E[MAXM+5];
void Addedge(int u,int v)
{
	E[++ecnt]=Edge(u,v);
	nxt[ecnt]=head[u];
	head[u]=ecnt;
}
void DFS(int u,int father)
{
	bool flag=false;
	old[u]=fa[u]=father;
	for(int i=head[u];i;i=nxt[i])
	{
		int v=E[i].v;
		if(v!=father)
			flag=true,DFS(v,u);
	}
	if(!flag)
		dis[u]=0;
}
int dfs(int u,int father)
{
	if(!dis[u])//是葉節點,dis[u]=0
		return dis[u];
	for(int i=head[u];i;i=nxt[i])
		if(E[i].v!=father)
		{
			dfs(E[i].v,u);
			old[E[i].v]=father;
			//類似並查集,把u的兒子往上變成father的兒子(與u同級) 
			//這樣一邊算出dis[]一邊實時更新兒子數 
		}
 
	for(int i=1;i<=n;i++)
		if(old[i]==u||fa[i]==u)//在u的子樹內 
		{
			if(dis[u]!=INF)	
				dis[u]=min(dis[u],dis[i]+a[u]*b[i]);
			else
				dis[u]=min(dis[u],dfs(i,u)+a[u]*b[i]);
		}
	return dis[u];
}
int main()
{
	memset(dis,0x3f,sizeof(dis));
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)
		scanf("%d",&b[i]);
	for(int i=1;i<=n;i++)
		fa[i]=i;	
	for(int i=1;i<=n-1;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		Addedge(u,v);
		Addedge(v,u);
	}
	DFS(1,0);
	for(int i=1;i<=n;i++)
		if(i==n)
			printf("%d\n",dfs(i,fa[i]));
		else
			printf("%d ",dfs(i,fa[i]));
	return 0;
}

正確代碼bf.cpp——官方標程(考場上就是隻有自己寫暴力什麼的了qwq)

#include "bits/stdc++.h"
#define ll          long long
#define pb          push_back
#define mp          make_pair
#define pii         pair<int,int>
#define vi          vector<int>
#define all(a)      (a).begin(),(a).end()
#define F           first
#define S           second
#define sz(x)       (int)x.size()
#define hell        1000000007
#define endl        '\n'
#define rep(i,a,b)    for(int i=a;i<b;i++)
using namespace std;

bool Q;
struct Line {
    mutable ll k, m, p;
    bool operator<(const Line& o) const {
        return Q ? p < o.p : k < o.k;
    }
};
struct LineContainer : multiset<Line> {
    const ll inf = LLONG_MAX;
    ll div(ll a, ll b){
        return a / b - ((a ^ b) < 0 && a % b);
    }
    bool isect(iterator x, iterator y) {
        if (y == end()) { x->p = inf; return false; }
        if (x->k == y->k) x->p = x->m > y->m ? inf : -inf;
        else x->p = div(y->m - x->m, x->k - y->k);
        return x->p >= y->p;
    }
    void add(ll k, ll m) {
        auto z = insert({k, m, 0}), y = z++, x = y;
        while (isect(y, z)) z = erase(z);
        if (x != begin() && isect(--x, y)) isect(x, y = erase(y));
        while ((y = x) != begin() && (--x)->p >= y->p)
            isect(x, erase(y));
    }
    ll query(ll x) {
        assert(!empty());
        Q = 1; auto l = *lower_bound({0,0,x}); Q = 0;
        return l.k * x + l.m;
    }
};

vector<int> x,y;
vector<vi> adj;
vector<ll> ans;
vector<int> subsize;
void dfs1(int u,int v){
    subsize[u]=1;
    for(auto i:adj[u]){
        if(i==v)continue;
        dfs1(i,u);
        subsize[u]+=subsize[i];
    }
}

void dfs2(int v, int p,LineContainer& cur){
    int mx=-1,bigChild=-1;
    bool leaf=1;
    for(auto u:adj[v]){
        if(u!=p and subsize[u]>mx){
            mx=subsize[u];
            bigChild=u;
            leaf=0;
        }
    }
    if(bigChild!=-1){
        dfs2(bigChild,v,cur);
    }
    for(auto u:adj[v]){
        if(u!=p and u!=bigChild){
            LineContainer temp;
            dfs2(u,v,temp);
            for(auto i:temp){
                cur.add(i.k,i.m);
            }
        }
    }
    if(!leaf)ans[v]=-cur.query(x[v]);
    else ans[v]=0;
    cur.add(-y[v],-ans[v]);
}
void solve(){
    int n;
    cin>>n;
    x.resize(n+1);
    y.resize(n+1);
    ans.resize(n+1);
    subsize.resize(n+1);
    adj.resize(n+1);
    rep(i,1,n+1)cin>>x[i];
    rep(i,1,n+1)cin>>y[i];
    rep(i,1,n){
        int u,v;
        cin>>u>>v;
        adj[u].pb(v);
        adj[v].pb(u);
    }
    dfs1(1,0);
    LineContainer lc;
    dfs2(1,0,lc);
    rep(i,1,n+1)cout<<ans[i]<<" ";
}

int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int t=1;
//    cin>>t;
    while(t--){
        solve();
    }
    return 0;
}

數據生成器makedata.cpp

這裏我只拍了部分分,若有多個部分分,可寫多個對應的數據生成器,如:makedata1.cpp,makedata2.cpp... ...

//參考博客:https://blog.csdn.net/sslz_fsy/article/details/83064905 
//30%:n<=5000
#include<cstdio>
#include<ctime>
#include<cstdlib>
const int MAXN=5000;
int n,m,cnt,fa[MAXN+5];
int find(int x)//並查集版本 
{
	return x==fa[x]?x:fa[x]=find(fa[x]);
} 
int main()
{
	srand(time(NULL));
	n=rand()%MAXN+2;//2<=n<=100000
	printf("%d\n",n);
	for(int i=1;i<=n;i++)
		printf("%d ",rand()%MAXN-rand()%MAXN);//題目要求有負數 
	printf("\n");
	for(int i=1;i<=n;i++)
		printf("%d ",rand()%MAXN-rand()%MAXN);
	printf("\n");
	for(int i=1;i<=n;i++)
		fa[i]=i;
	while(cnt<n-1)
	{
		int x=rand()%n+1,y=rand()%n+1;//生成兩個點 
		int X=find(x),Y=find(y);
		if(X!=Y)//如果不是同一個祖先就連邊 
		{
			fa[X]=Y,cnt++;
			printf("%d %d\n",x,y);
		}
	}
	return 0;
}

對拍器duipai.cpp

#include<iostream>
#include<windows.h>
using namespace std;
int main()
{
	//有對拍次數的版本: 
	int t=1000;
	while(--t)
	{
		system("makedata.exe > makedata.txt");
		system("bf.exe < makedata.txt > bf.txt");
		system("cheat.exe < makedata.txt > cheat.txt");
		if(system("fc cheat.txt bf.txt"))
			break;
	}
	if(t==0)
		cout<<"no error"<<endl;
	else
		cout<<"error"<<endl;
	return 0;
}
/*
直到錯了才停止的版本:
while(1)
{
	system("數據生成器名稱.exe > 數據生成器名稱.txt");
	system("程序1名稱.exe < 數據生成器名稱.txt > 程序1名稱.txt");
	system("程序2名稱.exe < 數據生成器名稱.txt > 程序2名稱.txt");
	if(system("fc 程序2名稱.txt 程序1名稱.txt"))
		break;
}
*/

把他們放一起然後運行就可以開始 愉快地  對拍了~

總結

花了近一天終於講解清楚了...QAQ...

對拍是個好東西,特別是防止你考試時思路的遺漏疏忽導致失分(我當年B題六七十分,就是因爲有個小情況沒考慮到orz)

考試時合理運用可助你飛昇哦2333

但是平時要多做對拍相關的練習,熟悉自己的做題節奏、合理分配做題+對拍時間(一不小心就會發現時間在狂奔而去...)

CSP2019加油!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章