最近接触到一个抽奖需求,加上平时玩的暗黑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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
 * @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);

验证代码

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
$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
)
*/

相关文章:

posted on 2014-04-15 11:55  mynetstudy  阅读(1090)  评论(0编辑  收藏  举报