抽奖案例一:按设置的概率实时抽奖-保证人人平等
抽奖案例一:按设置的概率实时抽奖-保证人人平等
本文介绍了一个抽奖系统的实现方案,重点讨论了如何通过设置概率来保证每次抽奖的公平性。文章详细描述了奖品配置、随机值生成、实现方式以及测试结果,并且使用了Java代码示例来说明具体实现。
核心思想
保证每一次抽奖的概率是一样的,这是一个放回问题(学过概率的朋友们都懂的)
一、奖品配置(本期不含库存,库存下期再讲)
比如我们想要这样配置奖品和概率:
奖品名称 | 中奖概率 |
---|---|
汤臣一品 | 80% |
迈巴赫 | 1% |
1元红包 | 0.01% |
用X轴描述一下我们的奖品分布:
为什么max值是10000呢?因为我允许它小数点后面最多就两位,再调整成整数,就是10000了。
或者说换个方式–对应函数:
可以看到,我们还是有很大机会可以获得汤臣一品的。
二、随机值与概率
从1-10000中间 随机获取一个x值,就可以得到对应f(x)的结果。这个结果就是我们的中奖结果
接下来就是用java来实现这个f(x)。
入参:一个随机值x,一个奖品与概率的mapping关系
出参:f(x)结果
首先我们设置一下奖品和概率,我一个反手就放在redis了(按道理是要放在数据库的,但我不讲道理
(^▽^))
不要额外设置未中奖的概率了,让系统自己算好吗?!
[
{"id":1,"name":"汤臣一品","rate":"0.8"},
{"id":2,"name":"迈巴赫","rate":"0.01"},
{"id":3,"name":"1元红包","rate":"0.0001"}
]
随机数的话 我们用SecureRandom即可,问什么不用Random?
SecureRandom secureRandom = new SecureRandom();
double v = secureRandom.nextDouble();
double ceil = Math.ceil(v * 10000); //等会就用这个随机数来抽奖
三、实现方式
把奖池数据处理成一个TreeMap,为什么要用TreeMap呢 ,等会儿我跟你讲
private TreeMap<Double, Integer> getTreeMap(String poolId) {
//获取奖池中奖品信息
List<Prize> prizes = getPool(poolId);
if (CollectionUtils.isEmpty(prizes)) {
return null;
}
TreeMap<Double, Integer> treeMap = new TreeMap<>();
Double js = 0.0;
for (int i = 0; i < prizes.size(); i++) {
double rate = Math.ceil((Double.valueOf(prizes.get(i).getRate()))* 10000);
treeMap.put(rate + js, prizes.get(i).getId());
js = js + rate;
}
//收个尾
treeMap.put(10000.0, 0);
return treeMap;
}
会得到-----------【手动加粗放大】
{8000.0:1,8100.0:2,8101.0:3,10000.0:0}
再使用tailMap()和firstEntry()去找到对应的value
Integer prizeId = treeMap.tailMap(ceil, false).firstEntry().getValue();
就实现了根据概率抽奖的核心流程,是不是很简单
四、插播----冷知识
插播一段:为什么使用的是TreeMap而不是FlowerMap不是GlassMap
与HashMap相比,TreeMap是一个能比较元素大小的Map集合,TreeMap 是SortedMap接口的一个实现。看的出来sorted,我该怎么跟你解释这个sorted呢?sorted?这个应该很好理解吧?S-O-R-T-E-D
就是排序的(因为TreeMap的key是要实现Comparable接口的,不然的话用不了哦)。
所以能很好的支持我们这个f(x)并且快速找到结果。然后他也实现了NavigableMap,
我们使用到的tailMap
public NavigableMap<K,V> tailMap(K fromKey, boolean inclusive)
其中有两个参数:
fromKey 范围为 [fromKey, +∞) / (fromKey, +∞) 的子视图
inclusive 就是要不要包含fromKey。为true的时候就是[fromKey, +∞) ,为false时是(fromKey, +∞)
这样只是获得了一个子视图,比如我们随机数ceil是8002的话,子视图是8002至+∞。这里对应还有很多value,我们想要的只是:8002往后遇到的第一个键对应的值。所以结合一下firstEntry()这个方法的作用是返回这个映射中最小键(即第一个键)所对应的键值对
咱这日子也是好上了,都用上AI了(无广)
所以以下代码段就实现了我们的随机并保证概率的一次抽奖。
Integer prizeId = treeMap.tailMap(ceil, false).firstEntry().getValue();
关于inclusive的取值,是这样想的👇🏻
有这样一个问题:如果随机数=8000,你希望中的是汤臣一品还是迈巴赫?
(小孩子才做选择,成年的我都要!!!)
醒醒啊!很明显啊 ,应该是迈巴赫的。0-7999有8000个整数,这样才能对应我们一开始的权重分布图。如果随机数8000算汤臣一品的话,那阿汤的概率就变成80.01%了。
—> 因此inclusive取值是false才能保证,随机到8000时,抽到的是阿迈。
五、测试结果
奖品名称 | 10人抽奖结果 | 100人抽奖结果 | 1000人抽奖结果 | 10000人抽奖结果 |
---|---|---|---|---|
汤臣一品 | 10 | 75 | 759 | 8037 |
迈巴赫 | 0 | 0 | 15 | 106 |
1元红包 | 0 | 0 | 0 | 3 |
未中奖 | 0 | 25 | 190 | 1853 |
就是这样,再见