科技·工程:造数据 - 下

前言

好吧,咕了很久的这篇文章的后半部分终于来啦!

上一篇文章链接:科技·工程:构建造数据神器 - 上

在上一篇文章中,我们写好了一个批处理的数据生成器,而在这篇文章中,你将了解到常见的一些数据都是怎么造出来的。

常见数据

有哪些常见的要造的数据呢?无非就是树啊、图啊、多测啊等等,我们今天就先探讨一部分,等我什么时候想到别的了再填坑

随机数

rand 和 srand

这两个函数是 C 中的随机数函数,在头文件 <cstdlib> 下定义,函数原型:

cpp 复制代码
int rand();
void srand( unsigned seed );

rand() 是一个伪随机的函数,返回一个 \([0,RAND\_MAX]\) 中的数。

srand( unsigned seed ) 是用于设置随机种子的,初始的随机种子为 \(1\)。

所以,我们常用 srand(time(0)) 来初始化随机种子,这样就能保证每次运行时随机出来的数不一样。

但是 rand() 有很大的问题,我们来看 cppreference 上的一段话:

......不保证生成的随机数列质量。 过去,一些 rand() 在随机性、分布和产生的序列周期上有严重缺陷(在一个众所周知的例子中,调用之间最低位简单地在 \(1\) 与 \(0\) 间切换)。

对于严肃的随机数生成需求不推荐使用 rand()。推荐用 C++11 的随机数生成设施替换 rand()。 (C++11 起)

由这段文字我们知道,rand() 生成的随机数的质量并不高,日常用用还行,造数据时就应该另寻他法了。

mt19937

这是 C++11 中的一个随机数引擎,也是目前 OI 中常见的生成随机数的工具。

这个随机数引擎采用梅森旋转算法 ,周期长度为可达 \(2^{19937}-1\),这也是它名字的由来。

它的值域大概是整个 \(\text{int}\) 的范围,而且它还有 \(64\) 位的版本------mt19937_64

我们在定义时就设置好随机种子,同样用时间作为随机种子:

cpp 复制代码
std::mt19937_64 rnd(std::chrono::steady_clock::now().time_since_epoch().count());

然后调用时用 rnd() 即可。

我们接下来用的也是这个随机数的引擎。

同时,我们还可以写一个在 \([l,r]\) 之间生成随机数的函数:

cpp 复制代码
ll rand(ll l, ll r) {
    return rnd() % (r - l + 1) + l;
}

打乱

打乱显然可以用 <algorithm> 下的 shuffle 函数,此函数的使用请自行参阅其他语法资料:

cpp 复制代码
template<class RandomIt>
void shuffle(RandomIt first, RandomIt last) {
    std::default_random_engine generator(rnd());
    std::shuffle(first, last, generator);
}

template<class RandomIt>
void shuffle(RandomIt* first, RandomIt* last) {
    std::default_random_engine generator(rnd());
    std::shuffle(first, last, generator);
}

注意到原生指针并不是 class template,所以我们需要对原生指针的情况进行偏特化。

多测

多测的题是很常见的,在多测的题中,通常会给定部分参数的 \(\footnotesize\sum\) 作为数据范围,那么我们就需要【随机出 \(n\) 个和为 \(s\) 的数】。

另外,这些数往往还会有下界 ,这个下界一般都是一个较小的常数,常见的应该就是 \(1\) 到 \(3\)。

所以问题转变为【随机出 \(n\) 个和为 \(s\) 的数,每个数至少为 \(mn\)】。

容易想到随机出前缀和再做一次差分即可,\(mn\) 的限制等价于【随机出来的两个前缀和的差至少为 \(mn\)】,于是 std::set 判重即可。

cpp 复制代码
std::vector<ll> split(ll n, ll s, ll mn = 1) {
    assert(n > 0 && s >= n * mn);
    std::vector<ll> res;
    std::set<ll> st;
    while ((ll) res.size() < n) {
        assert((ll) st.size() < s - mn + 1);
        int tmp = st.empty() ? s : rand(mn, s);
        if (st.insert(tmp).second) {
            res.push_back(tmp);
            for (ll i = 1; i < mn; i++) {
                if(tmp - i >= mn) st.insert(tmp - i);
                if(tmp + i <= s) st.insert(tmp + i);
            }
        }
    }
    std::sort(res.begin(), res.end());
    for (ll i = res.size() - 1; i >= 1; i--) res[i] -= res[i - 1];
    return res;
}

注意,这份代码要求 \(mn\gt1\),不然就无法造出为 \(0\) 的数(被 std::set 判掉了),如有需要,自行改代码/手写。

还有,这份代码是纯靠随机的。也就是说,如果只剩一个可选的数,我们认为要 \(O(s)\) 次才能选到这个数,于是复杂度 \(O(s\log s)\),数据范围太大时完全承受不起。但是我们说过,\(mn\) 一般是一个小常数,况且 \(n\) 的数量级应该会比 \(s\) 小很多(不然每个数就太小了),所以这样子的情况极低,如果真的有需要每个数都要很小那就自己另外写吧。

随机的树怎么造呢?容易想到 \(\text{prufer}\) 序列:先随机出 \(\text{prufer}\) 序列,再 \(\text{prufer to tree}\) 即可,期望深度 \(O(\log n)\),如果需求更复杂可以自行研究/使用别的工具,比如 ouuan 的 \(\text{Tree Generator}\)

cpp 复制代码
std::vector<std::pair<int, int>> tree(int n) {
    assert(n > 0);
    std::vector<std::pair<int, int>> res;
    if (n == 1) return res;
    std::vector<int> prufer(n - 2);
    for (auto &x : prufer) x = rand(1, n);
    std::vector<int> deg(n + 1, 1);
    for (int x : prufer) ++deg[x];
    int cur = 0;
    while (deg[++cur] != 1);
    int leaf = cur;
    for (int x : prufer) {
        res.push_back({x, leaf});
        if (--deg[x] == 1 && x < cur) leaf = x;
        else {
            while (deg[++cur] != 1);
            leaf = cur;
        }
    }
    res.push_back({n, leaf});
    return res;
}

只说连通图,其他自己造。

树上随机加边即可,用 std::set 判掉重边。

可以加个小优化 ,就是当 \(m>\frac{n(n-1)}{2}-(n-1)\) 时,这个时候图非常稠密,随机复杂度很高,建出完全图,接着随机删边即可。(但这样就意味着 \(n\) 很小,好像问题也不大)。

cpp 复制代码
std::vector<std::pair<int, int>> graph(int n, ll m) {
    assert(n > 0);
    assert(m >= n - 1 && m <= n * (n - 1) / 2);
    if(m > n * (n - 1) / 2 - (n - 1)){
    	std::vector<std::pair<int, int>> res;
    	for(int u = 1; u <= n; u++){
    		for(int v = u + 1; v <= n; v++){
    			res.push_back({u, v});
			}
		}
		shuffle(res.begin(),res.end());
		for(int i = m + 1; i <= n * (n - 1) / 2; i++) res.pop_back();
		return res;
	}
    auto res = tree(n);
    std::set<std::pair<int, int>> st;
    for (const auto &e : res) {
        st.insert(e);
        st.insert({e.second, e.first});
    }
    m -= (n - 1);
    while (m--) {
        generate_edge:;
        int u = rand(1, n), v = u;
        while (u == v) v = rand(1, n);
        if (!st.insert({u, v}).second || !st.insert({v, u}).second) goto generate_edge;
        res.push_back({u, v});
    }
    return res;
}

声明

本作品除代码块内的内容以外,其他内容均采用 CC BY-SA 4.0 进行许可,附加条款亦可使用。

本作品中所有代码均受 MIT License 保护,版权声明如下:

MIT License

Copyright (c) 2024 godmoo

Permission is hereby granted, free of charge, to any person obtaining a copy

of this software and associated documentation files (the "Software"), to deal

in the Software without restriction, including without limitation the rights

to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

copies of the Software, and to permit persons to whom the Software is

furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all

copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

SOFTWARE.

若要获取完整代码,请访问:Hint