公司最近策划一个红包活动,指定金额指定上下限后随机分发成若干个红包,以抽奖机制抽给员工。拿到这活,首先想到的当然是度娘哈,偷师学艺之后,初步进行代码实现,下面讲述下原理。
主要分两步:红包随机分割和随机抽奖
- 红包随机分割 依据红包个数,分割出可供随机分配的剩余金额,计算出剩余金额每个红包可以分配的最小金额、剩余金额每个红包可以分配的最大金额,获取每个红包可分配的随机值,计算出每个红包的金额:红包下限+可分配随机值,最后打乱红包集 复杂度 o(n)
- 随机抽奖 采用经典的Alias method/别名采样方法 复杂度为 o(1)
下面来看代码:红包分割时,得保证不会出现差异很大红包下限和上限,限值比例(0.25/1.25)可以微调。红包分配完之后,入库存储以供随机抽奖
/**
*
* 金额随机分配,上下限可以不设置
*
* @param cashAmount
* 金额(单位:分)
* @param allocationNumber
* 分配份数
* @param lowerLimit
* 下限(单位:分)、没有下限限制时传入0
* @param upperLimit
* 上限(单位:分)、没有上限限制时传入0
* @throws BusinessErrorException
* @author syuson
* @return ArrayList<Integer> 金额集合,单位分
*/
public static ArrayList<Integer> CashAllocation(Long cashAmount, Long allocationNumber, Long lowerLimit,
Long upperLimit) throws BusinessErrorException {
// 保证不会出现差异很大的红包下限
if (0 == lowerLimit) {
lowerLimit = (long) ((cashAmount / allocationNumber) * 0.25);
}
// 保证不会出现差异很大的红包上限
if (0 == upperLimit) {
upperLimit = (long) ((cashAmount / allocationNumber) * 1.25);
}
// 异常检测
exceptionValidate(cashAmount, allocationNumber, lowerLimit, upperLimit);
// 可供分配
Long remainCashAmount = cashAmount - (lowerLimit * allocationNumber);
ArrayList<Integer> cashList = new ArrayList<Integer>(allocationNumber.intValue());
// 剩余金额每个红包可以分配的最小金额
Long devideAmount = remainCashAmount / allocationNumber;
int randomMinAmount = devideAmount.intValue();
// 剩余金额每个红包可以分配的最大金额
Long subtractAmount = upperLimit - lowerLimit;
int randomMaxAmount = subtractAmount.intValue();
for (int i = 1; i <= allocationNumber; i++) {
Random random = new Random();
int randomAmount = 0;
// 获取每个红包可分配的随机值
if (randomMaxAmount < randomMinAmount || remainCashAmount == 0) {
randomAmount = remainCashAmount.intValue();
} else {
randomAmount = random.nextInt(randomMaxAmount) % (randomMaxAmount - randomMinAmount + 1)
+ randomMinAmount;
}
// 计算出每个红包的金额,红包下限+可分配随机值
cashList.add(lowerLimit.intValue() + randomAmount);
remainCashAmount = remainCashAmount - randomAmount;
if (remainCashAmount <= randomMaxAmount) {
randomMaxAmount = remainCashAmount.intValue();
}
}
// 打乱红包
Collections.shuffle(cashList);
return cashList;
}
异常检测机制
/**
*
* 异常检测
*
* @param cashAmount
* 金额
* @param allocationNumber
* 分配份数
* @param lowerLimit
* 下限
* @param upperLimit
* 上限
* @throws BusinessErrorException
* @author syuson
*/
private static void exceptionValidate(Long cashAmount, Long allocationNumber, Long lowerLimit, Long upperLimit)
throws BusinessErrorException {
if (0 >= cashAmount) {
throw new BusinessErrorException("总金额异常");
}
Long remainCashAmount = cashAmount - lowerLimit * allocationNumber;
if (0 > remainCashAmount) {
throw new BusinessErrorException("分配份数、下限、金额总额异常");
}
if (0 > lowerLimit || 0 >= upperLimit || lowerLimit > upperLimit) {
throw new BusinessErrorException("上限、下限金额异常");
}
Long maxAllocationCash = upperLimit * allocationNumber;
if (cashAmount > maxAllocationCash) {
throw new BusinessErrorException("分配份数、上限、金额总额异常");
}
}
测试:
public static void main(String[] args) throws BusinessErrorException {
// 单个红包下限
Long minAmount = 50L;
// 单个红包上限
Long maxAmount = 400L;
// 需要的红包总数量
Long total = 10000L;
// 可用的总金额(需要介于minAmount*total ,maxAmount*total)
Long totalAmount = 2000000L;
Long startime = (new Date()).getTime();
ArrayList<Integer> cashList = CashAllocation(totalAmount, total, minAmount, maxAmount);
Long endtime = (new Date()).getTime();
Iterator<Integer> cashListIterator = cashList.iterator();
//int i = 0;
BigDecimal hundred = new BigDecimal(100);
BigDecimal sum = new BigDecimal(0);
while(cashListIterator.hasNext()){
BigDecimal cash = new BigDecimal(cashListIterator.next()).divide(hundred, 2, RoundingMode.HALF_DOWN);
//i++;
sum = sum.add(cash);
//System.out.println(String.format("第%s个红包金额:%s元",i,cash));
}
System.out.println("红包总额(元):" + sum);
System.out.println("开始时间(豪秒):" + startime);
System.out.println("结束时间(豪秒):" + endtime);
System.out.println("分配个数:"+cashList.size()+",耗费时间(豪秒):" + (endtime - startime));
}
红包总额(元):20000.00
开始时间(豪秒):1520909221310
结束时间(豪秒):1520909221318
分配个数:10000,耗费时间(豪秒):8
Alias method 即别名采样法,有兴趣的可以参考博文[对点我],大致算法:
- 将整个概率分布拉平成为一个1*N的长方形即为Alias Table。储存两个数组,一个数组里面存着第ii列对应的事件ii矩形站的面积百分比即概率,另一个数组里面储存着第ii列不是事件ii的另外一个事件的标号
- 产生两个随机数,第一个产生1~N 之间的整数i,决定落在哪一列。扔第二次骰子,0~1之间的任意数,判断其与Prab[i]大小,如果小于Prab[i],则采样i,如果大于Prab[i],则采样Alias[i]
/* The random number generator used to sample from the distribution. */
private final Random random;
/* The probability and alias tables. */
private final int[] alias;
private final double[] probability;
/**
* Constructs a new AliasMethod to sample from a discrete distribution and
* hand back outcomes based on the probability distribution.
* <p/>
* Given as input a list of probabilities corresponding to outcomes 0, 1,
* ..., n - 1, this constructor creates the probability and alias tables
* needed to efficiently sample from this distribution.
*
* @param probabilities
* The list of probabilities.
*/
public AliasMethod(List<Double> probabilities) {
this(probabilities, new Random());
}
/**
* Constructs a new AliasMethod to sample from a discrete distribution and
* hand back outcomes based on the probability distribution.
* <p/>
* Given as input a list of probabilities corresponding to outcomes 0, 1,
* ..., n - 1, along with the random number generator that should be used as
* the underlying generator, this constructor creates the probability and
* alias tables needed to efficiently sample from this distribution.
*
* @param probabilities
* The list of probabilities.
* @param random
* The random number generator
*/
public AliasMethod(List<Double> probabilities, Random random) {
/* Begin by doing basic structural checks on the inputs. */
if (probabilities == null || random == null)
throw new NullPointerException();
if (probabilities.size() == 0)
throw new IllegalArgumentException("Probability vector must be nonempty.");
/* Allocate space for the probability and alias tables. */
probability = new double[probabilities.size()];
alias = new int[probabilities.size()];
/* Store the underlying generator. */
this.random = random;
/* Compute the average probability and cache it for later use. */
final double average = 1.0 / probabilities.size();
/*
* Make a copy of the probabilities list, since we will be making
* changes to it.
*/
probabilities = new ArrayList<Double>(probabilities);
/* Create two stacks to act as worklists as we populate the tables. */
Deque<Integer> small = new ArrayDeque<Integer>();
Deque<Integer> large = new ArrayDeque<Integer>();
/* Populate the stacks with the input probabilities. */
for (int i = 0; i < probabilities.size(); ++i) {
/*
* If the probability is below the average probability, then we add
* it to the small list; otherwise we add it to the large list.
*/
if (probabilities.get(i) >= average)
large.add(i);
else
small.add(i);
}
/*
* As a note: in the mathematical specification of the algorithm, we
* will always exhaust the small list before the big list. However, due
* to floating point inaccuracies, this is not necessarily true.
* Consequently, this inner loop (which tries to pair small and large
* elements) will have to check that both lists aren't empty.
*/
while (!small.isEmpty() && !large.isEmpty()) {
/* Get the index of the small and the large probabilities. */
int less = small.removeLast();
int more = large.removeLast();
/*
* These probabilities have not yet been scaled up to be such that
* 1/n is given weight 1.0. We do this here instead.
*/
probability[less] = probabilities.get(less) * probabilities.size();
alias[less] = more;
/*
* Decrease the probability of the larger one by the appropriate
* amount.
*/
probabilities.set(more, (probabilities.get(more) + probabilities.get(less)) - average);
/*
* If the new probability is less than the average, add it into the
* small list; otherwise add it to the large list.
*/
if (probabilities.get(more) >= 1.0 / probabilities.size())
large.add(more);
else
small.add(more);
}
/*
* At this point, everything is in one list, which means that the
* remaining probabilities should all be 1/n. Based on this, set them
* appropriately. Due to numerical issues, we can't be sure which stack
* will hold the entries, so we empty both.
*/
while (!small.isEmpty())
probability[small.removeLast()] = 1.0;
while (!large.isEmpty())
probability[large.removeLast()] = 1.0;
}
/**
* Samples a value from the underlying distribution.
*
* @return A random value sampled from the underlying distribution.
*/
public int next() {
/* Generate a fair die roll to determine which column to inspect. */
int column = random.nextInt(probability.length);
/* Generate a biased coin toss to determine which option to pick. */
boolean coinToss = random.nextDouble() < probability[column];
/* Based on the outcome, return either the column or its alias. */
/*
* Log.i("1234","column="+column); Log.i("1234","coinToss="+coinToss);
* Log.i("1234","alias[column]="+coinToss);
*/
return coinToss ? column : alias[column];
}
/** 匹配、选中 */
private final static String RANDOM_MATCHED = "RandomMatched";
/** 未匹配、未选上 */
private final static String NO_RANDOM_MATCHED = "NoRandomMatched";
/**
* 随机匹配抽奖
*
* @param probability
* 匹配抽奖中奖概率
* @return true:中奖/false:未中奖
*/
public static Boolean SystemRandomMatche(Double probability) {
if (1 < probability)
probability = 0.1;
TreeMap<String, Double> map = new TreeMap<String, Double>();
map.put(RANDOM_MATCHED, probability);
map.put(NO_RANDOM_MATCHED, 1.0 - probability);
List<Double> list = new ArrayList<Double>(map.values());
List<String> gifts = new ArrayList<String>(map.keySet());
AliasMethod method = new AliasMethod(list);
String key = gifts.get(method.next());
if (RANDOM_MATCHED.equalsIgnoreCase(key.trim()))
return true;
return false;
}
来看看测试:
public static void main(String[] args) {
boolean showFlag = true;
if(!showFlag){
Double probability = 0.35;
int exeNum = 10;
for (int j = 0; j < exeNum; j++) {
System.out.println(String.format("抽奖概率为:%s,抽奖结果为:%s", probability, SystemRandomMatche(probability)?"中奖":"谢谢参与"));
}
}
if (showFlag) {
TreeMap<String, Double> map = new TreeMap<String, Double>();
map.put("1级晶石", 0.25);
map.put("10药水", 0.25);
map.put("5钱袋", 0.20);
map.put("1饭盒", 0.1);
map.put("2饭盒", 0.1);
map.put("1碎片", 0.096);
map.put("30碎片", 0.002);
map.put("6666钻", 0.002);
List<Double> list = new ArrayList<Double>(map.values());
List<String> gifts = new ArrayList<String>(map.keySet());
AliasMethod method = new AliasMethod(list);
Map<String, AtomicInteger> resultMap = new HashMap<String, AtomicInteger>();
for (String in : map.keySet()) {
resultMap.put(in, new AtomicInteger());
}
int exeNum = 15;
for (int i = 0; i < exeNum; i++) {
int index = method.next();
String key = gifts.get(index);
if (!resultMap.containsKey(key)) {
resultMap.put(key, new AtomicInteger());
}
resultMap.get(key).incrementAndGet();
}
System.out.println("抽奖次数:"+ exeNum);
for (String key : resultMap.keySet()) {
System.out.println(String.format("[%s,设定概率:%s],实际抽中次数:%s",key,map.get(key),resultMap.get(key)));
}
}
}
抽奖次数:15
[30碎片,设定概率:0.002],实际抽中次数:0
[10药水,设定概率:0.25],实际抽中次数:2
[2饭盒,设定概率:0.1],实际抽中次数:3
[1碎片,设定概率:0.096],实际抽中次数:0
[1饭盒,设定概率:0.1],实际抽中次数:1
[6666钻,设定概率:0.002],实际抽中次数:0
[5钱袋,设定概率:0.2],实际抽中次数:5
[1级晶石,设定概率:0.25],实际抽中次数:4
抽奖机制:给定概率值(随机生成一个),通过Alias method获知是否抽中,抽中之后去之前存储红包的库中,随机抽一个红包,如果没中,很遗憾~
private Double generateProbability(int liseSize) {
BigDecimal probability = new BigDecimal(0.19);
if (3 < liseSize) {
return probability.doubleValue();
}
return (BigDecimal.ONE.subtract(probability.multiply(new BigDecimal(liseSize))))
.setScale(2, RoundingMode.HALF_UP).subtract(probability).doubleValue();
}
// 领用过该红包并且尚未使用的用户需进行随机匹配,次数越多概率越低
int listSize = bonus2UserListSize(eventId, userId);
if (listSize >= 1) {
double probability = generateProbability(listSize);
String msg = String.format("该用户%s(%s)已领用红包,此次随机匹配概率为:%s", user.getNickName(), user.getAuthUid(),
probability);
logger.debug(msg);
if (!AliasMethod.SystemRandomMatche(probability)) {
return OperationResult.buildFailureResult(msg);
}
}
有啥问题希望大家留言,一起学习~