抽獎機率-三種算法

最近接觸到一個抽獎需求,加上平時玩的暗黑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

 

由上圖方形可獲得兩個數組:
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
)
相關文章
相關標籤/搜索