学习笔记:随机数据生成与对拍
随机数据生成与对拍
引入
本文介绍随机数据的生成方法与对拍测试方法。读者将学习使用随机数产生器,根据题目要求构造各种规模的输入数据,用于对自己编写的程序进行检测。同时,读者也将学习编写简单的脚本,自动化、批量化运行“数据生成程序”和两份不同的“问题求解程序”,并对两份程序的输出结果进行比对——我们把这种过程称为“对拍”。
随机数据生成与对拍可用于以下场景:
- 
在无法获得实时评测反馈的比赛中,思考并实现了一个“高分解法”,但实在不会证明自己的结论,或者不能确保自己编写的程序是否完全正确。这种情况下,建议读者酌情分配一些时间,额外编写一份随机数据生成程序、一份用朴素算法求解的程序(通常朴素解法时间复杂度高,但实现简单,不易出错)。然后把“高分解法"与“朴素解法”进行对拍,看二者的输出结果是否始终保持一致。 
- 
在平时解题时,自己编写的程序无法在 Online Judge上 取得 Accepted 结果,调试很久仍未发现错误,并且不能下载到题目的测试数据,或者虽然能下载到测试数据,但发生错误的数据规模过大,不容易进行调试。这时,可以编写一个随机数据生成程序,再编写一个使用朴素算法的程序(或者直接在网络上搜索其他人的AC程序),与自己的“错误解法”对拍。我们可以适当调整随机数据的规模,控制在易于人工演算和调试的范围内。虽然数据越小,出错概率越低,但是“对拍”脚本能够批量化执行,在成千上万次检测中,一般总能找到一个造成错误的小规模数据。 
- 
有一个不错的构思,自己出了一道题目。此时当然需要生成一些测试数据,并且需要用“对拍”来检测自己编写的“标准程序”的正确性。不过,除了随机数据外,通常还需要增加一些特殊构造的数据,保证测试数据的全面性。 
写这个主要是因为笔者忘得差不多了急需复习一波。
随机数据生成
头文件 cstdlib(stdlib.h)包含 rand() 和 srand() 两个函数,以及 RAND_MAX 常量。RAND_MAX 是一个不小于 $32767$ 的整数常量,它的值与操作系统环境、编译器环境有关。一般来说,在 Windows 系统中为 $32767$,在类Unix系统中为 $2147483647$。rand() 函数返回一个 $0$~RAND_MAX 之间的随机整数。
srand(seed) 函数接受 unsigned int 类型的参数 seed,以 seed 为“随机种子”。rand 函数基于线性同余递推式生成随机数,“随机种子”相当于计算线性同余时的一个初始参数,感兴趣的读者可以查阅相关资料。若不执行 srand() 函数,则种子默认为 $1$。
当种子确定后,接下来产生的随机数列就是固定的,所以这种随机方法也被称为“伪随机”。因此,一般在随机数据生成程序 main() 函数的开头,用当前系统时间作为随机种子。
头文件 ctime(time.h)包含 time 函数,调用 time(0) 可以返回从 $1970$ 年 $1$ 月 $1$ 日 $0$ 时 $0$ 分 $0$ 秒(Unix纪元)到现在的秒数。执行 srand(time(0)) 即可初始化随机种子。
下面的程序可作为随机数据生成器的模板,函数 rand() 返回一个随机整数,并综合考虑了操作系统和编译器环境的差异。
若要产生随机实数,则可以先产生一个较大的随机整数,再用它除以 $10$ 的次幂。若要同时产生负数,则可以先产生一个 $0$ 到 $2n$ 之间的随机整数,再减去 $n$,就得到了 $-n$~$n$ 之间的随机整数。
以下给出一些实例:
#include <iostream>
#include <cstdlib> // rand() 函数和 srand() 函数所在头文件
#include <ctime> // time() 函数所在头文件
#define MAXN 1145141919810
using namespace std;
int n, m, l, r, w;
struct edge{int u, v, w;}e[MAXN];
void solve1(){ // 生成一个长度为 n 且每个数小于 m 的序列
    cin >> n >> m;
    for(int i = 1 ; i <= n ; i ++)cout << rand() % m << " ";
    cout << endl;
}
void solve2(){ // 生成 n 个大于 1 小于 m 的区间
    cin >> n >> m >> l >> r;
    for(int i = 1 ; i <= n ; i ++){
        int left = rand() % m;
        int right = rand() % m;
        if(left > right)swap(left, right);
        cout << left << " " << right << endl;
    }
}
void solve3(){ // 生成一棵 n 个点的树,并附带小于 m 的边权
    cin >> n >> m;
    for(int i = 2 ; i <= n ; i ++){
        int fa = rand() % i;
        int val = rand() % m;
        cout << fa << " " << i << " " << val << endl;
    }
}
void solve4(){ // 生成一张 n 个点 m 条边的图,并附带小于 m 的边权
    cin >> n >> m >> w;
    for(int i = 2 ; i <= n ; i ++){ // 首先生成一棵 n 个点的树
        int fa = rand() % i;
        int val = rand() % m;
        e[i].u = fa;e[i].v = i;e[i].w = val;
    }
    for(int i = n + 1 ; i <= m ; i ++){ // 然后补全剩下的 m - n + 1 条边
        int fa = rand() % m;
        int val = rand() % m;
        e[i].u = fa;e[i].v = i;e[i].w = val;
    }
    random_shuffle(e + 1, e + m + 1); // 随机打乱并输出
    for(int i = 1 ; i <= m ; i ++)
        cout << e[i].u << " " << e[i].v << " " << e[i].w << endl;
}
void solve5(){ // 生成一条 n 个点的链,并附带小于 m 的边权
    cin >> n >> m;
    for(int i = 1 ; i < n ; i ++) // 首先生成数据
        e[i].u = i,e[i].v = i + 1,e[i].w = rand() % m;
    random_shuffle(e + 1, e + (n - 1) + 1); // 随机打乱并输出
    for(int i = 1 ; i <= m ; i ++)
        cout << e[i].u << " " << e[i].v << " " << e[i].w << endl;
}
void solve6(){ // 生成一张 n 个点的菊花图,并附带小于 m 的边权
    cin >> n >> m;
    for(int i = 2 ; i <= n ; i ++) // 首先生成数据
        e[i].u = 1,e[i].v = i,e[i].w = rand() % m;
    random_shuffle(e + 1, e + (n - 1) + 1); // 随机打乱并输出
    for(int i = 1 ; i <= m ; i ++)
        cout << e[i].u << " " << e[i].v << " " << e[i].w << endl;
}
int main(){
    srand(time(0)); // 初始化随机数种子
    freopen("data.in", "w", stdout); // 将随机数据直接写入到文件中
    solve1();solve2();solve3();solve4();solve5();solve6();
    return 0;
}此外还有一种特殊的图——蒲公英图。即链和菊花图的综合。令树的一部分构成链,一部分构成菊花,再把两部分连接。在以上三种数据的基础上,再添加少量随机的边,即可得到一张既包含局部特殊结构、又不失一般性和多样性的图。请读者自己尝试生成相关的数据。
对拍
假设我们在考场上已经写好了 $3$ 个程序:
- 自己打的暴力 bf.cpp。
- 自己写的正解 sol.cpp。
- 依题意编写的随机数据生成器 datamaker.cpp。
我们首先将这 $3$ 份代码都开启文件输入输出,具体地:
| 输入文件名 | 输出文件名 | |
|---|---|---|
| bf.cpp | data.in | data.ans | 
| sol.cpp | data.in | data.out | 
| datamaker.cpp | \ | data.in | 
当然这个只是笔者习惯的写法,其实咋写都行(只要不搞混……)
现在,我们需要编写一个脚本 pai.cpp,循环执行以下过程:
- 
运行随机数据生成器 datamaker.cpp。
- 
运行“暴力”程序 bf.cpp。
- 
运行“正解”程序 sol.cpp。
- 
比对文件 data.out和data.ans的内容是否一致。
Windows 和类 Unix 系统分别有 bat 批处理脚本和 bash 脚本。不过,为了避免介绍一门新的脚本语言,这里就用读者都熟悉的 C++ 语言来编写“对拍”程序。
头文件 cstdlib (stdlib.h)中提供了一个函数 system(),它接受一个字符串参数,并把该字符串作为系统命令执行。例如代码 system("C:\\datamaker.exe") 执行 C 盘根目录下的 datamaker.exe 文件。
Windows 系统命令 fc 或类 Unix 系统命令 diff 可以执行文件比对的工作,它们接受两个文件路径,比较二者是否一致。若一致,返回 $0$,否则返回非零值。
编译运行该 C++ 程序即可开始“对拍”过程。
类 Unix 系统对拍程序如下(这里以 A + B problem 为例):
datamaker.cpp
#include <iostream>
#include <cstdlib>
#define int long long
using namespace std;
signed main(){
    freopen("data.in", "w", stdout);
    srand(time(0));
    cout << rand() << endl;
    cout << rand() << endl;
    return 0;
}bf.cpp
#include <iostream>
#define int long long
using namespace std;
int add(int a, int b){
    int c;
    c = a + b;
    return c;
}
signed main(){
    freopen("data.in", "r", stdin);
    freopen("data.ans", "w", stdout);
    int a, b;
    cin >> a >> b;
    cout << add(a, b) << endl;
    return 0;
}sol.cpp
#include <iostream>
#define int long long
using namespace std;
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
signed main(){
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    int a, b;a = read();b = read();
    write(a + b);putchar('\n');
    return 0;
}pai.cpp
#include <cstdio>
#include <cstdlib>
#define int long long
using namespace std;
signed main(){
    system("cd ~/OIer/0908/pai");int T = 0;
    while(true){T++;
        system("./datamaker");system("./bf");
        double s = clock_t();system("./sol");
        double t = clock_t();
        if(system("diff data.out data.ans") == true){
            printf("Wrong Answer on Subtask #%d, %.0lfs\n", T, t - s);return 0;
        }else{
            printf("Accepted on Subtask #%d, %.0lfs\n", T, t - s);
        }
    }return 0;
}至于 Windows 系统的话,在类 Unix 系统对拍程序的基础上,更改 system 中的路径格式,并把 diff 命令改为 fc 命令,用时单位改为“毫秒”即可。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号