学习日常:造数据 - 上

前言

上次自己造数据,感悟颇丰,今天就来写一下这个话题。

陈老师将这个任务交给我们时,给了我们一个板子,姑且叫它 build_data.cpp:

/*
测试数据生成说明:
1.本文件放入标程同文件夹
2.在标程内贴入右边语句(不要修改): freopen("data.in","r",stdin); freopen("data.out","w",stdout);
3.运行标程,生成标程的exe文件
4.修改下面 0-3 处地方,根据题意,打印 in 文件
5.运行本程序,批量生成对应的in文件和out文件
P.S.本程序无法覆盖同名文件,如果生成过程有错,需要手动删除之前同名文件
*/
#include<bits/stdc++.h>
using namespace std;
char fn[50],op[50];
int rand(int l,int r) { //RAND_MAX*RAND_MAX
	int tmp = (rand() * RAND_MAX + rand()) % (r - l + 1);
	return l+tmp;
}
mt19937_64 ran(time(0)^rand()^(*(new int)));
//修改 0 :控制数据规模
long long b[11]= {10,100,1000,10000,100000,100000,100000,100000,100000,100000};
long long bb[11]= {10,10,10,10,100,500,1000,1000,1000,1000};
long long bbb[11]= {50,50,50,100000000,100000000,100000000,100000000,100000000,100000000,100000000};
long long bbbb[11]= {2,2,2,5,5,10,10,10,10,10};
int main() {
	sprintf(fn,"A.exe");		//修改 1 :标程名称
	srand(time(0));
	for (int fd=0; fd<=19; fd++) {			//修改 2:fd是 data文件的数字后缀 0-9
		sprintf(op,"data.in",fn);
		freopen(op,"w",stdout);
		//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓  修改 3 : in文件的内容
		// do something...
		
		//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑  修改 3 : in文件的内容
		freopen("con","w",stdout);
		system(fn);
		sprintf(op,"ren data.in data%d.in",fd);
		system(op);
		sprintf(op,"ren data.out data%d.out",fd);
		system(op);
	}
	return 0;
}

不知道大家觉得这个这个板子怎么样,反正我是觉得又丑又没用,我认为可以改进,于是就自己搓了一个 generator.cpp

改进

发现缺陷

首先,我认为这个板子有以下的缺陷:

  • 需要手动删除造错的数据,很不方便。
  • 需要在标程内写以下语句:
freopen("data.in","r",stdin); 
freopen("data.out","w",stdout);

感觉不太好,破坏了标程其实就是强迫症

  • 生成输入数据批量生成数据的程序写到了一起,个人觉得看起来分开更好,层次更分明,也不需要对着这份 build_data.cpp 不停改,其实也是强迫症
  • 实现很丑,还有一些意义不明的神奇东西,其实还是强迫症

于是针对这些问题,我自己搓了一个板子,将原来一个 build_data.cpp 拆成了两个程序:datamaker.cppgenerator.cpp

一分为二

datamaker.cpp

datamaker.cpp 负责生成输入数据

#include<bits/stdc++.h>
#define to_int(x) atoi(x)
#define to_ll(x) atoll(x)
#define shuf(st,ed) shuffle(st,ed,default_random_engine(rnd()))
using namespace std;
typedef long long ll;
mt19937_64 rnd(time(0));
ll rand(ll l,ll r){return rnd()%(r-l+1)+l;} 
int main(int argc,char *argv[]){
	// do something...
	return 0;
}

这里我写了一些造数据时常用的函数和宏,我们一个个来看:

  • shuf(st,ed):打乱 \([st,ed)\) 中的元素。
  • to_intto_ll:将 C 风格字符串(字符数组)转化为其存储的整数类型。
  • rand(l,r):生成 \([l,r]\) 中的随机数。

\(p.s.\) 随机数的生成采用 mt19937_64

其中,to_intto_ll 的使用场景下文会讲到,实际操作时我们也只需要修改这个文件即可。

generator.cpp

generator.cpp 负责批量生成数据

#include<bits/stdc++.h>
#include<windows.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const int CAS=205;
int cas,st,cnt,flag;
string pn,dmn,stdn,u,order,fn;
// problem name,data maker name,std name,file name
struct Unknownn{ll a[CAS];}U;
vector<Unknownn> v;
int main(){
	// Part1.
	cout<<"Loading",Sleep(500),system("cls");
	cout<<"Problem's name? ";
	cin>>pn;
	cout<<"How many cases do you want? ";
	cin>>cas;
	cout<<"The beginning ID? ";
	cin>>st;
	cout<<"DataMaker's name?(No \".exe\") ";
	cin>>dmn;
	cout<<"Std's name?(No \".exe\") ";
	cin>>stdn;
	Sleep(500);
	// Part2.
	system("cls");
	cout<<"How many unknown do you want? ";
	cin>>cnt;
	for(int i=1;i<=cnt;i++){
		cout<<"Unknown "+to_string(i)+":\nname? ";
		cin>>u;
		cout<<"val?\n";
		for(int j=1;j<=cas;j++) cout<<"Case #"<<j+st-1<<":",cin>>U.a[j];
		v.push_back(U);
		cout<<"Unknown "+to_string(i)+" already configured!\n";
	}
	Sleep(500);
	system("cls");
	// Part3.
	cout<<"What do you want to delete?\n1:All\n2:Only for use";
	cin>>flag;
	if(flag==1){
		cout<<"Deleting..."<<endl;
		system(("del "+pn+"*.in").c_str());
		system(("del "+pn+"*.out").c_str());
		Sleep(500);
	}
	cout<<"Making Input..."<<endl;
	for(auto k:v){
		for(int i=1;i<=cas;i++) cout<<k.a[i]<<" ";
		puts("");
	}
	for(int i=st;i<=st+cas-1;i++){
		order=dmn+".exe";
		for(auto k:v) order+=" "+to_string(k.a[i-st+1]);
		order+=" > "+pn+to_string(i)+".in";
		system(order.c_str());
		cout<<order<<endl;
		Sleep(1000);
	}
	Sleep(500);
	cout<<"Making Output..."<<endl;
	for(int i=st;i<=st+cas-1;i++){
		system((stdn+".exe < "+pn+to_string(i)+".in > "+pn+to_string(i)+".out").c_str());
		cout<<(stdn+".exe < "+pn+to_string(i)+".in > "+pn+to_string(i)+".out").c_str()<<endl;
	}
	cout<<"Succeed!";
	return 0;
}

可能要一下子搞懂这么多东西有些困难,我们将其拆开进行讲解。

\(p.s.\) 实际上这份程序在造数据时也是无需修改的,只需写好标程和 datamaker.cpp,再直接运行这个程序即可,这个程序在使用过程中有很多引导性的提示词,直接运行它,根据提示词应该就可以使用,所以如果你只是来拿代码的,那你可以不用往下读了。

约定:在下文中会出现每个部分的输入输出,使用连续的下划线表示真正使用时需要输入内容的位置,后面使用括号表示举例和说明

Part1. 基本配置

代码
// Part1.
cout<<"Loading",Sleep(500),system("cls");
cout<<"Problem's name? ";
cin>>pn;
cout<<"How many cases do you want? ";
cin>>cas;
cout<<"The beginning ID? ";
cin>>st;
cout<<"DataMaker's name?(No \".exe\") ";
cin>>dmn;
cout<<"Std's name?(No \".exe\") ";
cin>>stdn;
交互部分

开头会闪现一个 Loading,接着:

Problem's name? ______(问题的名称,会生成在数据的文件名中)
How many cases do you want? ______(测试点的数量)
The beginning ID? ______(测试点的起始编号,在分段造数据时用到)
DataMaker's name?(No ".exe") ______(datamaker.cpp应用程序的文件名,不需".exe")
Std's name?(No \".exe\") ______(标程应用程序的文件名,不需".exe")
解释

这部分代码阅读起来应该没有问题,很基础。

Part2. 设置参数

代码
struct Unknownn{ll a[CAS];}U;
vector<Unknownn> v;
// Part2.
cout<<"How many unknown do you want? ";
cin>>cnt;
for(int i=1;i<=cnt;i++){
cout<<"Unknown "+to_string(i)+":\nname? ";
cin>>u;
cout<<"val?\n";
for(int j=1;j<=cas;j++) cout<<"Case #"<<j+st-1<<":",cin>>U.a[j];
v.push_back(U);
cout<<"Unknown "+to_string(i)+" already configured!\n";
}
交互部分
How many unknown do you want? ______(需要调控的参数个数)
Unknown [k]:
name? ______(参数的名称)

解释

用于调控数据规模增加数据限制,后文再讲。

Part3. 生成数据

代码
// Part3.
cout<<"What do you want to delete?\n1:All\n2:Only for use";
cin>>flag;
if(flag==1){
	cout<<"Deleting..."<<endl;
	system(("del "+pn+"*.in").c_str());
	system(("del "+pn+"*.out").c_str());
	Sleep(500);
}
cout<<"Making Input..."<<endl;
for(auto k:v){
	for(int i=1;i<=cas;i++) cout<<k.a[i]<<" ";
	puts("");
}
for(int i=st;i<=st+cas-1;i++){
	order=dmn+".exe";
	for(auto k:v) order+=" "+to_string(k.a[i-st+1]);
	order+=" > "+pn+to_string(i)+".in";
	system(order.c_str());
	cout<<order<<endl;
	Sleep(1000);
}
Sleep(500);
cout<<"Making Output..."<<endl;
for(int i=st;i<=st+cas-1;i++){
	system((stdn+".exe < "+pn+to_string(i)+".in > "+pn+to_string(i)+".out").c_str());
	cout<<(stdn+".exe < "+pn+to_string(i)+".in > "+pn+to_string(i)+".out").c_str()<<endl;
}
cout<<"Succeed!";
交互部分
What do you want to delete?
1:All(删除所有对应问题名下的之前生成的数据)
2:Only for use(不物理删除文件,直接创建新文件或者覆盖之前的同名文件)
______(输入1或2,表示删除方式)
......(删除指令)
Making Input...
......(生成输入文件指令)
Making Output...
......(生成输出文件指令)
Succeed!
解释

这里运用了 cmd 中运行应用程序并指定输入输出位置的技术。

// cmd
test.exe [参数,空格分隔] < [输入文件名] > [输出文件名]

合二为一

理论部分

为什么前面的小标题叫 “一分为二”,这里又叫 “合二为一” 呢?因为我们还需要一种媒介,使得 generator.cpp 可以与 datamaker.cpp “沟通”,使它们在结构上分开在功能上合为一个整体

由于 generator.cpp 是带交互性的,所以我们需要让 generator.cpp 能将交互内容 “告诉” datamaker.cpp ,“指导” datamaker.cpp 的工作。

具体而言,datamaker.cpp 生成的数据应有与数据点相关的限制,如不同的数据规模特殊性质等,那我们的 generator.cpp 就做到了这一点。

我们看回 datamaker.cpp

#include<bits/stdc++.h>
#define to_int(x) atoi(x)
#define to_ll(x) atoll(x)
#define shuf(st,ed) shuffle(st,ed,default_random_engine(rnd()))
using namespace std;
typedef long long ll;
mt19937_64 rnd(time(0));
ll rand(ll l,ll r){return rnd()%(r-l+1)+l;} 
int main(int argc,char *argv[]){ // main 函数的参数是干什么用的?
	// do something...
	return 0;
}

你知道 main 函数的参数是干什么用的吗?

它就是两个程序 “沟通” 的媒介!

再看回 generator.cppPart 2

struct Unknownn{ll a[CAS];}U;
vector<Unknownn> v;
// Part2.
cout<<"How many unknown do you want? ";
cin>>cnt;
for(int i=1;i<=cnt;i++){
cout<<"Unknown "+to_string(i)+":\nname? ";
cin>>u;
cout<<"val?\n";
for(int j=1;j<=cas;j++) cout<<"Case #"<<j+st-1<<":",cin>>U.a[j];
v.push_back(U);
cout<<"Unknown "+to_string(i)+" already configured!\n";
}

刚开始我给这个部分取了一个小标题:设置参数

它的作用就是帮助你设置一些限制 datamaker.cpp 的参数,具体的调用参数则是在 Part 3 完成的:

// cmd
test.exe [参数,空格分隔] < [输入文件名] > [输出文件名]
//           这里↑

接着只需在 datamaker.cpp 中获取这些参数即可。

其中,main 函数的参数就是接受这些参数的部分。

  • int argc:接受到的参数个数。
  • char *argv[]:每个参数的 C 风格字符串(字符数组)。

因为我太菜了,还没写出不定长的参数,所以其实第一个参数没啥用。

第二个参数则是以 C 风格字符串(字符数组)的形式给出的,从 \(1\) 开始编号每个参数,我们可以使用 to_intto_ll 将其转化为整数类型。

目前的参数是用 long long 类型,仅支持整数类型,大家感兴趣的话可以加上别的类型,还可以写成模板的形式;或者可以写成按照数据点顺序排布的参数配置,这样每个数据点的参数数量可以都不一样。

实例部分

我们以 P1003 铺地毯 为例。

看它对数据范围的表述:

【数据范围】
对于 \(30\%\) 的数据,有 \(n \le 2\)
对于 \(50\%\) 的数据,\(0 \le a, b, g, k \le 100\)
对于 \(100\%\) 的数据,有 \(0 \le n \le 10^4\), \(0 \le a, b, g, k \le {10}^5\)

我们不要那么复杂,不管 \(a, b, g, k\),简化一下:

【数据范围】
对于 \(30\%\) 的数据,有 \(n \le 2\)
对于 \(100\%\) 的数据,有 \(0 \le n \le 10^4\)

这里有对 \(n\) 的限制,而是有数据点决定的,所以我们以数据点编号为参数,写出 datamaker.cpp

#include<bits/stdc++.h>
#define to_int(x) atoi(x)
#define to_ll(x) atoll(x)
#define shuf(st,ed) shuffle(st,ed,default_random_engine(rnd()))
using namespace std;
typedef long long ll;
mt19937_64 rnd(time(0));
ll rand(ll l,ll r){return rnd()%(r-l+1)+l;} 
int t,n;
int main(int argc,char *argv[]){
	t=to_int(argv[1]);
	n=(t<=3?rand(1,2):rand(200,1000)); // 200 是随便设的下限
	return 0;
}

这样就写好了,具体在 generator.cpp 中就创建一个参数,参数值直接赋为数据点编号即可,交互部分参照上文

当然,这只是一个比较粗略的实现,你当然可以通过别的参数设置判断方式来更加精细的配置。直接以 \(n\) 的范围为参数也可以,你甚至可以限制一个 \(n\),再让它波动一下,比如 \(n=[n-\sqrt{n},\min(n+\sqrt{n},20)]\)

尾声

那希望这篇文章对大家有帮助,你也可以继续改进我的板子,还可以私信跟我交流。

至于这篇文章的后继:科技·工程:造数据 - 下,肯定不会咕着的,已经在写了哦!

\(\bf{完结撒}\color{pink}{\bf{花}}\color{black}{\bf{!}}\)

posted @ 2024-08-08 21:21  godmoo  阅读(15)  评论(0编辑  收藏  举报