學習日常:造資料 - 下

godmoo發表於2024-08-08

前言

好吧,咕了很久的這篇文章的後半部分終於來啦!

上一篇文章連結:科技·工程:構建造資料神器 - 上

在上一篇文章中,我們寫好了一個批處理的資料生成器,而在這篇文章中,你將瞭解到常見的一些資料都是怎麼造出來的。

常見資料

有哪些常見的要造的資料呢?無非就是樹啊、圖啊、多測啊等等,我們今天就先探討一部分,等我什麼時候想到別的了再填坑

隨機數

rand 和 srand

這兩個函式是 C 中的隨機數函式,在標頭檔案 <cstdlib> 下定義,函式原型:

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

我們在定義時就設定好隨機種子,同樣用時間作為隨機種子:

std::mt19937_64 rnd(std::chrono::steady_clock::now().time_since_epoch().count());

然後呼叫時用 rnd() 即可。

我們接下來用的也是這個隨機數的引擎。

同時,我們還可以寫一個在 \([l,r]\) 之間生成隨機數的函式:

ll rand(ll l, ll r) {
    return rnd() % (r - l + 1) + l;
}

打亂

打亂顯然可以用 <algorithm> 下的 shuffle 函式,此函式的使用請自行參閱其他語法資料:

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 判重即可。

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}\)

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\) 很小,好像問題也不大)。

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

相關文章