前言
上次自己造数据,感悟颇丰,今天就来写一下这个话题。
陈老师将这个任务交给我们时,给了我们一个板子,姑且叫它 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
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.cpp 和 generator.cpp。
一分为二
datamaker.cpp
datamaker.cpp 负责生成输入数据。
#include
#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_int 和 to_ll:将 C 风格字符串(字符数组)转化为其存储的整数类型。
rand(l,r):生成 \([l,r]\) 中的随机数。
\(p.s.\) 随机数的生成采用 mt19937_64。
其中,to_int 和 to_ll 的使用场景下文会讲到,实际操作时我们也只需要修改这个文件即可。
generator.cpp
generator.cpp 负责批量生成数据。
#include
#include
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair
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
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 #"<
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..."< system(("del "+pn+"*.in").c_str()); system(("del "+pn+"*.out").c_str()); Sleep(500); } cout<<"Making Input..."< for(auto k:v){ for(int i=1;i<=cas;i++) cout< 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< Sleep(1000); } Sleep(500); cout<<"Making Output..."< 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()< } 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 // 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 #"< 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..."< system(("del "+pn+"*.in").c_str()); system(("del "+pn+"*.out").c_str()); Sleep(500); } cout<<"Making Input..."< for(auto k:v){ for(int i=1;i<=cas;i++) cout< 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< Sleep(1000); } Sleep(500); cout<<"Making Output..."< 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()< } 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 #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.cpp 的 Part 2: struct Unknownn{ll a[CAS];}U; vector // 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 #"< 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_int 和 to_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 #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{!}}\)