隨機數字生成器(RNG)和Hash函數組合武器背後的黑暗祕密

  本文主要的參考文獻爲:
1. 遊戲編程中的數學——隨機數字生成(RNG)的黑暗祕密
2. A Primer on Repeatable Random Numbers

  文章題目之所以叫“黑暗祕密”,只是我覺得這個名字比較酷=。=然而並沒有涉及到太多背後的數學原理,只是對其分佈作了一些有趣的實驗~
  進入正題,在遊戲編程當中,總是會不可避免地用上隨機數生成器(簡稱RNG,Random Number Generators )。但是,RNG 這東西可不是隨便用的,你需要根據實際應用的場景進行一些合理的選擇。比方說,隨機數是否允許重複?
  我相信,絕大多數情況下我們是不希望生成的隨機數出現重複的,比方說,在一些消除類遊戲當中,


這裏寫圖片描述
開心消消樂遊戲截圖

  出現的遊戲方塊都是利用隨機數產生的,然而開發者並不希望遊戲出現太多重複的遊戲方塊,這樣會影響開發者預期的玩家得分流程,甚至出現bug 。總的來說,什麼場景下我們不希望重複隨機數的出現?最主要的一點便是:

  • 不希望玩家能夠重新審視同一世界。例如,在一些沙盒遊戲當中,當你從特定的種子創建出某個世界時, 如果再次使用了相同的種子,則會再次獲得相同的世界。

  我們提到了“種子”這個概念。一般來說,種子可以是intstring 類型的數據,也可以是其它類型的數據,以此作爲輸入獲得一個隨機的輸出。種子最大的特點便是:相同的種子會得到相同的隨機數序列,但當種子發生輕微變化時,得到的隨機數序列便與原來的相比大相徑庭。
  在本文中,我將研究兩種不同方法產生的隨機數分佈:隨機數生成器RNGHash 函數,並嘗試將其結合起來。其中,利用C# 產生數據,利用Matlab 繪製圖形。具體代碼可以參見我的Github 項目:RNGHash 函數相關代碼

RNG

  產生隨機數最常用的方法便是使用隨機數生成器(簡稱RNG )。 許多編程語言(如本文使用的C# )都提供了隨機數的生成方法。
  隨機數生成器根據初始種子產生一系列隨機數。 在許多面向對象的語言中,隨機數生成器通常是一個經種子初始化的對象,然後可以重複調用該對象的方法來產生隨機數。


  在這種情況下,我們可以利用C#Random 類提供的Next() 方法得到0~1000000之間的隨機整數值。之所以選擇1000000作爲最大值,主要是爲了圖形繪製的方便,後面可以看到,我會將每一個隨機數映射到一個邊長爲100單位的立方體上的某個格點上。
  如下所示的圖像,包含由種子0生成的100000個隨機數。其中,每個隨機數表示一個立方體上的一個位置,以行序爲主。立方體每一個格點的像素值範圍爲(0,0,0)→(1,1,1),初始RGB 值爲(0,0,0),每當有一個隨機數落在該處時,該處像素點的RGB 三個通道值均會自增0.1,直到達到1爲止。


這裏寫圖片描述

  這裏需要注意的是如果沒有第一個和第二個隨機數,你就不能得到第三個隨機數。 就其性質而言,RNG 使用先前的隨機數產生每個隨機數作爲計算的一部分。 因此我們研究RNG ,研究的對象都應該是一個隨機序列。


這裏寫圖片描述

  這樣一來,使用RNG 就會有一個明顯的缺點:如果你僅需要一個特定的隨機數(比如序列中的第100個隨機數),那麼你就需要調用Next() 方法26次,並使用最後一個數字,但這是非常不方便的。

爲什麼你需要序列中的特定隨機數?

  如果你一次性生成所有的東西,你可能不需要一個序列中的特定隨機數。然而存在這樣一種情形:
  假設你的世界中有三個部分:A,BC 。其中A 部分使用0號種子生成的隨機數序列前100個隨機數生成。然後玩家進入B 部分,使用第101-200個隨機數生成。此時部分A 被銷燬以釋放內存。最後玩家進入C 區,使用第201-300個隨機數生成。同樣地,部分B 被銷燬。
  然而,如果玩家現在又回到B 部分,那麼爲了使部分B 看起來不變,應該使用與第一次相同的100個隨機數生成。然而因爲A 已經被銷燬,所以我們無法直接得到第101-200個隨機數,需要利用0號種子重新生成整個隨機數序列。

不能只使用具有不同種子值的隨機數生成器嗎?

  需要澄清的是,這是對於RNG 的一個非常常見的誤解。 事實是,儘管同一序列中的不同隨機數字可以說是相互獨立的,但是來自不同隨機序列的相同索引的數字彼此之間並不是獨立的,它們之間可能呈現某種分佈。


這裏寫圖片描述

  所以如果你有100個序列,並從每個序列中取出第i(i1) 個隨機數數,那麼這100個隨機數就不會是相互獨立的隨機數。 它們之間可能會服從某種分佈。
  不妨做一下實驗看看~我將0到99999這100000個數字作爲RNG 的種子,得到100000個隨機序列,分別取每一個隨機序列的第一個隨機數將其映射到一個邊長爲100單位的立方體上,如下圖所示:


這裏寫圖片描述

  顯然此時的模式分佈已經不再均勻,相反呈現出一個直線族!如果分佈已知,那還不如直接用一個線性函數去進行隨機數的生成23333……
  顯然,這樣一種使用具有不同種子值的隨機數生成器的操作是不大可行的。最起碼,我們也應該保證輸出在肉眼上看起來是隨機的,因爲玩家通常不會認真跟你去計算具體的隨機數分佈……想象一下,如果你通過上述操作生成隨機數創建座標,用於在一塊平地上種植樹木。現在,所有的樹木都被被排成一條直線,其餘的平地部分都是空的!相信這會是一件十分尷尬的事囧。
  爲了解決這個問題,我覺得我們有必要引入哈希函數。

Hash 函數

  通常,Hash 函數是將把原空間的一個數據集映射到像空間的一個函數。且像空間一般要比原空間更小,以方便處理。
  與隨機數生成器RNG 不同,使用Hash 函數生成隨機數是不需要隨機序列的,即可以按任意順序得到隨機數,只要你提供了一個輸入~


這裏寫圖片描述

  Hash 函數的設計是非常講究的,其中核心的設計原則便是降低碰撞的概率,常用的做法便是降低card(X)card(X) 的大小,其中X 是原空間,X 是像空間,card(X) 表示X 的勢,card(X) 表示X 的勢,沒學過實變函數的沒關係=。=就理解爲集合的數量即可。顯然,當card(X)=card(X) 的時候我們可以實現無碰撞。
  有一篇文章對於哈希表的數學原理是講得十分透徹的,有興趣的同學可以看看:哈希表之數學原理
  偷懶了一下,我僅僅測試了兩種十分經典的Hash 函數:MD5SHA1 函數。

  • MD5 :這可能是最著名的Hash 函數之一了。 在生成隨機數時,我們通常只需要一個32位int 值作爲返回值,而MD5 會返回一個更大的散列值,其中大部分我們只是扔掉。儘管如此,我覺得MD5 還是值得測試一波的。
  • SHA1 :這也是一個十分經典的Hash 函數,通常返回20字節(160位)的數據摘要。由於它產生的數據摘要的長度更長,因此更難以發生碰撞,因此也更爲安全,但也因此在一定程度上降低了它的運算速度。

  我將0到99999這100000個數字分別作爲MD5SHA1 函數的輸入,得到100000個隨機數,依舊是將其映射到一個邊長爲100單位的立方體上。如下兩圖是MD5SHA1 函數的測試結果:


這裏寫圖片描述
MD5 測試結果


這裏寫圖片描述
SHA1 測試結果

  容易看出,MD5 函數生成的數字具有良好的隨機性,而SHA1 函數生成的數字卻再次呈現出一個直線族的分佈!!!總能與直線扯上關係,細思極恐……相信背後一定有深刻的數學原理,若有時間,會再深入研究一波~
  然而,這並不能說明SHA1 函數不適用於隨機數的生成。因爲在實際操作當中,每生成一個隨機數就得調用一次Hash 函數,這太消耗性能了。。。我們不妨將RNGHash 函數結合起來,這便有了下面的討論。

RNGHash 函數的合體

  RNGHash 函數也可以結合使用。 我們前面已經說到,一個明智的方法是使用不同種子的隨機數生成器,但爲了使輸出結果在肉眼上看起來儘可能地隨機,種子(例如在創建迷宮世界時,種子可以是迷宮位置座標)必須首先通過Hash 函數的處理而非直接使用,否則會使輸出結果呈現出直線族分佈。


這裏寫圖片描述

  再做一波實驗,分別測試一下經MD5 函數和SHA1 函數處理的隨機數生成器效果,如下兩圖所示:


這裏寫圖片描述
MD5 函數處理的隨機數生成器效果


這裏寫圖片描述
SHA1 函數處理的隨機數生成器效果

  可以看出,使用經MD5 函數和SHA1 函數處理的隨機數生成器進行隨機數的生成時,隨機數的分佈已經能夠呈現出不錯的隨機性,這是滿足我們的預期目標的。

總結

  總而言之,如果你需要一堆隨機數,最簡單的方法就是使用隨機數生成器(RNG ),例如C 中的System.Random 類。 爲了使所有的隨機數相互之間是隨機的,要麼只使用一個隨機序列(即用一個種子初始化,但是當需要序列中的特定隨機數時它便顯得不是很方便,具體原因文中已經解釋),要麼使用多個種子,但是在使用這堆種子之前需要先通過一個Hash 函數進行處理。個人還是比較推薦第二種方法的~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章