如何实现抢红包算法,怎样才能做到随机公平

本文探讨了三种不同的抢红包算法实现,包括基础的非公平实现、二倍均值法和线段切割法。基础实现中,先抢者优势明显;二倍均值法保证每次随机金额的平均值相等;线段切割法则通过随机切割点确保更公平的分配。每种方法都有其优缺点,线段切割法更能达到公平目标。
摘要由CSDN通过智能技术生成

🧧问题描述

例如一个人在群里发了10块钱的红包,群里有5个人一起来抢红包,每人抢到的金额随机分配。

红包功能需要满足哪些具体规则呢?

  1. 所有人抢到的金额之和要等于红包金额,不能多也不能少
  2. 每个人至少抢到1分钱
  3. 要保证红包拆分的金额尽可能分布均衡,不要出现两极分化太严重的情况

【金额处理】

  • 先将输入的totalMoney(元)扩大100倍,换做分
  • 在输出时用BigDecimal做转换
// 换做分输出
for (int i = 0; i < amount; i++) {
	System.out.println("抢到金额" + new BigDecimal(redPackage[i]).divide(new BigDecimal(100)));
}

1. 基础实现-非公平

【思路】

  1. 每次抢红包的时候直接随机就好啦,随机的上限是剩余的红包金额
  2. 每次抢到的金额 = 随机区间 ( 0, 剩余金额 )
public class _01_RedPackage {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int total = in.nextInt() * 100;
        in.nextLine();
        int amount = in.nextInt();
        int[] redPackage = new int[amount];
        getRandomMoney(total, amount, redPackage);
        // 换做分输出
        for (int i = 0; i < amount; i++) {
            System.out.println("抢到金额" + new BigDecimal(redPackage[i]).divide(new BigDecimal(100)));
        }
    }

    public static void getRandomMoney(int total, int amount, int[] redPackage) {
        final int MIN_RED = 1;
        Arrays.fill(redPackage, MIN_RED);
        int last = total - amount;
        Random random = new Random();
        for (int i = 0; i < amount - 1; i++) {
            //rand.nextInt(MAX - MIN + 1) + MIN
            int randRedValue = random.nextInt(last - 1);
            redPackage[i] += randRedValue;
            last -= randRedValue;
        }
        redPackage[amount - 1] += last;
    }
}

【存在的问题】

如果以这种方式随机,先抢的人会有很大优势,越往后的人随机到的平均金额越小。

假设有10个人,红包总额100元。

  • 第一个人的随机范围是(0,100元),平均可以抢到50元。假设第一个人随机到50元,那么剩余金额是100-50 = 50 元。
  • 第二个人的随机范围是 (0, 50元),平均可以抢到25元。假设第二个人随机到25元,那么剩余金额是50-25 = 25 元。
  • 第三个人的随机范围是 (0, 25元),平均可以抢到12.5元。

以此类推,每一次随机范围越来越小。


2. 二倍均值法

剩余红包金额M,剩余人数N,那么:

每次抢到金额 = 随机区间(0, M/N*2)

保证了每次随机金额的平均值是公平的

假设10人,红包金额100元

  • 第一人:100/10*2=20,随机范围(0,20),平均可以抢到10元
  • 第二人:90/9*2=20,随机范围(0,20),平均可以抢到10元
  • 第三人:80/8*2=20,随机范围(0,20),平均可以抢到10元

以此类推,每次随机范围的均值是相等的

缺点:除了最后一次,任何一次抢到的金额都不会超过人均金额的两倍,并不是任意的随机

public class _02_RedPackage {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 输入金额,分为单位, 扩大100倍
        int total = in.nextInt() * 100;
        in.nextLine();
        int amount = in.nextInt();
        int[] redPackage = new int[amount];
        getRandomMoney(total, amount, redPackage);
        // 换做分输出
        for (int i = 0; i < amount; i++) {
            System.out.println("抢到金额" + new BigDecimal(redPackage[i]).divide(new BigDecimal(100)));
        }
    }

    public static void getRandomMoney(int total, int amount, int[] redPackage) {
        int last_money = total;
        int last_people = amount;
        Random random = new Random();
        for (int i = 0; i < amount - 1; i++) {
            //随机范围:[1,剩余人均金额的2倍 - 1] 分
            int randRedValue = random.nextInt(last_money / last_people * 2 - 1) + 1;
            redPackage[i] = randRedValue;
            last_money -= randRedValue;
            last_people--;
        }
        //最后一人分剩余金额, 此处为非公平
        redPackage[amount - 1] = last_money;
    }
}

3. 线段切割法

前两种方法,并不能达到真正意义上的公平。而线段切割法,可以达到要求。

我们可以把红包总金额想象成一条很长的线段,而每个人抢到的金额,则是这条主线段所拆分出的若干子线段。

在这里插入图片描述

如何确定每一条子线段的长度呢?由“切割点”来决定。当N个人一起抢红包的时候,就需要确定N-1个切割点。

因此,当N个人一起抢总金额为M的红包时,我们需要做N-1次随机运算,以此确定N-1个切割点。随机的范围区间是(1, M)

当所有切割点确定以后,子线段的长度也随之确定。这样每个人来抢红包的时候,只需要顺次领取与子线段长度等价的红包金额即可。

这就是线段切割法的思路。在这里需要注意以下两点:

  1. 当随机切割点出现重复,如何处理
  2. 如何尽可能降低时间复杂度和空间复杂度
public class _03_RedPackage {
    //线段分割法
    private static List<Integer> getRandomMoney(int totalMoney, int amount) {
        //验证参数合理校验
        //为了使用random.nextInt(Integer)方法, 不得不先把红包金额放大100倍,最后在main函数里面再除以100
        //这样就可以保证每个人抢到的金额都可以精确到小数点后两位
        int redMoney = (int) (totalMoney * 100);
        if (redMoney < amount || redMoney < 1) {
            System.out.println("红包个数必须大于0,并且最小红包不少于1分");
        }
        List<Integer> boards = new ArrayList<>();
        boards.add(0);
        boards.add(redMoney);
        //红包个数和线段个数的关系
        while (boards.size() <= amount) {
            // 随机生成切割点
            int index = new Random().nextInt(redMoney - 1) + 1;
            if (boards.contains(index)) {
                //保证线段的位置不相同
                continue;
            }
            boards.add(index);
        }

        //计算每个红包的金额,将两个板子之间的钱加起来
        Collections.sort(boards);
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < boards.size() - 1; i++) {
            Integer e = boards.get(i + 1) - boards.get(i);
            list.add(e);
        }
        return list;

    }

    public static void main(String[] args) {
        List<Integer> accountList = getRandomMoney(10, 5);
        BigDecimal count = new BigDecimal(0);
        for (Integer amount : accountList) {
            //将抢到的金额再除以100进行还原
            BigDecimal tmpcount = new BigDecimal(amount).divide(new BigDecimal(100));
            count = count.add(tmpcount);
            System.out.println("抢到金额:" + tmpcount);

        }
        System.out.println("total = " + count);
    }
}

【文章参考】

  1. 漫画:如何实现抢红包算法?
  2. 微信红包的随机算法是怎样实现的?
  3. 面试题:如何实现红包算法
  4. 微信红包算法以及带上下限的红包算法
  5. 抢红包算法–四种抢红包算法对比
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值