• Testlib-Generator使用笔记


    Testlib-Generator使用笔记

    Testlib 使用来配合算法竞赛出题的工具,本文仅介绍其中的一个模块——数据生成器的使用方法。

    Testlib 分为四部分:

    • 编写 Generator,即数据生成器。
    • 编写 Validator,即数据校验器,判断生成数据是否符合题目要求,如数据范围、格式等。
    • 编写 Interactor,即交互器,用于交互题。
    • 编写 Checker,即 Special Judge

    项目地址:MikeMirzayanov/testlib

    下载该项目,拷贝出 testlib.h 文件。

    Testlib库仅有 testlib.h 这一个文件,使用时仅仅需要在编写的程序开头添加 #include "testlib.h"即可。本文记录 Generator 数据生成器的常见使用方法。

    从一个简单的例子开始

    // clang-format off
    #include "testlib.h"
    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char* argv[]) { // argc 与 argv 是命令行参数
      registerGen(argc, argv, 1);
      int n = atoi(argv[1]);  // 获取命令行的第一个参数
      cout << rnd.next(1, n) << " ";
      cout << rnd.next(1, n) << endl;
    }
    

    这个程序可以生成两个([1,n]) 范围内的整数,n可以通过程序执行时传入。

    平时我们用的 rand() 或 C++11 的 mt19937/uniform_int_distribution ,当操作系统不同、使用不同编译器编译、不同时间运行等,它们的输出都可能不同(对于非常常用的 srand(time(0)) ,这是显然的),而这就会给生成数据带来不确定性。而 Testlib 中的随机值生成函数则保证了相同调用会输出相同值,与 generator 本身或平台均无关。

    需要注意的是,一旦使用了 Testlib,就不能再使用标准库中的 srand()rand() 等随机数函数,否则在编译时会报错。另外,不要使用 std::random_shuffle() ,请使用 Testlib 中的 shuffle() ,它同样接受一对迭代器。它使用 rnd 来打乱序列,即满足如上“好的 generator”的要求。

    在一切之前,先执行 registerGen(argc, argv, 1) 初始化 Testlib(其中 1 是使用的 generator 版本,通常保持不变),然后我们就可以使用 rnd 对象来生成随机值。

    随机数种子取自命令行参数的哈希值,对于某 generator g.cppg 100 (类Unix系统) 和 g.exe "100" (Windows 系统) 将会有相同的输出,而 g 100 0 则与它们不同。

    rnd 的用法总结:

    1. rnd.next(4):等概率生成一个 [0,4]范围内的整数

    2. rnd.next(4, 100):等概率生成一个 [4,100]范围内的整数

    3. rnd.next(4.0):等概率生成一个 [0, 4.0)范围内的浮点数

    4. rnd.next("one | two | three"):等概率返回onetwothree 中的一个

    5. rnd.next("[1-9][0-9]{99}"):长度为100的数字型字符串

    6. rnd.wnext(4, t)

      wnext() 是一个生成不等分布(具有偏移期望)的函数,(t) 表示调用 next() 的次数,并取生成值的最大值。例如 rnd.wnext(3, 1) 等同于 max({rnd.next(3), rnd.next(3)})rnd.wnext(4, 2) 等同于 max({rnd.next(4), rnd.next(4), rnd.next(4)}) 。如果 (tlt 0),则为调用(-t) 次,取最小值;如果 (t=0) ,等同于 next()

      关于 rnd.wnext(i,t) 的形式化定义:

      [wnext(i,t) = egin{cases} next(i) & t = 0\ max(next(t), wnext(i-1, t)) & t > 0\ min(next(t), wnext(i+1, t)) & t < 0 \ end{cases} ]

      另外,从官方给定的示例中,也支持传入两个范围参数:

      [wnext(l,r,t) = egin{cases} next(l,r) & t=0\ max(next(l,r), wnext(l,r,t-1)) & t > 0 \ min(next(l,r), wnext(l,r,t+1)) & t < 0 end{cases} ]

    7. rnd.any(container):等概率返回一个具有随机访问迭代器(如 std::vectorstd::string )的容器内的某一元素的引用

    新特性:解析命令行参数

    通常,我们使用 int a = atoi(argv[3]) 来获取命令行中的参数,并将其转换为整数,但这么做有时候会出现一些问题:

    • 不存在第三个参数时,这么做不安全
    • 第三个参数可能不是有效的32位有符号整数

    使用 testlib,你可以这样写:int a = opt<int>(3)

    同时,你也可以这样:long long b = opt<long long>(2)bool f = opt<bool>(2)string s= opt(4)

    如果你有很多参数需要输入,执行命令类似于这样:g 10 20000 a true,那么将其改成这样会更具有可读性: g -n10 -m200000 -t=a -increment

    在这种情况下,你可以在 generator 中使用如下方法获取参数

    int n = opt<int>("n");
    long long n = opt<long long>("m");
    string t = opt("t");
    bool increment = opt<bool>("increment");
    

    编写命名参数的方案有如下几种:

    1. -key=value--key=value
    2. -key value--key value。这种情况下,value不能以 - 开头
    3. --k12345-k12345 ——如果 key k 是一个字母,且后面是一个数字;
    4. -prop--prop ——启用 bool 属性。
    g1 -n1
    g2 --len=4 --s=oops
    g3 -inc -shuffle -n=5
    g4 --length 5 --total 21 -ord
    

    一些示例

    下面的例子均来自官方示例:testlib/generators at master · MikeMirzayanov/testlib (github.com)

    1. 生成随机整数

    // igen.cpp
    #include "testlib.h"
    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char* argv[])
    {
        registerGen(argc, argv, 1);
        
        cout << rnd.next(1, 1000000) << endl;
    
        return 0;
    }
    

    如果你运行上述程序多次,每一次将会得到相同的结果。如果想生成不同的结果,可以在执行程序时,加入不同的参数。例如分别运行igen.exe 1igen.exe 3 ,将会产生不同的结果。testlib通过传入的参数来设置随机数种子。可以将此方法运用到后面的例子中。

    2. 生成指定范围内的不等分布随机整数

    //iwgen.cpp
    #include "testlib.h"
    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char* argv[])
    {
        registerGen(argc, argv, 1);
    
        cout << rnd.wnext(1, 1000000, opt<int>(1)) << endl;
    
        return 0;
    }
    

    通过参数指定生成的数字是偏大还是偏小,iwgen.exe 1000 将产生偏大的数字,iwgen.exe -1000 将产生偏小的数字。

    3. 生成多组测试数据

    以下内容引用自:Testlib——最强出题辅助工具库

    有两种方法可以一次性生成多组数据:

    1. 写一个批处理脚本。
    2. 使用 Testlib 内置的 startTest(test_index) 函数。

    第一种方法非常简单,只需设好参数,将输出重定向到指定输出文件即可。

    gen 1 1 > 1.in
    gen 2 1 > 2.in
    gen 1 10 > 3.in
    

    对于第二种方法,在每生成一组数据前,调用一次 startTest(test_index),即可将输出重定向至名为 test_index 的文件。

    //multigen.cpp 生成10组100以内的数字对
    #include "testlib.h"
    #include <iostream>
    
    using namespace std;
    
    void writeTest(int test)
    {
        startTest(test);
        
        cout << rnd.next(1, test * test) 
            << " " << rnd.next(1, test * test) << endl;
    }
    
    int main(int argc, char* argv[])
    {
        registerGen(argc, argv, 1);
    
        for (int i = 1; i <= 10; i++)
            writeTest(i);
        
        return 0;
    }
    

    4. 生成字符串

    //sgen.cpp 生成 random token
    #include "testlib.h"
    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char* argv[])
    {
        registerGen(argc, argv, 1);
    
        cout << rnd.next("[a-zA-Z0-9]{1,1000}") << endl;
    
        return 0;
    }
    

    其中 [a-zA-Z0-9] 指定了生成的字符串中所包含的字符,{1,1000} 指定了生成的长度范围。关于类似的写法规范可以参考:Testlib极简正则表达式 - OI Wiki (oi-wiki.org)

    在下面这个例子中,可以使用参数控制生成字符串的长度范围。

    //swgen.cpp
    #include "testlib.h"
    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char* argv[])
    {
        registerGen(argc, argv, 1);
    
        int length = rnd.wnext(1, 1000, opt<int>(1));
        cout << rnd.next("[a-zA-Z0-9]{1,%d}", length) << endl;
    
        return 0;
    }
    

    如果灵活使用参数,可以指定构造字符串。

    下面这个程序通过参数给定的结构来输出字符串。

    Examples:
        gs 1 4 ab => abababab 
        gs 2 5 a 1 b => aaaaab
        gs 3 1 a 5 b 1 a => abbbbba
    
    // gs.cpp
    #include "testlib.h"
    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char* argv[]) {
        registerGen(argc, argv, 1);
        string t;
        int n = opt<int>(1);
        for (int i = 2; i <= 1 + 2 * n; i += 2) {
            int k = opt<int>(i);
            string s = opt<string>(i + 1);
            for (int j = 0; j < k; j++)
                t += s;
        }
        println(t);
    }
    

    5. 生成一棵树

    下面是生成一棵树(无根树)的主要代码,它接受两个参数——顶点数和伸展度。例如,当 (n=10,t=1000)时,可能会生成链;当 (n=10,t=-1000) 时,可能会生成菊花。

    #include "testlib.h"
    #include <bits/stdc++.h>
    
    #define forn(i, n) for (int i = 0; i < int(n); i++)
    
    using namespace std;
    
    int main(int argc, char* argv[])
    {
        registerGen(argc, argv, 1);
    
        int n = opt<int>(1);
        int t = opt<int>(2);
    
        vector<int> p(n);
        forn(i, n)
            if (i > 0)
                p[i] = rnd.wnext(i, t); 
    
        printf("%d
    ", n);
        vector<int> perm(n); 
        forn(i, n)
            perm[i] = i;
        shuffle(perm.begin() + 1, perm.end());
        vector<pair<int,int> > edges;
    
        for (int i = 1; i < n; i++)
            if (rnd.next(2))
                edges.push_back(make_pair(perm[i], perm[p[i]]));
            else
                edges.push_back(make_pair(perm[p[i]], perm[i]));
    
        shuffle(edges.begin(), edges.end());
    
        for (int i = 0; i + 1 < n; i++)
            printf("%d %d
    ", edges[i].first + 1, edges[i].second + 1);
    
        return 0;
    }
    

    如果想要生成一颗无根树,你可以:

    #include "testlib.h"
    
    #include <bits/stdc++.h>
    
    #define forn(i, n) for (int i = 0; i < int(n); i++)
    
    using namespace std;
    
    int main(int argc, char* argv[])
    {
        registerGen(argc, argv, 1);
    
        int n = opt<int>(1);
        int t = opt<int>(2);
    
        vector<int> p(n);
        forn(i, n)
            if (i > 0)
                p[i] = rnd.wnext(i, t);
    
        printf("%d
    ", n);
        vector<int> perm(n);
        forn(i, n)
            perm[i] = i;
        shuffle(perm.begin() + 1, perm.end());
    
        vector<int> pp(n);
        for (int i = 1; i < n; i++)
            pp[perm[i]] = perm[p[i]];
    
        for (int i = 1; i < n; i++)
        {
            printf("%d", pp[i] + 1); // 输出 2 到 n 的每个父亲
            if (i + 1 < n)
                printf(" ");
        }
        printf("
    ");
    
        return 0;
    }
    

    6. 生成一个二分图

    #include "testlib.h"
    
    #include <bits/stdc++.h>
    
    #define forn(i, n) for (int i = 0; i < int(n); i++)
    
    using namespace std;
    
    int main(int argc, char* argv[])
    {
        registerGen(argc, argv, 1);
    
        int n = opt<int>(1);
        int m = opt<int>(2);
        size_t k = opt<int>(3);
    
        int t = rnd.next(-2, 2);
    
        set<pair<int,int> > edges;
    
        while (edges.size() < k)
        {
            int a = rnd.wnext(n, t);
            int b = rnd.wnext(m, t);
            edges.insert(make_pair(a, b));
        }
    
        vector<pair<int,int> > e(edges.begin(), edges.end());
        shuffle(e.begin(), e.end());
    
        vector<int> pa(n);
        for (int i = 0; i < n; i++)
            pa[i] = i + 1;
        shuffle(pa.begin(), pa.end());
    
        vector<int> pb(m);
        for (int i = 0; i < m; i++)
            pb[i] = i + 1;
        shuffle(pb.begin(), pb.end());
    
        println(n, m, e.size());
        forn(i, e.size())
            println(pa[e[i].first], pb[e[i].second]);
    
        return 0;
    }
    
    注:转载请注明出处
  • 相关阅读:
    RRC Server安装配置过程
    开园啦~
    VB.NET 初涉线程的定义和调用
    使用 VB.NET 开发多线程
    多线程 与 单线程 的区别
    Marshal 类的内存操作的一般功能
    VB.NET 内存指针和非托管内存的应用
    OpenProcess() 函数
    DataTable与结构不同实体类之间的转换
    C#的同步和异步调用方法
  • 原文地址:https://www.cnblogs.com/1625--H/p/15036991.html
Copyright © 2020-2023  润新知