抽奖 - 算法
目标
- 奖品尽量在活动期间被平均分布(不要在活动一开始就被抽完)
- 大奖尽量在后面才被抽中
- 奖品尽量不要有剩余
- 每个奖品可以设置抽中概率
对于最后一点,因为活动前并不知道参加活动人数,所以设置的概率反而影响了前面几点。如果概率大了可能活动一开始就被抽完,如果太小参与人数不多可能到活动结束之后奖品还余了一半。
其实平均分布才是最关键的。满足了这个条件之后可以根据奖品的数量或概率让奖品在活动时间内靠前或者靠后被抽中。我们来分析一下如何满足这点
分析
要让奖品平均分布在活动时间段内,可以把时间段分成n(奖品总数)段,每一小段时间抽走一个奖品即可 n = (endTime-startTime)/amount
活动结束时间-开始时间 然后除奖品总数就得到时间分片
在每一个时间片段里随机选一个点让用户中奖,随机是为了避免暴露精确的时间点
time(x)=startTime + (x-1)*n + random(n)
第x次抽奖中奖时间点:开始时间+此次抽奖之前的时间片总和+此次抽奖片段内的随机点
实现
实现分成两步
- 随机抽取一个奖品(可以根据奖品数量,概率,奖品重要程度等)
- 判断当前时间片的中奖时间和当前时间的大小,如果大于当前时间则中奖
具体实现看代码:
//随机抽取一个奖品
public static AwardTo randomGetAward(List<AwardTo> awardToList) {
if (awardToList == null || awardToList.isEmpty()) {
return null;
}
double weight = 0;
for (AwardTo awardTo : awardToList) {
//此处用了奖品的概率,实际结果并不好
weight += awardTo.getAwardBalance() * awardTo.getPossible();
}
if (weight == 0) {
return null;
}
//每次的种子数都是奖品的总量,所以只要奖品没被抽中,当前时间片的中奖时间点始终是固定的
Random random = new Random((long) weight);
int num = random.nextInt((int) weight);
for (AwardTo awardTo : awardToList) {
//数量少的奖品在一开始被抽走的概率很小,数量越多越容易被抽中
num -= awardTo.getAwardBalance() * awardTo.getPossible();
if (num < 0) {
return awardTo;
}
}
return null;
}
//判断是否中奖
//LotteryVO为抽奖活动实体对象,包含活动开始,结束时间等
public AwardVO lottery(LotteryVO lottery) {
List<AwardVO> awards = lottery.getAwardList();
long now = System.currentTimeMillis();
long startTime = lottery.getStartTime().getTime();
long endTime = lottery.getEndTime().getTime();
int amount = 0;
int abalance = 0;
for (AwardVO award : awards) {
amount += award.getAwardAmount();
abalance += award.getAwardBalance();
}
AwardVO award = randomGetAward(awards);
//如果当前奖品没被抽走,出奖时间点要求不变,所以需要一个种子。我们在奖品类上保存该奖品上一次被抽中时间
long lastUpdateTime = award.getLastUpdateTime().getTime();
int amount = award.getAwardAmount();
//时间片数
long deltaTime = (endTime - startTime) / amount;
// 使用lastUpdateTime做为随机因子,保证下一个奖品的随机时间对每个抽奖者都一样
Random random = new Random(lastUpdateTime);
// 计算下一个奖品释放时间
long releaseTime = startTime + (amount - abalance)*deltaTime + Math.abs(random.nextLong()) % deltaTime;
// 未达到下一奖品发放时间
if (now < releaseTime) {
return null;
}
return award;
}
如果每次抽奖必中,省略第二步即可。另外可通过调整第一步中的算法实现不同的需求。