最近接觸到一個抽獎需求,加上平時玩的暗黑3不多掉暗金裝備,就抽空學習下這類機率問題,暫時按網絡稱爲掉寶類型機率。
例如遊戲中戰勝一個boss,會掉落下面其中一個物品,而每一個物品都有必定機率:
1. 靴子 20%
2. 披風 25%
3. 飾品 10%
4. 雙手劍 5%
5. 金幣袋 40%
如今的問題就是如何根據機率掉落一個物品給玩家。php
一. 通常算法:生成一個列表,分紅幾個區間,例如列表長度100,1-20是靴子的區間,21-45是披風的區間等,而後隨機從100取出一個數,看落在哪一個區間。算法時間複雜度:預處理O(MN),隨機數生成O(1),空間複雜度O(MN),其中N表明物品種類,M則由最低機率決定。html
2、離散算法:也就是上面的改進,居然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算法
3、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網絡
![alias-method](http://static.javashuo.com/static/loading.gif)
由上圖方形可獲得兩個數組:
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)spa
PHP實現Alias Method.net
/**
* @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
)