0x72
随机数据生成与对拍本节介绍随机数据的生成方式与对拍测试方式。将学习使用C++随机数产生器,根据题目要求构造各种规模的输入数据,用于对自己编写的程序进行检测。同时,也将学习编写简单的脚本,自动化、批量化运行“数据生成程序”和两份不同的“问题求解程序”,并对两份程序的输出结果进行对比——我们把这种过程称为“对拍”。
随机数据生成与对拍可用于一下场景:
1.在无法获得实时测评反馈的比赛中,思考并实现了一个“高分解法”,但实在不会证明自己的结论,或者不能确保自己编写的程序是否完全正确。
这种情况下,建议斟酌分配一些时间,额外编写一份随机数据生成程序、一份用朴素算法求解的程序(通常朴素解法时间复杂度高,但容易实现,不易出错)。然后把“高分解法”与“朴素解法”进行对拍,看二者的输出结果是否始终保持一致。
2.在平时解题时,自己编写的程序无法在Online Judge
取得Accepted
结果,调试很久之后仍未发现错误,并且不能下载到题目的测试数据,或者虽然能下载到测试数据,但发生错误的数据规模过大,不容易进行调试。
这时,可以编写一个随机数据生成程序,再编写一个使用朴素算法的程序(或者直接在网络上搜索其他人的AC程序),与自己的“错误解法”对拍。我们可以适当调整随机数据的规模,控制在易于人工演算和调试的范围内。虽然数据越小,出错概率越低,但是“对拍”脚本能够批量化执行,在成千上万次检测中,一般总能找到一个造成错误的小规模数据。
3.有一个不错的构思,自己出了一道题目。
此时当然需要生成一些测试数据,并且需要用“对拍”来检测自己编写的“标准程序”的正确性。不过,除了随机数据外,通常还需要增加一些特殊构造的数据,保证数据的全面性。
头文件cstdlib(stdlib.h)
包含了rand
和srand
两个函数,以及RAND_MAX
常量。
RAND_MAX
是一个不小于32767的整数常量,它的值与操作系统、编译环境有关。一般来说,在Windows
系统中为32767,在类Unix
系统中为2147483647。
rand()
函数返回一个0~RAND_MAX
之间的随机整数int。
srand(seed)
函数接受unsigned int
类型的参数seed
,以seed
为“随机种子”。rand
函数基于线性同余递推式生成随机数,“随机种子”相当于计算线性同余时的一个初始参数,感兴趣的可以查阅相关资料。若不执行srand
函数,则种子默认为1。
当种子确定下来,接下来产生的随机数列就是固定的,所以这种随机方法也别称为“伪随机”。因此,一般在随机数据生成程序main
函数的开头,用当前系统时间作为随机种子。
头文件ctime(time.h)
包含time
函数,调用time(0)
可以返回从1970年1月1日0时0分0秒(Unix纪元)到现在的秒数。执行srand((unsigned)time(0))
即可初始化随机种子。
下面的程序可作为随机数据生成器的模版,函数random(n)
返回 0 ∼ n − 1 0\sim n-1 0∼n−1之间的随机整数,并综合考虑了操作系统和编译器环境的差异,对int范围内的 n n n均能正常工作。
#include
#include
int random(int n)
{
return (long long)rand()*rand()%n;
}
int main()
{
srand((unsigned)time(0));
//...具体内容...
}
若要产生随机实数,则可以先产生一个较大的随机整数,在用它除以10的次幂。若要同时产生负数,则可以先产生一个 0 ∼ 2 n 0\sim 2n 0∼2n之间的随机整数,再减去 n n n,就得到了 − n ∼ n -n\sim n −n∼n之间的随机整数。
随机生成一张 n n n个点 m m m条边的无向图,图中不存在重边、自环,且必须连通,保证 5 ≤ n ≤ m ≤ n ∗ ( n − 1 ) / 4 ≤ 1 0 6 5\leq n\leq m\leq n*(n-1)/4\leq 10^6 5≤n≤m≤n∗(n−1)/4≤106。
pair e[1000005];
map,bool> h; //防止重边
//先生成一棵树,保证连通
for(int i=1;i
在树、图结构中,有三类特殊数据常用于对程序进行极端情况下的效率测试:
1.链形数据——有很长的直径。
就是把 N N N个节点用 N − 1 N-1 N−1条边连成一条长度为 N − 1 N-1 N−1的“链”。这种数据会造成很大的递归深度,也是点分治等算法需要特别注意的数据。
2.菊花形数据——有度数很大的节点。
以1号节点为中心, 2 ∼ N 2\sim N 2∼N号节点都用一条边与1号节点相连,最终1号节点的度数为 N − 1 N-1 N−1。这种数据画出来形似一朵“菊花”,缩点等图论算法若处理不当,复杂度容易在这种数据上退化。
3.蒲公英形数据。
即链形和菊花形数据的综合。令树的一部分构成链,一部分构成菊花,再把两部分连接。
在以上三种数据的基础上,再添加少量的随机的边,即可得到一张包含局部特殊结构、又不失一般性和多样性的图。
假设我们已经编好了三个程序:
1.自己编写的“正解”,即准备提交测评的程序,名为sol.cpp
。
该程序从文件data.in
中读入输入数据,并输出答案到data.out
中。
2.自己编写的“朴素解法”程序,名为bf.cpp
。
该程序从文件data.in
中读入输入数据,并输出答案到data.ans
中。
3.自己编写的随机数据生成程序,名为random.cpp
。
该程序输出随机数据到文件data.in
中。
依次编写这三个程序,得到三个可执行文件,例如在Windows
系统下得到sol.exe
,bf.exe
和random.exe
。
现在我们需要编写一个脚本,循环执行以下过程:
1.运行随机数据生成器random
。
2.运行“正解”程序sol
。
3.运行“朴素解法”程序bf
。
4.对比文件data.out
和data.ans
的内容是否一致。
Windows
和类Unix
系统中分别有bat
批处理脚本和bash
脚本。不过,为了避免介绍一门新的脚本语言,这里就用C++语言来编写“对拍”程序。
头文件cstdlib(stdlib.h)
中提供了一个函数system
,它接受一个字符串参数,并把该字符串作为系统命令执行。例如代码system("C:\\random.exe")
执行C盘根目录下的random.exe
文件。
Windows
系统命令fc
或类Unix
系统命令diff
可以执行文件比对的工作,它们接受两个文件路径,比较二者是否一致。若一致,返回0,否则返回非零值。
Windows
系统对拍程序#include
#include
#include
int main()
{
for(int T=1;T<=10000;T++)
{
//自行设定适当的路径
system("C:\\random.exe");
//当前程序已经运行的CPU时间,Windows下单位ms,Unix下单位s
double st=clock();
system("C:\\sol.exe");
double ed=clock();
system("C:\\bf.exe");
if(system("fc C:\\data.out C:\\data.ans"))
{
puts("Wrong Answer");
//程序立即退出,此时data.in即为发生错误的数据
return 0;
}
else
{
printf("Accept,测试点 #%d,此时 %.0lfms\n",T,ed-st);
}
}
}
编译运行该C++程序即可开始“对拍”过程。
Unix
系统对拍程序在Windows
系统对拍程序的基础上,更改system
中的路径格式,并把fc
命令改为diff
命令,用时单位改为“秒”。