一、概述
平时,经常会遇到权重随机算法,从不同权重的N个元素中随机选择一个,并使得总体选择结果是按照权重分布的。如广告投放、负载均衡等。
如有4个元素A、B、C、D,权重分别为1、2、3、4,随机结果中A:B:C:D的比例要为1:2:3:4。
总体思路:累加每个元素的权重A(1)-B(3)-C(6)-D(10),则4个元素的的权重管辖区间分别为[0,1)、[1,3)、[3,6)、[6,10)。然后随机出一个[0,10)之间的随机数。落在哪个区间,则该区间之后的元素即为按权重命中的元素。
实现方法:
利用TreeMap,则构造出的一个树为:
B(3)
/ \
/ \
A(1) D(10)
/
/
C(6)
然后,利用treemap.tailMap().firstKey()即可找到目标元素。
当然,也可以利用数组+二分查找来实现。
二、源码
package com.xxx.utils; import com.google.common.base.Preconditions; import org.apache.commons.math3.util.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.SortedMap; import java.util.TreeMap; public class WeightRandom<K,V extends Number> { private TreeMap<Double, K> weightMap = new TreeMap<Double, K>(); private static final Logger logger = LoggerFactory.getLogger(WeightRandom.class); public WeightRandom(List<Pair<K, V>> list) { Preconditions.checkNotNull(list, "list can NOT be null!"); for (Pair<K, V> pair : list) { Preconditions.checkArgument(pair.getValue().doubleValue() > 0, String.format("非法权重值:pair=%s", pair)); double lastWeight = this.weightMap.size() == 0 ? 0 : this.weightMap.lastKey().doubleValue();//统一转为double this.weightMap.put(pair.getValue().doubleValue() + lastWeight, pair.getKey());//权重累加 } } public K random() { double randomWeight = this.weightMap.lastKey() * Math.random(); SortedMap<Double, K> tailMap = this.weightMap.tailMap(randomWeight, false); return this.weightMap.get(tailMap.firstKey()); } }
三、性能
4个元素A、B、C、D,其权重分别为1、2、3、4,运行1亿次,结果如下:
元素 | 命中次数 | 误差率 |
A | 10004296 | 0.0430% |
B | 19991132 | 0.0443% |
C | 30000882 | 0.0029% |
D | 40003690 | 0.0092% |
从结果,可以看出,准确率在99.95%以上。
四、另一种实现
利用B+树的原理。叶子结点存放元素,非叶子结点用于索引。非叶子结点有两个属性,分别保存左右子树的累加权重。如下图:
看到这个图,聪明的你应该知道怎么随机了吧。
此方法的优点是:更改一个元素,只须修改该元素到根结点那半部分的权值即可。
【项目实战】——Java根据奖品权重计算中奖概率实现抽奖(适用于砸金蛋、大转盘等抽奖活动)
双蛋节(圣诞+元旦)刚刚过去,前几天项目上线的砸金蛋活动也圆满结束。
现在在许多网站上都会有抽奖的活动,抽奖的算法也是多种多样,这里介绍一下如何根据每种奖品的权重来抽奖,适用于多种抽奖形式。
奖品设置
比如现在举行一次砸金蛋活动中,奖品如下:
奖品够丰富的哇,香车美女豪宅都有了~不过由于法律的原因,活人是不能赠送的,所以一等奖只能送海报了~
奖品在数据库中的存储情况
抽奖实现
奖品实体 Prize.java
public class Prize { private int id;//奖品id private String prize_name;//奖品名称 private int prize_amount;//奖品(剩余)数量 private int prize_weight;//奖品权重 //getter、setter
这里只考虑最简单的抽奖实现,所以暂时只为奖品设计如上4个字段。
见注释,prize_name表示奖品名称;prize_amount表示奖品数量,即本次抽奖活动计划发放此奖品的数量;prize_weight表示奖品权重,表示奖品被抽到的几率的比重,权重越大,被抽到的几率越大,比如本次砸金蛋活动有4种奖品,权重分别是1、2、3、4,总权重是10,那么每种奖品被抽到的几率就是1/10,2/10,3/10,4/10。
核心算法:
/** * 根据Math.random()产生一个double型的随机数,判断每个奖品出现的概率 * @param prizes * @return random:奖品列表prizes中的序列(prizes中的第random个就是抽中的奖品) */ public int getPrizeIndex(List<Prize> prizes) { DecimalFormat df = new DecimalFormat("######0.00"); int random = -1; try{ //计算总权重 double sumWeight = 0; for(Prize p : prizes){ sumWeight += p.getPrize_weight(); } //产生随机数 double randomNumber; randomNumber = Math.random(); //根据随机数在所有奖品分布的区域并确定所抽奖品 double d1 = 0; double d2 = 0; for(int i=0;i<prizes.size();i++){ d2 += Double.parseDouble(String.valueOf(prizes.get(i).getPrize_weight()))/sumWeight; if(i==0){ d1 = 0; }else{ d1 +=Double.parseDouble(String.valueOf(prizes.get(i-1).getPrize_weight()))/sumWeight; } if(randomNumber >= d1 && randomNumber <= d2){ random = i; break; } } }catch(Exception e){ System.out.println("生成抽奖随机数出错,出错原因:" +e.getMessage()); } return random; }
抽奖的逻辑可以用下面这张图表示:
分析:如上图,为了便于计算和理解,设置每种奖品的权重分别为1,2,3,4,所以被抽到的概率分别为0.1,0.2,0.3,0.4(本次活动中奖概率为100%)。
先生成一个随机数randomNumber,然后根据随机数所处区域判断奖品:
0<randomNumber<=0.1 表示抽中一等奖 0.1<randomNumber<=0.3 表示抽中二等奖 0.3<randomNumber<=0.6 表示抽中三等奖 0.6<randomNumber<=1.0 表示抽中四等奖
抽奖测试
public static void main(String[] agrs) { int i = 0; PrizeMathRandom a = new PrizeMathRandom(); int[] result=new int[4]; List<Prize> prizes = new ArrayList<Prize>(); Prize p1 = new Prize(); p1.setPrize_name("范冰冰海报"); p1.setPrize_weight(1);//奖品的权重设置成1 prizes.add(p1); Prize p2 = new Prize(); p2.setPrize_name("上海紫园1号别墅"); p2.setPrize_weight(2);//奖品的权重设置成2 prizes.add(p2); Prize p3 = new Prize(); p3.setPrize_name("奥迪a9"); p3.setPrize_weight(3);//奖品的权重设置成3 prizes.add(p3); Prize p4 = new Prize(); p4.setPrize_name("双色球彩票"); p4.setPrize_weight(4);//奖品的权重设置成4 prizes.add(p4); System.out.println("抽奖开始"); for (i = 0; i < 10000; i++)// 打印100个测试概率的准确性 { int selected=a.getPrizeIndex(prizes); System.out.println("第"+i+"次抽中的奖品为:"+prizes.get(selected).getPrize_name()); result[selected]++; System.out.println("--------------------------------"); } System.out.println("抽奖结束"); System.out.println("每种奖品抽到的数量为:"); System.out.println("一等奖:"+result[0]); System.out.println("二等奖:"+result[1]); System.out.println("三等奖:"+result[2]); System.out.println("四等奖:"+result[3]); }
尝试抽奖10000次的结果如下:
一等奖:962 二等奖:2007 三等奖:3043 四等奖:3988
类奖品获奖次数比例刚好大约为1:2:3:4,学过概率的你肯定知道抽奖次数越多,测试结果越准确~
Tips:
如果计划中奖率是100%的话,那么10个奖品只能抽奖10次,所以还要根据实际情况设置每种奖品数量和权重。
如果需要设置中奖率不为100%,可以添加一个“伪奖品”,并为其设置权重,那么抽到这个“伪奖品”的概率就是不中奖的概率。
如果在抽奖过程中某类奖品抽完了,可以做个判断,如果此奖品的剩余数量为0,则重新抽取奖品,直到抽到其他奖品位置。
参考文章: