C++标准库中的随机数生成
C++標準庫中的隨機數生成
一、偽隨機與真隨機
數字計算機的結果可以說是固定的、必然的。都是根據現有數據的狀態得出接下來的狀態。除非硬件損壞,計算機不會產生真正的隨機和無法預料的事。在生活中隨手拋一個硬幣也受到出手動作、狀態,以及風速等環境影響。一只蟑螂的運動路線也可能不是隨機的。但是在隔絕的環境中一個分子的運動方向可以說是隨機的。在量子力學中的測不準原理和波函數坍縮,都說明隨機是一個概率,這個概率是定值可估計的,但是具體在哪兒卻是隨機的,人類并不能掌控計算它。所以人造出來的計算機也無法實現真正的隨機,僅僅是模擬出來接近隨機的效果,所以叫做偽隨機。即在初始條件一定的情況下,產生的隨機數結果是固定的。在玩許多可以生成隨機地圖的游戲時,很多游戲會提供一個種子值,固定的種子值會生成一模一樣的地圖。所以偽隨機也不一定沒有優點,至少程序員不期望bug的出現是隨機的。在非硬件問題(比如太陽耀斑之比特翻轉)的邏輯結果上,如果出現的結果與推導得出的結果不同,就說明推導過程錯誤。在這一點上,計算機模擬的世界就和人所在的世界大有不同,所以人工智能的什么就再等下輩子了,現有的AI只是建立在數學統計學的模型之上的,從理論上我就覺得不會有多么智能,除非換一種原理出發,比如從大腦怎么運作的,人類如何思考的。
所以,程序上的隨機數一般都是通過生成一個種子值作為初始狀態,再通過遞歸算法根據這個值計算出下一個值。只是按這個算法產生的數字序列分布看起來是隨機的。比如高中的數學課本后面就有偽隨機數表,幾頁的數字序列雖然本身是固定的。但我們可以隨便選一個數字(比如擲骰子),然后取百位往后得到另一個數字。比如百位是8就往后得到往后8個的數字,按這種方法也就取出了新的序列,雖然實際是確定的,但只要起點和規則不同,這個新的數列也可以作為一般的隨機用途了。
二、常用實現
C和C++的許多代碼都如下生成隨機數:
//以距1970年1月1號的0點的秒數作為種子值(注意long轉到了uint) srand(time(0));//返回0至RAND_MAX,RAND_MAX被定義為2147483647 int num = rand();//分布到 [min, max) num = min + num % (max - min);一直以來我都是使用的HGE引擎的算法,如下:
//省略了參數檢查 UINT32 Math::g_seed = 0; int DND::Math::GetRandInt(int min, int max) {g_seed = 214013 * g_seed + 2531011;return min + (g_seed ^ g_seed >> 15) % (max - min + 1); }float DND::Math::GetRandFloat(float min, float max) {g_seed = 214013 * g_seed + 2531011;return min + (g_seed >> 16)*(1.0f / 65535.0f)*(max - min); }void DND::Math::SetSeed(UINT32 seed) {if (!seed) g_seed = (UINT32)time(0);else g_seed = seed; }三、標準庫的使用
在實際的使用中,我嚴重懷疑了上述代碼的可靠性。至于C語言的rand函數我就感覺更有局限性了。于是我決定花點時間用上新一點的、更強大的標準庫實現。標準庫提供了21個概率分布類,我只列舉幾個常用的:
1.離散均勻分布(整數)
//返回[min, max]的概率相等,返回整型//定義種子發生器 random_device rd;//定義隨機數序列生成器rng,傳入無符號整型作為種子,rd()會返回一個隨機值(或許比time(0)好) default_random_engine rng {rd()};//指定類型和范圍 [1, 100] uniform_int_distribution<int> dist {1, 100};//返回隨機數 [1, 100] dist(rng);2.連續均勻分布(實數)
//返回[min, max)的概率相等,返回浮點數,注意前閉后開 //忽略了 序列生成器和種子值//指定類型和范圍 [0, 50) uniform_real_distribution<float> dist {0.0f, 50.0f};//返回隨機數 [0, 50) dist(rng);3.正態分布
也叫高斯分布,它的曲線由期望和期望差決定。期望為所有數據的平均值,也是最有概率出現的值。標準差為方差的算術平方根,代表著隨差值范圍出現概率的變化程度。比如期望為50,標準差為10。那么最可能出現的數字是50。小于40和大于60的概率相同,小于32%。隨機到小于30和大于70的值的概率小于5%。在確定一顆果樹掉落幾顆蘋果的時候,填入(10,2)。那么玩家最可能獲得10個蘋果,然后獲得小于8個或大于12個的概率大概三分之一。當然也有二十分之一概率獲得小于6個或者大于14個。(注意正態分布只支持實數,至于轉換為整數或許四舍五入會更符合需求)
//指定期望和標準差 (10.0, 2.0) normal_distribution<double> dist {10.0, 2.0};//返回10的概率最高 round(dist(rng));4.對數分布?
對數分布與正態分布類似,即隨機變量的對數符合正態分布,即log x符合正態分布,x的概率分布就是對數分布。曲線特點是較快的到達高峰然后拖一條長尾巴。
//指定期望和標準差 (5.0, 0.5) lognormal_distribution<> dist {5.0, 0.5};dist(rng);5.抽樣離散分布?
假設我們解決這樣一個問題:一個農場出現3種不同動物的概率百分比分別是30、30、40,然后一共有N只動物,需要隨機生成每一種。一般想到的代碼實現,只需要用離散均勻分布生成0到99的數字,然后判斷范圍即可知道生成哪一種,然后迭代N次即可。不過當新加一種動物的時候,或者改變某一種動物的出現概率時,就需要改動很多的代碼,并且一大串的if-else看上去也很不美觀。用抽樣分布即可,所有項的概率之和不需要為1,也就是說用權值代替每一項的出現概率。
//指定權值 discrete_distribution<size_t> dist {10, 20, 15, 5, 99};//注意這個返回的范圍是索引 [0, 4] //所以返回4的概率最高,返回3的概率最低 dist(rng);一般來說權值的值和個數應該是動態變化的,如下改變:
//使用新的權值(個數不限) dist.param({2,2,2,3,4,7,9});6.抽樣分段常數分布?
這個分布需要指定兩個列表,一個說明了數軸如何分段,第二個指定了每一段的權值。由于n個值可以分成n-1段,所以第二個列表長度應該是第一個列表長度減一。其中每一段內的數字出現概率相同。
//定義分段,分為了 [0, 60),[60, 90), [90, 100) //連續分布均為前閉后開區間 vector<float> b {0, 60, 90, 100};//指定3段的權值 vector<float> w{7, 5, 9};//應用數據 piecewise_constant_distribution<> d {begin(b), end(b), begin(w)};7.分段線性分布
這個和前一個類似,只不過權值列表多一個,和分段的邊界一一對應,因為它表示的不是區間的權值了,而是區間邊界的權值。意思就是0分出現的權值是7,而60分出現的權值是5,那么30分的權值可計算得出是6。而前一個分段常數分布的權值指的是[0,60)的權值為7。
//定義分段,分為了 [0, 60),[60, 90), [90, 100) //連續分布均為前閉后開區間 vector<float> b {0, 60, 90, 100};//指定4個邊界的權值,區間的權值由邊界權值線性計算 //例如可以假想 30分的權值為 6 vector<float> w{7, 5, 9, 2};//應用數據 piecewise_linear_distribution<> d {begin(b), end(b), begin(w)};8.其它分布
其他分布的類名如下,每一種都有各自適用的模型,使用形式都差不多,想要了解更多的可以再查資料^_^。
泊松分布:poisson_distribution
幾何分布:geometric_distribution
指數分布:exponetial_distribution
伽馬分布:gamma_distribution
威爾分布:weibull_distribution
二項式分布:binomial_distribution
負二項式分布:negative_binomial_distribution
極值分布:extreme_value_distribution
PS:概率密度函數簡稱PDF(Probability Density Function)
9.其他隨機數序列的生成器
看了這么多分布后,我們應該知道了生成一個隨機數大致分為了三步。第一步是設定種子值起點,第二步生成隨機數序列,第三步應用分布。而STL提供的default_random_engine生成器是默認的隨機數序列生成器,它生成隨機的無符號整型序列。雖然是無符號整型,但也可以用到所有分布上。至于分布類是如何完成和保證這些結果正確的,確實有些像變魔術。
所以額外的標準庫還提供了不同的隨機數序列生成器。如下:
線性同余引擎:minstd_rand(32位無符號整型)、minstd_rand0、knuth_b
馬特賽特旋轉算法引擎:mt19937(32位無符號整型)、mt19937_64(64位無符號整型)
帶進位減法引擎:ranlux24_base(24位整數)、anlux48_base(48位整數)、ranlux24、ranlux48
其中minstd_rand是minstd_rand0的改進版本,ranlux24和ranlux48是base版本產生的序列舍棄一大部分得來的。不過我們用default_random_engine默認生成器就夠了。
四、原樣封裝一下
由于已經有很多地方使用了現有的隨機數接口,所以暫時不可能大幅度的修改接口。不過只替換一下實現倒是沒問題,等以后重寫的時候再寫得好看點(再說標準庫實現得這么好,也沒必要封裝了,我也不考慮封裝進dll了,開源就好了)。
首先我們需要定義變量,用于保存種子值、隨機數序列生成器、分布對象(放到函數定義):
//引入頭文件和命名空間 #include <random> using namespace std;class Math { private://種子值static unsigned int g_seed;//隨機數序列生成器static default_random_engine g_random; };//靜態成員需要類外定義 unsigned int Math::g_seed = 0; default_random_engine Math::g_random;?對于種子值,為無符號整型。傳入0時讓程序自動生成隨機種子,有時候我們需要知道隨機產生的種子值是多少,所以有必要緩存一下:
//設置種子 0代表隨機 static void SetSeed(unsigned int s) {if (s == 0){random_device rd;g_random.seed(g_seed = rd());}elseg_random.seed(g_seed = s); } //返回種子值 static unsigned int GetSeed() { return g_seed; }分布的一般化寫法可以寫成模板,將分布對象放入函數作為靜態變量:
//返回[min,max]區間的 整型隨機值 template <typename T> static T GetRandInteger(T min, T max) {static uniform_int_distribution<T> dist_int;//設定區間dist_int.param(uniform_int_distribution<T>::param_type{ min, max });return dist_int(g_random); }//返回[min,max)區間的 實數隨機值 template <typename T> static T GetRandReal(T min, T max) {static uniform_real_distribution<T> dist_real;//設定區間dist_real.param(uniform_real_distribution<T>::param_type{ min, max });return dist_real(g_random); }?這下舊的接口特例化模板即可:
//返回[min,max]區間的隨機int static int GetRandInt(int min, int max) {return GetRandInteger<int>(min, max); }//返回[min,max)區間的隨機float static float GetRandFloat(float min, float max) {return GetRandReal<float>(min, max); }滿足了舊的需求,我再簡單用一下正態分布吧 ,也很簡潔:
//返回 期望mu,標準差sigma 的正態分布隨機值 template <typename T> static T GetRandNormal(T mu, T sigma) {static normal_distribution<T> dist_normal;//設定期望與標準差dist_normal.param(normal_distribution<T>::param_type{ mu, sigma });return dist_normal(g_random); }?五、更新實現后的地圖效果
從HGE的算法改為STL實現后,并沒有出現bug。但是生成的島嶼風格還是有那么一點點的不同,其中必定有一些原因。不過算是如愿以償的解決了隨機數的需求,以后再用隨機數的時候就不用擔心暗藏bug了。
六、蒙特卡洛方法估計圓周率
向一塊正方形區域投擲飛鏢,假設飛鏢隨落在正方形內任意位置的概率相等,那么落在內接圓的概率等于圓面積除以正方形面積。通過統計圓內飛鏢數就可以估算出pi值,這種方法就叫統計模擬方法。
即?,落在圓內的數量比上總數的比值應該為四分之pi。
通過編寫代碼,測出了以下數據:
| 總量 | 圓內 | pi值 |
| 10^1 | 5 | 2 |
| 10^2 | 69 | 2.76 |
| 10^3 | 784 | 3.136 |
| 10^4 | 7900 | 3.16 |
| 10^5 | 78593 | 3.14372 |
| 10^6 | 785144 | 3.14058 |
| 10^7 | 7855222 | 3.14209 |
| 10^8 | 78535828 | 3.14143 |
| 10^9 | 一分鐘內未得出結果 | - |
代碼如下,得出的數據算是符合了。
#include <iostream> #include <random>using namespace std;template <typename T, typename T2> void one_test(unsigned all_num, T& dist, T2& rng) {double x, y;unsigned circle_num = 0;for (unsigned i = 0; i != all_num; ++i){x = dist(rng);y = dist(rng);if (x*x + y*y < 1.0){++circle_num;}}cout << "隨機了 " << all_num << "個點,在圓內的有: " << circle_num << "個" << endl;cout << "估計PI值為: " << 4.0 * circle_num / all_num << endl;cout << endl; }int main() {//隨機種子random_device rd;//序列生成器default_random_engine rng{ rd() };//分布到 [-1.0, 1.0)uniform_real_distribution<double> dist{ -1.0, 1.0 };for (unsigned i = 1; i != 10; ++i){one_test(pow(10, i), dist, rng);}return 0; }不過標準庫還有一種分布叫標準均勻分布(generate_canonical)專門產生[0,1)的連續分布,區別是可以指定尾數比特個數。不過這是一個模板函數,所以如下使用:
template <typename T> void one_test2(unsigned all_num, T& rng) {double x, y;unsigned circle_num = 0;for (unsigned i = 0; i != all_num; ++i){x = generate_canonical<double, 32>(rng) * 2.0 - 1.0;y = generate_canonical<double, 32>(rng) * 2.0 - 1.0;if (x*x + y*y < 1.0){++circle_num;}}cout << "隨機了 " << all_num << "個點,在圓內的有: " << circle_num << "個" << endl;cout << "估計PI值為: " << 4.0 * circle_num / all_num << endl;cout << endl; }| 總量 | 圓內 | pi值 |
| 10^1 | 7 | 2.8 |
| 10^2 | 69 | 2.76 |
| 10^3 | 789 | 3.156 |
| 10^4 | 7898 | 3.1592 |
| 10^5 | 78478 | 3.13912 |
| 10^6 | 785271 | 3.14108 |
| 10^7 | 7853267 | 3.14131 |
| 10^8 | 78539235 | 3.14157 |
| 10^9 | 一分鐘內未得出結果 | - |
?這個標準均勻分布的結果在總量相同的時候,pi的精度也大概高一位,所以還是比前者好。不過梅森旋轉算法被稱為最好的隨機數算法,所以我們也該試一下:
//梅森旋轉隨機數序列生成器 mt19937_64 rng_2{ rd() };for (unsigned i = 1; i != 10; ++i) {one_test2(pow(10, i), rng_2); }這個的結果我就不貼圖了,效果也一般。不過還說有個特別適合蒙特卡洛模擬的生成器(帶進位減法),如下:
//帶進位減法隨機數序列生成器 ranlux48 rng_3{ rd() };//連續均勻分布 for (unsigned i = 1; i != 10; ++i) {one_test(pow(10, i), dist, rng_3); }//標準均勻分布 for (unsigned i = 1; i != 10; ++i) {one_test2(pow(10, i), rng_3); }經測試,無論哪一種分布,在10^6數量生成的時候就超過了十幾秒,并且精度也沒有顯著提高。所以使用默認序列生成器應該能滿足一般的需求。
2019年11月21日,略游
總結
以上是生活随笔為你收集整理的C++标准库中的随机数生成的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【XAudio2】8.怎么播放音效
- 下一篇: 【DND图形库】二、创建控制台窗口和游戏