用JavaScript玩轉游戲編程(一)掉寶類型概率

本文轉載自:https://www.cnblogs.com/miloyip/archive/2010/04/21/1717109.html 作者:miloyip 轉載請註明該聲明。

問題定義

遊戲(和一些模擬程序)經常需要使用隨機數,去應付不同的遊戲(或商業)邏輯。本文分析一個常見問題:有N類物件,設第i類物件的出現概率爲P(X=i),如何產生這樣的隨機變量X?

例如對概率的要求是

P(X=0)=0.12
P(X=1)=0.4
P(X=2)=0.4
P(X=3)=0.07
P(X=4)=0.01

輸入數組<0.12, 0.4, 0.4, 0.07, 0.01> 輸出符合以上概率的隨機數序列,如<1, 4, 2, 1, 2, 2, 1, 0, ...> 。

以下先談一些統計學背景知識,再給這問題的可行解法。

概率分佈

這問題要產生一個隨機變量,接近指定的概率分佈(probability distribution)。大部份程序語言都提供接近均勻分佈(uniformly distributed)的僞隨機數產生器(pseudorandom number generator, PRNG),例如JavaScript提供的Math.random()函數,可傳回[0, 1)半開區間的均勻分佈僞隨機數。

密度分佈函數

現在,不仿測試一下JavaScript的Math.random()函數,看看它是否均勻分佈。一個變數的分佈以密度分佈函數(probability density function, PDF)定義,一般寫作f_X(x),隨機變量X在區間[a,b]上的概率爲定積分:

P(a\leq x\leq b)=\int_{a}^{b} f_X(x)dx

爲了把PDF視覺化,可以把X分爲若干區間,統計各區間X出現的頻率,繪畫其直方圖(histogram)。筆者寫了一個簡單的JavaScript框架,用HTML5 Canvas繪畫直方圖。以下測試代碼,可繪畫Math.random()的PDF估值(estimate)。

function step() { var x = Math.random(); var bin = Math.floor(x * frequency.length); frequency[bin]++; sampleCount++; plotPdf(frequency, sampleCount, 1/frequency.length, "Estimated pdf of Math.random (n=" + sampleCount + ")");}var frequency = new Array(10);var sampleCount = 0;for (var i = 0; i < frequency.length; i++) frequency[i] = 0; start("canvas1", step);
RunStop

在統計學中,每個數據稱爲取樣(sample),當取樣數目n越大,可以看到其PDF估值越接近平均。

讀者可以試試,把x的賦值改爲Math.pow(Math.random(), 2)。你會發現,PDF的分佈改變了,其密度更集中於左邊。讀者也可以改爲其他表達式(只要其輸出在[0, 1)的範圍),看看其分佈。如果想看精確一點,也可以加大frequency數組。

累積分佈函數

密度分佈函數,可以變換爲累積分佈函數(cumulative distribution function, CDF),代表隨機變量X小於x的概率:

F_X(x)=P(X\leq x)

在X爲連續(continuous)的情況下,CDF可用PDF定義:

F_X(x)=\int_{-\infty }^{x}f_X(t)dt

在X爲離散(discrete)的情況下,CDF可定義爲:

F_X(x)=\sum_{x_i\leq x}{P(X=x_i)}

以下的pdf2cdf()函數,能把離散的PDF數組,轉換爲CDF數組。由於浮點小數相加會有誤差,最後的值可能少於1,有機會產生bug,函數裏強制指定最後一個元素爲1。

function pdf2cdf(pdf) {
var cdf = pdf.slice();

for (var i = 1; i < cdf.length - 1; i++)
cdf[i] += cdf[i - 1];

// Force set last cdf to 1, preventing floating-point summing error in the loop.
cdf[cdf.length - 1] = 1;

return cdf;
}

以下代碼測試繪畫Math.random()的CDF估值(只把plotPdf改了為plotCdf):

function step() { var x = Math.random(); var bin = Math.floor(x * frequency.length); frequency[bin]++; sampleCount++; plotCdf(frequency, sampleCount, "Estimated cdf of Math.random (n=" + sampleCount + ")");}var frequency = new Array(10);var sampleCount = 0;for (var i = 0; i < frequency.length; i++) frequency[i] = 0; start("canvas2", step);
RunStop

均勻分佈的Math.random(),其CDF估值接近斜線。

題解

這問題其實正式來說,可稱爲模擬離散取樣(simulated discrete sampling),跟據有限類別的指定概率,來模擬取樣。

要製造指定的概率分佈隨機變量,關鍵就是如何把均勻分佈變換。

逆變換取樣

在上節中,顯示了CDF的一些特性,例如CDF的範圍是[0,1],而且是一個單調遞增(monotonic increasing)函數。逆變換取樣(inverse transform sampling)利用了這些特性,去解決這個問題。逆變換取樣方法其實很簡單,給一個目標CDF,只要計算其逆函數(inverse function),就可以把均勻的隨機變數轉換爲目標CDF:

X=F_X^{-1}(Y)

這方法能用在所有CDF(包括連續及離散的)。其數學證明可參考維基百科

下圖顯示這個方法的直觀解讀,在Y軸[0,1]範圍裏均勻取樣(y_i),之後向右和CDF取交點,求交點的X軸位置(x_i),X則是符合CDF的概率分佈。

7b6624b99613af80bd58339e6c308e3d.jpe

這個方法用在離散的情況就更簡單,只需搜尋目標的CDF,找出超過均勻取樣的元素即可。代碼如下:

function discreteSampling(cdf) {
var y = Math.random();
for (var x in cdf)
if (y < cdf[x])
return x;

return -1; // should never runs here, assuming last element in cdf is 1
}

題目的測試:

var targetPdf = [0.12, 0.4, 0.4, 0.07, 0.01];var targetCdf = pdf2cdf(targetPdf);function step() { var bin = discreteSampling(targetCdf); frequency[bin]++; sampleCount++; plotPdf(frequency, sampleCount, 0.4, "Estimated cdf of discreteSampling (n=" + sampleCount + ")");}var frequency = new Array(targetCdf.length);var sampleCount = 0;for (var i = 0; i < frequency.length; i++) frequency[i] = 0; start("canvas3", step);
RunStop

分析

在離散的情況下(本文題目要求),其時間複雜度是O(N),其中N爲類別數目。

讀者可能會注意到,這裏用了線性搜尋(linear search),如果targetPdf數組是由大至小排列,平均而言會更快找到結果。另外,也可以用二分搜尋(binary search),那麼複雜度會降低爲O(lg N),這留給讀者作爲練習。

事實上,這個問題用二分搜尋是標準的方法。那麼,還有沒有更快的方法呢?答案是肯定的,例如別名方法(alias method)、近似方法等,有興趣的讀者可參考[1]。當然,在N很小的情況下,線性搜尋和二分搜尋也足夠。

結語

筆者以前的著作也簡單提及過這個問題,不過本文加入理論背景,希望讀者能更深入瞭解。這個問題在遊戲中經常使用,例如按設計概率產生怪物、寶物,或是用來控制非玩家角色(non-playable character, NPC)的行爲。模擬取樣亦使用在計算機圖形學上,例如粒子系統,或是採用蒙地卡羅積分法(Monte Carlo integration)的渲染算法。後者大概會在不可預計的將來,於另一系列裏探討。

筆者撰寫本文,靈感來自這篇博文。其算法實際上是儲存CDF的逆函數取樣,利用空間和有限的CDF精確度,換取O(1)的時間複雜度。衡量N的大小、精確度、空間需求、緩存延遲後,或許該方法也能適合某些個別需求。但對於該文作者說N最大爲100,二分搜尋只需最多7次迭代,因緩存問題可能二分搜尋更快。有鑑於該文未詳細討論這些需求分析、背後理論、以至代碼可能對一些網友來說比較難理解,希望本文能加以補充。

這系列會探討一些遊戲編程相關的問題,例如隨機相關(PRNG、洗牌、其他隨機取樣方法)、遊戲機制相關(狀態機、細包自動機)等等。網友們也可以提供一些題目,大家互相討論學習。

本文的JavaScript完整程序可在此下載

參考

更新

  • 2010-04-21 感謝livepine,修正問題定義中,數組最後一個值。
  • 2010-04-26 修正下載連結
發佈了0 篇原創文章 · 獲贊 130 · 訪問量 77萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章