C++ discrete_distribution離散分佈亂數函數用法詳解

2020-07-16 10:04:28
discrete_distribution 模板定義了返回隨機整數的範圍在 [0,n) 內的分布,基於每個從 0 到 n-1 的可能值的概率權重。權重可以使我們能夠決定為返回值使用何種分布。這種分布通常用返回值來選擇隨機物件,或從可以用索引存取的序列得到的值。序列可以包含任何型別的物件,包括函數物件,因此提供了極大的靈活性。如果想實現一個水果機模擬器,這種分布會有幫助。

必須為生成的值提供一些權重;權重的數量會決定生成的可能值的數目,而且權重的值也被用來決定概率。下面是一個範例,演示了該如何模擬投擲一個面值從 1 到 6 的骰子:
std::discrete_distribution<size_t> d{1, 1, 1, 1, 1, 3}/ // Six possible values std::random_device rd;
std::default_random_engine rng {rd()};
std::map<size_t, size_t> results;   // Store the results of throws
for(size_t go {}; go < 5000; ++go)  // 5000 throws of the die
++results[d(rng)];
for(const auto& pr : results)
    std::cout << "A" << (pr.first+1) << " was thrown " << pr.second << " timesn";
建構函式的初始化列表包含 6 個權值,因此分布只會生成 [0,6) 這個範圍內的值,這意味著只包含從 1 到 5 的值。最後一個權值是其他權值的 3 倍,因此它出現的可能性是其他權值的 3 倍。執行這段程式碼會生成如下內容:

A 1 was thrown 607 times
A 2 was thrown 645 times
A 3 was thrown 637 times
A 4 was thrown 635 times
A 5 was thrown 617 times
A 6 was thrown 1859 times

6 出現的可能性更大。權重是用來表示生成的整數的相對概率的浮點值。每個值的概率是它的權重除以所有權重之和,所以前 5 個值中的每一個的概率都是 1/8,最後一個值的概率是 3/8。下面這條語句也會產生同樣的分布:
std::discrete_distribution<size_t> d{20, 20, 20, 20, 20, 60};
從 0 到 4,每個值的概率為 20/160,也就是 1/8,最後一個值的概率是 60/160 或 3/8。 也可以用序列指定權重。下面是使用相同的亂數生成器的第一個程式碼段的變化版:
std::array<double,6> wts {10.0, 10.0, 10.0, 10.0, 10.0, 30.0};
std::discrete_distribution<size_t> d{std::begin(wts), std::end(wts)};
std::array<string, 6> die_value {"one", "two", "three", "four", "five", "six"};
std::map<size_t, size_t> results;  // Store the results of throws
for(size_t go {}; go < 5000; ++go)  // 5000 throws of the die
    ++results[d(rng)];
for(const auto& pr : results)
    std::cout << " A " << die_value [pr.first] << " was thrown " << pr.second << " timesn";
這裡的權值是從陣列容器中得到的。分布物件生成的值被用來對陣列進行索引輸出。下面是得到的輸出:

A one was thrown 653 times
A two was thrown 601 times
A three was thrown 611 times
A four was thrown 670 times
A five was thrown 600 times
A six was thrown 1865 times

更進一步,可以選擇為 discrete_distribution 物件定義權重來提供一個具有一元函數的建構函式,此建構函式可以從兩個引數值中生成給定個數的權重。這種工作方式有一些複雜,因此我們會一步一步地檢查它。

這個建構函式有 4 個引數:
  • 權重 n 的個數兩個;
  • double 型別的值:xmin 和 xmax,通常被用來計算概率;
  • 一元運算子 op;
xmax 必須大於 xmin,如果 n 是 0,只有值為 1 的概率才會生成。因此在這種情況下,分布總會產生相同的值 0。

增量會被定義為 (xmax - xmin)/n,又稱步進。可以通過執行表示式 op(xmin + (2*k+1)* step/2) 來計算 k 從 0 到 n-1 的概率。

因此權重為:
op(xmin + step/2), op(xmin + 3*step/2), op(xmin + 5*step/2),... op(xmin + (2*n-1)*step/2)
數值的範例可以幫助說明發生了什麼。假設 n 是 6、xmin 是 0、xmax 是 12,因此步進值為 2。如果我們假設定義了使引數翻倍的 op,權重為 2、6、10、14、18、22,概率因此為 1/36、1/12、5/36、7/36、1/4、11/36。下面是這個分布物件的定義:
std::discrete_distribution<size_t> dist {6, 0, 12, [](double v) { return 2*v; }};
一元運算子是由 lambda 表示式定義的,它會返回引數值的兩倍。可以通過呼叫 discrete_distribution 物件的成員函數 probabilities() 來獲取概率。對於 dist 物件可以按如下方式獲取概率:
auto probs = dist.probabilities(); // Returns type vector<double>
std::copy(std::begin(probs), std::end(probs),std::ostream_iterator<double> { std::cout << std::fixed << std:: setprecision (2), " "});
std::cout << std::endl; // Output: 0.03 0.08 0.14 0.19 0.25 0.31
通常,概率的個數是任意的,它和指定的權重的個數對應,因此這裡返回了一個 vector<double> 容器。注釋中顯示的輸出對應於先前展示的分數值。

可以呼叫成員函數 param(),為有不同權重值的 discrete_distribution 物件設定新的概率;權重的個數也可以是不同的:
dist.param({2, 2, 2, 3, 3});    // New set of weights
auto parm = dist.param().probabilities(); std::copy(std::begin(parm), std::end(parm),std::ostream_iterator<double> {std::cout << std::fixed << std::setprecision (2)," "});
std::cout << std::endl;  // Output: 0.17 0.17 0.17 0.25 0.25
第一次呼叫 pamm() 成員函數時,它的引數是一個權重列表,這個列表中的值和原始值不同,值的個數超過之前的。呼叫無引數的 param() 版本會返回一個 param_type 物件,但是並不能準確地知道別名代表的型別是什麼。然而,我們知道它提供了和原始的分布物件相同的成員函數來存取引數。在這個範例中,意味著可以通過呼叫 param_type 物件的成員函數 probabilities() 來得到 param_type 物件中的值。這會返回一個 vector<double> 容器,然後就可以存取它。注釋顯示了它所包含的概率,並且可以看出它們是和新的權值對應的。