抽獎概率-三種算法


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

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)

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
)



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM