奖品类型
抽奖活动,通常有两类奖品:
a.一定数量的奖品(概率=剩余数量/总数)
b.一定概率的奖品(数量无限)
下面有两个example,描述这两类奖品:
一定数量奖品的抽奖策略——抓阄
问题1:假设有 A奖品8个,B奖品2个,如何实现抽奖?
使用抓阄的办法,空间复杂度为O(n),n是奖品的总数量, 时间复杂度为O(1)
一定概率奖品的抽奖策略——掷骰子、轮询随机数组
问题2:假设A奖品概率为1/2,B奖品的概率为1/3,C奖品的概率为1/6,如何实现抽奖?
使用掷骰子的办法,空间复杂度O(m),m是奖品的整数比之和,时间复杂度为O(1)
问题3:假设A奖品概率为0.99, B奖品的概率是0.01,如何实现抽奖?
掷骰子确实是一种理想化的办法,但是实际中,我们并不太好使用它。因为,我们无法保证低概率的奖品被抽中,比如一个概率为0.01的奖品,100次抽奖后,一次也没抽到的概率超过34%,200次抽奖后,一次也没抽到的概率超过13%,这是我们不想看到的。
(实际应用中,我们往往希望能够尽早的公平的让用户抽到低概率价值较高的奖品,这样比较友好。)
轮询法:空间复杂度O(n),n为各个概率整数比之和,时间复杂度O(1)
混合两种奖品类型
问题4:假设有A奖品数量为5,B奖品的概率为1/2,预估最少抽奖次数为10次,如何实现抽奖?
上面的问题,听起来有点荒唐,但实际中,我们会经常碰到这种情况,我们为了控制成本,希望A奖品被抽走5个之后,就不发A奖品了。(有趣的是,在开始宣传的时候,又会称抽到A的概率高达50%)
混合奖品类型,有两种解决思路:
a.数量转换成概率
A奖品的数量为5,最少抽奖次数为10次,则A的最大概率为1/2,这样就能采用上面说的掷骰子(或轮询)的策略实现。
需要考虑的是:A抽完5个以后,不应该再抽到A了,此时需要做一些处理(比如:用一个其他奖品去替代A,或者是直接返回一个默认值,比如谢谢参与)
b.概率转换成数量
B奖品的概率为1/2,最少抽奖次数为10次,则B正常被抽到5次以上,设置B的数量为5,就能采用抓阄的策略实现。
需要考虑的是:抽完10次以后,阄已经抓完,抽奖还在继续,因此需要做一些处理(比如:构建新的奖品和概率,使用新的抽奖策略)
实际应用举例
问题5:产品需求:618端午节抽奖活动,奖品:一千元机票代金券1张,500元酒店代金券3张,机票会员96个,18元免息券无限张概率为33.33%,3天免息券无限张概率33.33%,酒店折扣券无限张概率33.33%,谢谢参与概率0%~0.01%,预估最少抽奖次数100万
方案1:抓阄+掷骰子
将概率的奖品转成对应的数量,初始化100万的奖品并随机,再存储到redis队列中
每次抽奖pop一个,直到所有的pop完了,再采用掷骰子的方式继续抽奖。
这是一个可行的方案,尽管看起来很挫,但是它的实现非常简单,并且投入到实际中使用,并且它能够保证公平性和并发量,且在100万次内,将所有的高价值奖品抽完。
方案2:轮询+抓阄
将数量的奖品转换成概率,由于数量的奖品概率太低,我们把它们合并到一起记作其他奖品,所以现在的概率如下:
按照概率比例随机生成大小为10000的轮询数组,将有数量的奖品随机生成大小为100的队列存储到redis中
当抽到“其他奖品”时,再采用抓阄的策略从redis队列中 pop一个奖品,当队列为空时,返回谢谢参与。
对比
功能对比:
性能对比: