最近接觸到一個抽獎需求,加上平時玩的暗黑3很少掉暗金裝備,就抽空學習下這類概率問題,暫時按網絡稱為掉寶類型概率。
例如游戲中打敗一個boss,會掉落下面其中一個物品,而每個物品都有一定概率:
1. 靴子 20%
2. 披風 25%
3. 飾品 10%
4. 雙手劍 5%
5. 金幣袋 40%
現在的問題就是如何根據概率掉落一個物品給玩家。
一. 一般算法:生成一個列表,分成幾個區間,例如列表長度100,1-20是靴子的區間,21-45是披風的區間等,然后隨機從100取出一個數,看落在哪個區間。算法時間復雜度:預處理O(MN),隨機數生成O(1),空間復雜度O(MN),其中N代表物品種類,M則由最低概率決定。
二、離散算法:也就是上面的改進,竟然1-20都是靴子,21-45都是披風,那抽象成小於等於20的是靴子,大於20且小於等於45是披風,就變成幾個點[20,45,55,60,100],然后也是從1到99隨機取一個數R,按順序在這些點進行比較,知道找到第一個比R大的數的下標,比一般算法減少占用空間,還可以采用二分法找出R,這樣,預處理O(N),隨機數生成O(logN),空間復雜度O(N)。
請點擊查看詳細:http://www.cnblogs.com/miloyip/archive/2010/04/21/1717109.html
三、Alias Method
Alias Method就不太好理解,實現很巧妙,推薦先看看這篇文章:http://www.keithschwarz.com/darts-dice-coins/
大致意思:把N種可能性拼裝成一個方形(整體),分成N列,每列高度為1且最多兩種可能性,可能性抽象為某種顏色,即每列最多有兩種顏色,且第n列中必有第n種可能性,這里將第n種可能性稱為原色。
想象拋出一個硬幣,會落在其中一列,並且是落在列上的一種顏色。這樣就得到兩個數組:一個記錄落在原色的概率是多少,記為Prob數組,另一個記錄列上非原色的顏色名稱,記為Alias數組,若該列只有原色則記為null。
之前的例子,為了便於演示換成分數
1. 靴子 20% -> 1/4
2. 披風 25% -> 1/5
3. 飾品 10% -> 1/10
4. 雙手劍 5% -> 1/20
5. 金幣袋 40% -> 2/5
然后每個都乘以5(使每列高度為1),再拼湊成方形
拼湊原則:每次都從大於等於1的方塊分出一小塊,與小於1的方塊合成高度為1

由上圖方形可得到兩個數組:
Prob: [3/4, 1/4, 1/2, 1/4, 1]
Alias: [4, 4, 0, 1, null] (記錄非原色的下標)
之后就根據Prob和Alias獲取其中一個物品
隨機產生一列C,再隨機產生一個數R,通過與Prob[C]比較,R較大則返回C,反之返回Alias[C]。
Alias Method 復雜度:預處理O(NlogN),隨機數生成O(1),空間復雜度O(2N)
PHP實現Alias Method
/**
* @desc 拼湊,獲得Prob和Alias數組
* @param array $data
* @param array $prob
* @param array $alias
*/
function init(array $data, array &$prob, array &$alias) {
$nums = count($data);
$small = $large = array();
for ($i = 0; $i < $nums; ++$i) {
$data[$i] = $data[$i] * $nums; // 擴大倍數,使每列高度可為1
/** 分到兩個數組,便於組合 */
if ($data[$i] < 1) {
$small[] = $i;
} else {
$large[] = $i;
}
}
/** 將超過1的色塊與原色拼湊成1 */
while (!empty($small) && !empty($large)) {
$n_index = array_shift($small);
$a_index = array_shift($large);
$prob[$n_index] = $data[$n_index];
$alias[$n_index] = $a_index;
// 重新調整大色塊
$data[$a_index] = ($data[$a_index] + $data[$n_index]) - 1;
if ($data[$a_index] < 1) {
$small[] = $a_index;
} else {
$large[] = $a_index;
}
}
/** 剩下大色塊都設為1 */
while (!empty($large)) {
$n_index = array_shift($large);
$prob[$n_index] = 1;
}
/** 一般是精度問題才會執行這一步 */
while (!empty($small)) {
$n_index = array_shift($small);
$prob[$n_index] = 1;
}
}
/**
* @desc 獲取某種物品
* @param array $prob
* @param array $alias
* @return int
*/
function generation($prob, $alias) {
$nums = count($prob) - 1;
$MAX_P = 100000; // 假設最小的幾率是萬分之一
$coin_toss = rand(1, $MAX_P) / $MAX_P; // 拋出硬幣
$col = rand(0, $nums); // 隨機落在一列
$b_head = ($coin_toss < $prob[$col]) ? TRUE : FALSE; // 判斷是否落在原色
return $b_head ? $col : $alias[$col];
}
$data = array(0.25, 0.2, 0.1, 0.05, 0.4);
$prob = $alias = array();
init($data, $prob, $alias);
$result = generation($prob, $alias);
$count = array(0, 0, 0, 0, 0);
for ($i = 0; $i < 10000; $i++) {
$result = generation($prob, $alias);
$count[$result]++;
}
echo '<pre>';
print_r($count);
echo '</pre>';
/**
Array
(
[0] => 2463
[1] => 1982
[2] => 972
[3] => 507
[4] => 4076
)