学习日常:造数据 - 上
前言
上次自己造数据,感悟颇丰,今天就来写一下这个话题。
陈老师将这个任务交给我们时,给了我们一个板子,姑且叫它 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.cpp
和 generator.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_int
和to_ll
:将 C 风格字符串(字符数组)转化为其存储的整数类型。rand(l,r)
:生成 \([l,r]\) 中的随机数。
\(p.s.\) 随机数的生成采用
mt19937_64
。
其中,to_int
和 to_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.cpp
的 Part 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_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<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{!}}\)