目录
1. 前言
近几天在家中闲来无事,打算学一下开发中python的技术栈,于是快速过了py的语法,还是很容易上手的,后续打算继续学习爬虫和Django框架等。但想到下学期要举办新生赛及几场校内算法赛和组织程序设计训练诸多事情等,于是连忙联系到学长咨询了维护oj评测网站的相关事宜。接着买了一台云服务器,我在上面部署了oj网站并决定后续买一年的服务器供使用。
熟悉了所有操作后,于是决定出两题题目试一试(把所有的坑都踩一遍,下次就不会再踩),打过比赛的同学应该都清楚,我们算法竞赛题目一般是给1-3个样例点供大家理解题意,要想给本题完整AC(accepted)掉,是要通过题目后台所有的若干个测试点(一般至少10个),否则都会WA(答案错误)或者因为时间复杂度而超时(TLE)。对于做题人来说只要找到正确解法,使用不同语言都可以ac掉。但对于出题人来说不仅要知道本题的正确解法,简称为标程,而且还要根据合理的数据范围在后台添加若干组测试数据供选手评测,包含.in和.out两种文件,因为我们在算法竞赛中有输入也有输出。当数据很大,当然不是手造,那样会累死hh,我们就需要它来帮助我们自动造数据。于是笔者去网上找了一些造测试点的方法,但我个人认为写的一言难尽,像GitHub里也有介绍Generator的使用,即数据生成器。但我认为不怎么适合小白(出题的小白,比如我哈哈)使用。遂自己查阅了一些资料及方法,看blog回去复习了一下c里面的文件流的读写,经过几个两三个下午的时间,自己总结了在算法竞赛中几种常见测试点的造法,下面和大家分享一下———
2. 常见分类
我把造数据分为以下三个大类:一为数字字符串输入型,二为最小生成树型,三为最短路(图)模板型。第二和第三种我们一般是用不到的,用到最小生成树的时候题目难度会有很大的提升,本篇文章根据笔者打比赛的经验和绝大多数的适用场合来说,我在这里只介绍第一种普通数字字符串输入型造数据的方法,下面开始分小类进行介绍——
2.1 只输入一行n个数(当n很小时)
例如A+B这一题(低精度),我们会像下面这样造:
#include <iostream>
#include <vector>
#include <random>
#include <fstream>
#include <time.h>
#include <cstring>
#include <cstdio>
using namespace std;
typedef long long LL;
LL random(LL x)
{
return (LL)rand()*rand()%x;//此处生成的数小于等于(x-1),大于等于0
}
char str[5]=".in";
char head[5];
int main()
{
srand(time(NULL));
int a,b;
FILE* fp;
for(int i=1;i<=15;i++)//生成多少个测试点就到几
{
a = random(1e8+1);//由上面知这里范围要加1
b = random(1e8+1);
sprintf(head,"%d",i);
fp=fopen(strcat(head,str),"w");
fprintf(fp,"%d %d",a,b);//写入fp
fclose(fp);//关闭fp
}
return 0;
}
这样我们会在每个.in文件中生成随机的a,b两个数,可以随意调整a,b分别的数据范围,建议在出题时当n非常小的时候用上面源码生成,且不要开那么大的数据范围生成所有随机样例,这样会使测试点中没有小数据,可以先缩小数据范围设置30%-50%的小数据范围测试点,再扩大范围造更大数据范围测试点。
2.2 随机生成n个数
/*
3
23 96 36
这一份源码是生成这样的n个数
*/
#include <iostream>
#include <fstream>
#include <random>
#include <vector>
using namespace std;
int main() {
// 初始化随机数生成器
random_device rd;
mt19937 gen(rd());
// 生成n的值,这里我们假设n在1到100之间(包含1和100)
uniform_int_distribution<> dis_n(1, 100);
for(int i=1;i<=3;i++)//要生成多少组测试点这里就改到几
{
int n = dis_n(gen);
// 生成n个随机数的分布,这里我们假设每个数在1到1000之间
uniform_int_distribution<> dis_num(1, 1000);
// 创建一个vector来存储n个随机数
vector<int> numbers(n);
// 生成n个随机数并存储在vector中
for (int i = 0; i < n; ++i) {
numbers[i] = dis_num(gen);
}
// 构造文件名
string filename = to_string(i) + ".in"; // 你可以根据需要更改文件名生成规则
// 打开文件准备写入
ofstream outFile(filename);
if (!outFile.is_open()) {
cerr << "无法打开文件 " << filename << " 进行写入" << endl;
return 1; // 返回一个错误码
}
outFile<<n<<endl;//打印出n
// 将n个随机数写入文件
/*for (int num : numbers) {
outFile << num << " "; // 将每个数写入文件,并在数之间加上空格
} */
for(int i=0;i<n;i++)
{
outFile <<numbers[i]<<" ";
}
// 如果不需要在文件末尾有空格,可以添加一个换行符或者不在循环内添加空格
outFile << endl; // 在文件末尾添加一个换行符
// 关闭文件
outFile.close();
cout << "n的值是 " << n << ",随机生成的数已成功写入 " << filename << " 文件" << endl;
}
return 0;
}
2.3 随机生成n组数
随机生成n组数和随机生成n个数其实差不多,只需要多开一个数组即可,若是生成固定的n个数或n组数,只需要把代码中的随机n改为一个固定值即可,相信出题的童鞋都可以自己改好的hh ,随机生成n组数代码如下:
/*
4
1 100
77 13
32 66
68 19
这一份源码是生成这样的n组数
*/
#include <iostream>
#include <fstream>
#include <random>
#include <vector>
using namespace std;
int main() {
// 初始化随机数生成器
random_device rd;
mt19937 gen(rd());
// 生成n的值,这里我们假设n在1到100之间(包含1和10)
uniform_int_distribution<> dis_n(1, 10);
for(int i=1;i<=3;i++)//要生成多少组测试点这里就改到几
{
int n = dis_n(gen);
// 生成n个随机数的分布,这里我们假设每个数在1到100之间
uniform_int_distribution<> dis_num(1, 100);
uniform_int_distribution<> dis_num1(1, 100);
// 创建一个vector来存储n个随机数
vector<int> numbers(n);
vector<int> numbers1(n);
// 生成n个随机数并存储在vector中
for (int i = 0; i < n; ++i) {
numbers[i] = dis_num(gen);
numbers1[i] = dis_num1(gen);
}
// 构造文件名
string filename = to_string(i) + ".in"; // 你可以根据需要更改文件名生成规则
// 打开文件准备写入
ofstream outFile(filename);
if (!outFile.is_open()) {
cerr << "无法打开文件 " << filename << " 进行写入" << endl;
return 1; // 返回一个错误码
}
outFile<<n<<endl;
// 将n个随机数写入文件
/*for (int num : numbers) {
outFile << num << " "; // 将每个数写入文件,并在数之间加上空格
} */
for(int i=0;i<n;i++)
{
outFile <<numbers[i]<<" "<<numbers1[i]<<endl;
}
// 如果不需要在文件末尾有空格,可以添加一个换行符或者不在循环内添加空格
outFile << endl; // 在文件末尾添加一个换行符
// 关闭文件
outFile.close();
cout << "n的值是 " << n << ",随机生成的数已成功写入 " << filename << " 文件" << endl;
}
return 0;
}
注:若生成的n组数是区间不是点,区间我们知道右边数字一定大于最左边的,那我们只需加上如下代码块再fprintf到fp中即可,总之我们还是要根据题意要求去写random生成随机.in文件。
if(l>r) swap(l,r);
2.4 随机生成固定长度字符串
如我们想随机输入只包含大小写字母的字符串,则按以下源码来写:
#include <iostream>
#include <fstream>
#include <random>
#include <string>
using namespace std;
string generateRandomString(int length) {
// 定义包含26个英文字母大小写的字符串
string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// 初始化随机数生成器
random_device rd;
mt19937 gen(rd());
// 定义一个分布,用于从0到51之间生成随机数(因为letters长度为52)
uniform_int_distribution<> dis(0, letters.size() - 1);
string result;
result.resize(length); // 预先分配足够的空间
// 生成随机字符串
for (int i = 0; i < length; ++i) {
result[i] = letters[dis(gen)]; // 使用生成的随机数作为索引来从letters中选择字符
}
return result;
}
int main() {
// 假设我们想要一个长度为10的随机字符串
int n = 10;
for(int i=1;i<=5;i++)
{
// 生成随机字符串
string randomStr = generateRandomString(n);
// 构造文件名
string filename = to_string(i) + ".in"; // 你可以根据需要更改文件名生成规则
// 打开文件准备写入
ofstream outFile(filename);
if (outFile.is_open()) {
outFile << randomStr << endl; // 将随机字符串写入文件,并在末尾添加换行符
outFile.close();
} else {
cerr << "无法打开文件 randomString.in 进行写入" << endl;
}
// 输出到控制台
cout << "随机生成的字符串是: " << randomStr << endl;
}
return 0;
}
2.5 随机生成不固定长度字符串
#include <iostream>
#include <fstream>
#include <random>
#include <string>
using namespace std;
string generateRandomString(int length) {
// 定义包含26个英文字母大小写的字符串
string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// 初始化随机数生成器
random_device rd;
mt19937 gen(rd());
// 定义一个分布,用于从0到51之间生成随机数(因为letters长度为52)
uniform_int_distribution<> dis(0, letters.size() - 1);
string result;
result.resize(length); // 预先分配足够的空间
// 生成随机字符串
for (int i = 0; i < length; ++i) {
result[i] = letters[dis(gen)]; // 使用生成的随机数作为索引来从letters中选择字符
}
return result;
}
int main()
{
random_device rd;
mt19937 gen(rd());
// 假设我们想要一个长度为n的随机字符串
uniform_int_distribution<> dis_n(1, 10);
for(int i=1;i<=5;i++)
{
int n = dis_n(gen);
// 生成随机字符串
string randomStr = generateRandomString(n);
// 构造文件名
string filename = to_string(i) + ".in"; // 你可以根据需要更改文件名生成规则
// 打开文件准备写入
ofstream outFile(filename);
if (outFile.is_open()) {
outFile << randomStr << endl; // 将随机字符串写入文件,并在末尾添加换行符
outFile.close();
} else {
cerr << "无法打开文件 randomString.in 进行写入" << endl;
}
// 输出到控制台
cout << "长度为:"<<n<<",随机生成的字符串是: " << randomStr << endl;
}
return 0;
}
2.6 随机生成固定特殊字符
/*
此源码生成+ * +这种固定长度的字符形式
*/
#include <iostream>
#include <fstream>
#include <random>
#include <string>
using namespace std;
int main()
{
// 初始化随机数生成器
random_device rd;
mt19937 gen(rd());
// 定义包含字母(大小写)和特殊字符的字符串
string characters = "+*";
// 定义一个分布,用于从0到characters.size()-1之间生成随机数
int n = 3;
vector<char> result(n);
result.resize(n); // 预先分配足够的空间
// 假设我们想要一个长度为3的随机字符串
for(int i=1;i<=3;i++)//生成3个.in文件,即三个测试点
{
uniform_int_distribution<> dis(0, characters.size() - 1);
// 生成随机字符串
for (int i = 0; i <n; ++i)
{
result[i] = characters[dis(gen)]; // 使用生成的随机数作为索引来选择字符
}
// 构造文件名
string filename = to_string(i) + ".in"; // 你可以根据需要更改文件名生成规则
// 打开文件准备写入
ofstream outFile(filename);
if (!outFile.is_open())
cerr << "无法打开文件 randomStringWithSpecialChars.in 进行写入" << endl;
for (int i = 0; i < n; ++i)
{
outFile<<result[i]<<" " ; // 使用生成的随机数作为索引来选择字符
}
outFile << endl; // 在文件末尾添加一个换行符
outFile.close();
}
return 0;
}
注:通过2.4,2.5,2.6我们可以知道通过修改string characters里所包含的字母以及特殊字符来获得我们题目中输入想要的随机字符或者字符串数组等。(生成随机值的.in文件的代码我把命名为rondom.cpp)
3. .out文件的生成
当我们写对了正确的.in文件,接着就要生成对应的.out文件了,前面提到过,我们想要写对正确对应的.out文件,首先我们自己出题就要有正确的标程。因为本质上我们就是利用标程,在标程的源文件里运用c语言里的文件读写方法去读取.in文件里的数据,通过跑正确标程的代码得到正确的.out文件中的数据。(这里我把.out文件的生成命名为data.cpp)
下面演示一下以上A+B这道题对应的data.cpp:
#include <iostream>
#include <vector>
#include <random>
#include <fstream>
#include <time.h>
#include <cstring>
#include <cstdio>
using namespace std;
typedef long long LL;
char strin[5]=".in";
char strout[5]=".out";
char head[5];
int main()
{
FILE* fp;
FILE* fo;
int a,b,c;
for(int i=1;i<=15;i++)
{
sprintf(head,"%d",i);
fp = fopen(strcat(head,strin),"r");//找到对应的.in
sprintf(head,"%d",i);
fo = fopen(strcat(head,strout),"w");//找到对应的.out
fscanf(fp,"%d%d",&a,&b);//读取对应的.in中的数据 (注意读数据的写法)
c = a+b;//这里运行标程
fprintf(fo,"%d",c);//写入对应的.out
}
return 0;
}
tips:上面其实也就是写data.cpp代码的模板,必须严格通过每个题目不同的标程,输入输出,数据范围来得到,不能不改动直接套,不然得不到正确的data.cpp及生成随机值的rondom.cpp
4. 实际举例
在这里我以一道考察dfs的题目完整的演示一下怎么造数据,先看题目————
4.1 CF55B题面(Smallest number)
Recently, Vladimir got bad mark in algebra again. To avoid such unpleasant events in future he decided to train his arithmetic skills. He wrote four integer numbers𝑎,𝑏,𝑐,𝑑on the blackboard. During each of the next three minutes he took two numbers from the blackboard (not necessarily adjacent) and replaced them with their sum or their product. In the end he got one number. Unfortunately, due to the awful memory he forgot that number, but he remembers four original numbers, sequence of the operations and his surprise because of the very small result. Help Vladimir remember the forgotten number: find the smallest number that can be obtained from the original numbers by the given sequence of operations.
4.2 Input Description
First line contains four integers separated by space:0<=𝑎,𝑏,𝑐,𝑑<=1000— the original numbers. Second line contains three signs ('+' or '*' each) separated by space — the sequence of the operations in the order of performing. ('+' stands for addition, '*' — multiplication)
4.3 Output Description
Output one integer number — the minimal result which can be obtained.
Input 1:
1 1 1 1
+ + *
output 1:
3
Input 2:
2 2 2 2
* * +
output 2:
8
Input 3:
1 2 3 4
* + +
output 3:
9
4.4 提示
数据范围
0<=𝑎,𝑏,𝑐,𝑑<=1000
前25%测试点保证0<=𝑎,𝑏,𝑐,𝑑<=300;
保证100%测试点0<=𝑎,𝑏,𝑐,𝑑<=1000
温馨提示
三个操作符必须按顺序以此操作,四个数字可以不依次操作,下面给出样例二解释
如样例二,前两个二相乘得到4,后两个二相乘得到4,故最终相加得到最小的数为8
Please, do not use %lld specificator to read or write 64-bit integers in C++. It is preffered to use cin (also you may use %I64d).
题目就描述到这里,下面开始我们的造数据之旅了————
4.5 标程
先给出本题的标程:正解为dfs,当然笔者后来在洛谷上看到有好几种不一样的解法,如使用 srand 和 rand 随机数,还有全排列好像都是能ac掉的,在这里插个链接感兴趣供大家参考点我跳转原题哈哈,下面我按老套路的dfs给个ac代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = LLONG_MAX; // 使用 C++ 标准库中定义的最大值
const int maxl = 15; // 足够存储文件名
int n = 4;
int m = 3;
vector<int> a(n);
vector<char> ch(m);
int ans = maxn;
bool vis[maxl];
void dfs(int cnt) {
if (cnt == 4) {
for (int i = 1; i <= n; i++) if (!vis[i]) ans = min(ans, a[i]);
return;
}
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= 4; j++) {
if (i == j || vis[i] || vis[j]) continue;
int tmp = a[i];
vis[j] = 1;
if (ch[cnt - 1] == '+') a[i] += a[j];
else a[i] *= a[j];
dfs(cnt + 1);
a[i] = tmp;
vis[j] = 0;
}
}
}
void solve() {
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 0; i < m; i++) cin >> ch[i]; // 从 0 开始读取操作符
dfs(1);
cout<<ans<<endl;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int t = 1;
while(t--) solve();
return 0;
}
好啦,标程已经有了,不再做过多的解释,下一步我们来写生成随机值的rondom.cpp文件———
4.6 rondom.cpp
首先当看到输入第一行为4个数,第二行为三个字符时,就应该想到用笔者在前面写的2.2及2.6两种造数据方法,a,b,c,d均在0-1000,笔者是想造前25%的数据a,b,c,d均在0-300之间,100%的数据保证在0-1000之间,这个在我给的方法源码里可以直接修改造出的数据范围和造的测试点个数,十分方便,大家可根据实际情况傻瓜式操作即可,下面给出romdom.cpp源码:
/*
3 4 5 6
+ + *
这一份源码是生成这样的
*/
#include <iostream>
#include <fstream>
#include <random>
#include <vector>
using namespace std;
int main() {
// 初始化随机数生成器
random_device rd;
mt19937 gen(rd());
string characters = "+*";
int n = 4;
int m = 3;
for(int i=1;i<=10;i++)//要生成多少组测试点这里就改到几,这里是造10个
{
// 生成n个随机数的分布,这里我们假设每个数在1到1000之间
uniform_int_distribution<> dis_num(0, 300);
uniform_int_distribution<> dis(0, characters.size() - 1); // 生成随机字符串
// 创建一个vector来存储n个随机数
vector<int> numbers(n);
vector<char> result(m);
result.resize(m); // 预先分配足够的空间
// 生成n个随机数并存储在vector中
for (int i = 0; i < 4; ++i) {
numbers[i] = dis_num(gen);
}
for(int i=0;i<3;i++)
{
result[i] = characters[dis(gen)];
}
// 构造文件名
string filename = to_string(i) + ".in"; // 你可以根据需要更改文件名生成规则
// 打开文件准备写入
ofstream outFile(filename);
if (!outFile.is_open()) {
cerr << "无法打开文件 " << filename << " 进行写入" << endl;
return 1; // 返回一个错误码
}
//outFile<<n<<endl;
// 将n个随机数写入文件
/*for (int num : numbers) {
outFile << num << " "; // 将每个数写入文件,并在数之间加上空格
} */
for(int i=0;i<n;i++)
{
outFile <<numbers[i]<<" ";
}
// 如果不需要在文件末尾有空格,可以添加一个换行符或者不在循环内添加空格
outFile << endl; // 在文件末尾添加一个换行符
for(int i=0;i<m;i++)
{
outFile <<result[i]<<" ";
}
outFile << endl;
// 关闭文件
outFile.close();
//cout << "n的值是 " << n << ",随机生成的数已成功写入 " << filename << " 文件" << endl;
}
return 0;
}
4.7 data.cpp
完成上面的工作后,我们就到了最后一步根据标程和rondom.cpp写没一个测试点对应的输出,由上面笔者写的标题三的内容,我给出此题data.cpp的写法:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxl = 1e1 + 7;
const int maxn = 0x3f3f3f3f3f3f3f3f;
int n = 4;
int m = 3;
vector<int> a(n+1);
vector<char> ch(m+1);
//int a[maxl],
int ans = maxn;
//char ch[maxl];
bool vis[maxl];
char strin[5]=".in";
char strout[5]=".out";
char head[10];
void dfs(int cnt) {
if (cnt == 4) {
for (int i = 1; i <= 4; i++) if (!vis[i]) ans = min(ans, a[i]);
return;
}
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= 4; j++) {
if (i == j || vis[i] || vis[j]) continue;
int tmp = a[i];
vis[j] = 1;
if (ch[cnt] == '+') a[i] += a[j];
else a[i] *= a[j];
dfs(cnt + 1);
a[i] = tmp;
vis[j] = 0;
}
}
}
int slove() {
//for (int i = 1; i <= 4; i++) cin >> a[i];
//for (int i = 1; i <= 3; i++) cin >> ch[i];
dfs(1);
return ans;
}
signed main(){
FILE* fp;
FILE* fo;
for(int i=1;i<=10;i++)
{
a = {0};
ch = {0};
ans = maxn;//对两个数组一个变量进行初始化
sprintf(head,"%lld",i);
fp = fopen(strcat(head,strin),"r");//找到要读取的.in文件
sprintf(head,"%lld",i);
fo = fopen(strcat(head,strout),"w");//找到要读取的.out文件
for(int i=1;i<=4;i++)
fscanf(fp,"%lld",&a[i]);//读取数据
for(int i=1;i<=3;i++)
fscanf(fp," %c",&ch[i]);//读取数据
fprintf(fo,"%lld",slove());//写入对应的.out文件中
}
return 0;
}
注:这里要注意的是根据实际的题目要求和标程来写data.cpp,要注意对两个数组一个变量进行初始化,因为是一个大循环,生成很多个测试点文件。每一次循环一般都会对变量及数组等进行初始化,不然会影响后续写入.out的结果。
5 总结
完结撒花!!!通过笔者的努力给大家总结带来了Acm算法竞赛中常见出题造测试点的方法,感谢大家的观看,希望大家可以点个赞赞关注一下哈哈,也是我前进道路上的动力&& 下次见!!!
(如果想尝试ac这个例题的朋友欢迎来我个人部署的OJ网站试一下,链接在这里BBU_AC_OJ)
每一次AC都是一场感动!!!拜拜~~