掘金 后端 ( ) • 2024-07-01 09:58

1、前言

提到抽奖想必所有人都不陌生,生活中到处都能遇到。那么如何实现随机抽奖呢?又如何实现奖品在指定区域才能抽到呢?

2、实现思路

此次抽奖采用的方式为“权重抽奖”。 那么什么是“权重抽奖”呢? 权重抽奖是抽奖方式中的一种,其中参与抽奖的每个人都有一个与他们的参与度或贡献度相对应的权重值。通常其权重值越大,就意味着在抽奖中获得奖品机会就越大。所以同理可得,奖品分配的权重越大,其抽中的概率越高,分配的权重越小,抽中的概率越小。

image.png 上图中,我们暂且称0,20,60,110,125为节点,那么节点0-20为奖品1所在区域,20-60为奖品2所在区域,60-110为奖品3所在区域,110-125为奖品4所在区域。此时,生成一个0-125的随机数x,那么x的值在哪个奖品的区间内,抽中的就是哪个奖品。

那么又如何实现奖品在指定区域出现呢?

此时,我们只需要给指定奖品给予一个阈值。比如,我们一共有1000个奖品,共有:一等奖、二等奖、三等奖、四等奖。此时我们需要一等奖在65%~70%之间才能被抽取,我们就可以给一等奖一个起始阈值650,结束阈值700就可以了。

3、代码实现

Tip:RandomUtil来自hutool第三方库。

(1)权重抽奖

创建奖品对象

package com.cashier.commodity.gift;

import lombok.Data;

/**
 * @author wangf
 * @date 2024/6/19
 */
@Data
public class Prize1 {
    public Prize1(Integer id, String name, Integer weight, Integer number) {
        this.id = id;
        this.name = name;
        this.weight = weight;
        this.number = number;
    }


    /**
     * id
     */
    private Integer id;
    /**
     * 奖品名称
     */
    private String name;
    /**
     * 权重
     */
    private Integer weight;
    /**
     * 奖品库存
     */
    private Integer number;
}

抽奖方法

public static Prize lottery(List<Prize> prizeList) {
    // 按照权重从小到大排列奖品
    prizeList.sort(Comparator.comparingInt(Prize::getWeight));
    //计算节点 节点的数量比奖品的数量多一个,即0
    List<Integer> nodeList = new ArrayList<>();
    // 第一个节点为0
    nodeList.add(0);
    for (Prize prize : prizeList) {
        //每一个节点等于前一个节点+当前权重
        nodeList.add(nodeList.get(nodeList.size()- 1)  + prize.getWeight());
    }
    // 生成0-结束节点的随机数
    int randomInt = RandomUtil.randomInt(0, nodeList.get(nodeList.size() -1));
    // 如果为0 则表示奖品库存为空
    if (nodeList.get(nodeList.size() -1) == 0) {
        return null;
    }
    // 最终抽奖逻辑
    for (int i = 1; i < nodeList.size(); i++) {
        // 本次奖品
        Prize prize = prizeList.get(i - 1);
        // 本次节点
        Integer endNode = nodeList.get(i);
        //前一个节点
        Integer startNode = nodeList.get(i - 1);
        if (randomInt >= startNode && randomInt < endNode){
            // 查看是否还有库存
            if (prize.getNumber() == 0) {
                prizeList.remove(i - 1);
                return lottery(prizeList);
            }
            // 抽中后数量递减
            prize.setNumber(prize.getNumber() - 1);
            return prize;
        }
    }
    throw new RuntimeException("程序异常");
}

创建模拟数据验证概率

public static void main(String[] args) {
    List<Prize> prizeList = new ArrayList<>();
    prizeList.add(new Prize(0, "一等奖", 0, 0));
    prizeList.add(new Prize(0, "二等奖", 0, 0));
    prizeList.add(new Prize(0, "三等奖", 1 ,2));
    prizeList.add(new Prize(0, "四等奖", 1, 2));
    prizeList.add(new Prize(0, "五等奖", 300,295));
    prizeList.add(new Prize(0, "六等奖", 300,295));
    prizeList.add(new Prize(0, "七等奖", 380,380));
    prizeList.add(new Prize(0, "八等奖", 18,26));
    // 进行1000次抽奖验证概率
    List<Prize> lotteryResult = new ArrayList<>();
    for (int i = 1; i<=1000; i++) {
        Prize prize = lottery(prizeList);
        if (prize != null) {
            lotteryResult.add(prize);
        }
    }
    Map<String, List<Prize>> collect = lotteryResult.stream().collect(Collectors.groupingBy(Prize::getName));
    collect.forEach((k, v) -> System.out.println(k + "被抽中" + v.size() + "次"));
}

结果

三等奖被抽中2次
七等奖被抽中380次
五等奖被抽中295次
八等奖被抽中26次
六等奖被抽中295次
四等奖被抽中2次

(2)指定区域抽中奖品

创建奖品对象

package com.cashier.commodity.gift;

import lombok.Data;

/**
 * @author wangf
 * @date 2024/6/19
 */
@Data
public class Prize1 {
    public Prize1(Integer id, String name, Integer weight, Integer number) {
        this.id = id;
        this.name = name;
        this.weight = weight;
        this.number = number;
    }


    /**
     * id
     */
    private Integer id;
    /**
     * 奖品名称
     */
    private String name;
    /**
     * 权重
     */
    private Integer weight;
    /**
     * 奖品库存
     */
    private Integer number;
}

设置周期阈值、次数,用来循环,避免出现第一轮奖品比如1000个,抽完后1001个人无法抽奖。设置指定奖品的阈值,用来指定该奖品出现区域。

/***
 * 总次数
 */
private static int countTotal = 1;

/***
 * 周期次数
 */
private static int count = 1;

/***
 * 周期阈值 100%
 */
private static int thresholdCount = 1000;

/***
 * 奖池
 */
private static List<Prize> prizeList = new ArrayList<>();

/***
 * 三等奖出现阈值 85%-90%
 */
private static int thirdPrizeStartThreshold = 850;
private static int thirdPrizeEndThreshold = 900;
private static Integer thirdId = 3;

/***
 * 四等奖出现阈值 65%-70%
 */
private static int fourthPrizeStartThreshold = 650;
private static int fourthPrizeEndThreshold = 700;
private static Integer fourthId = 4;

核心抽奖方法

public static Prize lottery(List<Prize> prizeList) {
    // 过滤掉奖池中库存等于 0 的奖品
    Iterator<Prize> iterator = prizeList.iterator();
    while (iterator.hasNext()) {
        Prize prize = iterator.next();
        if (Objects.equals(prize.getNumber(), 0)) {
            iterator.remove(); // 删除元素
        }
    }
    // 如果奖池为空则直接返回
    if (CollUtil.isEmpty(prizeList)) {
        return null;
    }
    // 判断是否加入四等奖
    if (count == fourthPrizeStartThreshold) {
        prizeList.add(new Prize(fourthId, "奖品4", 100, 1));
    }
    // 判断是否加入三等奖
    if (count == thirdPrizeStartThreshold) {
        prizeList.add(new Prize(thirdId, "奖品3", 100, 1));
    }
    // 判断当前次数如达到四等奖结束阈值则直接返回
    if (count == fourthPrizeEndThreshold) {
        for (Prize prize : prizeList) {
            // 如果当前四等奖项还存在,到达结束阈值则直接返回奖项
            if (Objects.equals(fourthId, prize.getId())) {
                prize.setNumber(prize.getNumber() - 1);
                count++;
                countTotal++;
                return prize;
            }
        }
    }
    // 判断当前次数如达到三等奖结束阈值则直接返回
    if (count == thirdPrizeEndThreshold) {
        for (Prize prize : prizeList) {
            // 如果当前三等奖项还存在,到达结束阈值则直接返回奖项
            if (Objects.equals(thirdId, prize.getId())) {
                prize.setNumber(prize.getNumber() - 1);
                count++;
                countTotal++;
                return prize;
            }
        }
    }
    // 按照权重从小到大排序奖品
    prizeList.sort(Comparator.comparingInt(Prize::getWeight));
    // 计算节点 节点的数量比奖品的数量多一个,即 0
    List<Integer> nodeList = new ArrayList<>();
    //第一个节点为 0
    nodeList.add(0);
    for (Prize prize : prizeList) {
        // 每一个节点等于前一个节点+当前奖品的权重
        nodeList.add(nodeList.get(nodeList.size() - 1) + prize.getWeight());
    }
    // 生成 0-结束节点 的随机数
    Integer currentNode = nodeList.get(nodeList.size() - 1);
    // 递归结束点,如果都为0则代表奖品池已清空
    if (currentNode == 0) {
        return null;
    }
    int randomInt = RandomUtil.randomInt(0, currentNode);
    // 最终抽奖逻辑 此处需要从第二个节点开始遍历
    for (int i = 1; i < nodeList.size(); i++) {
        // 当前奖品
        Prize prize = prizeList.get(i - 1);
        // 本次节点
        Integer endNode = nodeList.get(i);
        // 前一个节点
        Integer startNode = nodeList.get(i - 1);
        // 若随机数大于等于前一个节点并且小于本节点,在prizeList中位于i-1位置的奖品为抽中奖品
        // Tip:比较大小时,左闭右开与左开右闭都可以,不影响整体概率
        if (randomInt >= startNode && randomInt < endNode) {
            // 判断当前奖品是否还有库存,没有则重新生成随机数
            if (prize.getNumber() == 0) {
                prizeList.remove(i - 1);
                return lottery(prizeList);
            }
            // 抽中奖品,数量递减
            prize.setNumber(prize.getNumber() - 1);
            // 判断如果达到一个周期后,将周期次数归 1
            if (countTotal % thresholdCount == 0) {
                count = 1;
                // 重置奖池
                prizeList = initPrize();
            }
            count++;
            countTotal++;
            return prize;
        }
    }
    throw new RuntimeException("程序异常 生成的随机数不在任何奖品区间内");
}

初始化奖池

public static List<Prize> initPrize() {
    prizeList.add(new Prize(1, "奖品1", 0, 0));
    prizeList.add(new Prize(2, "奖品2", 0, 0));
    prizeList.add(new Prize(5, "奖品5", 300, 300));
    prizeList.add(new Prize(6, "奖品6", 300, 300));
    prizeList.add(new Prize(7, "奖品7", 380, 380));
    prizeList.add(new Prize(8, "奖品8", 18, 18));
    return prizeList;
}

模拟5000人抽奖验证概率

public static void main(String[] args) {
    int drawCount = 5000;
    System.out.println("==========================当前抽奖次数为:" + drawCount + "次=============================");
    List<Prize> prizeList = initPrize();
    //进行五千次抽奖验证概率
    List<Prize> lotteryResult = new ArrayList<>();
    for (int i = 1; i <= drawCount; i++) {
        Prize prize = lottery(prizeList);
        if (prize != null) {
            lotteryResult.add(prize);
        }
        System.out.println("在抽奖过程中 => " + prize + ",在第【" + i + "】次被抽中");
    }
    Map<String, List<Prize>> collect = lotteryResult.stream().collect(Collectors.groupingBy(Prize::getName));
    System.out.println("===================本批次抽奖结果为======================");
    collect.forEach((k, v) -> System.out.println(k + " 被抽中 " + v.size() + " 次"));
}

结果

===================本批次抽奖结果为======================
奖品4 被抽中 5 次
奖品3 被抽中 5 次
奖品6 被抽中 1500 次
奖品5 被抽中 1500 次
奖品8 被抽中 90 次
奖品7 被抽中 1900 次

本文章部分内容借鉴与作者:zyan1226 的文章《 使用 Java 实现指定概率的抽奖 》 链接:https://juejin.cn/post/7178693368506449976?searchId=2024062816333723E92D877D636A679D1E

相关内容