基于正态分布的红包生成算法

简介

如果想生成的红包大小相对均衡,可以同时控制最大最小和总额,那么不妨试试我这个方法。 在此之前我们先简单介绍一下正态分布。

什么是正态分布(normal distribution)

复习一下高中知识,正态分布又叫做常态分布、高斯分布(Gaussian distribution)。

其中有两个有两个参数: mu为学期望,可以看作最高概率的点。 sigma^2为方差(实数),可以看作分布的均匀程度。

正态分布的应用

现实中很多现象都符合正态分布,十分神奇,比如下面这个图。 我们可以利用正态分布对很多分布情况建模。所以我们如果想创建随机分布的红包也可以用使用随机分配红包。

java中的正态分布

JDK中Random类中的nextGaussian()方法,可以产生服从标准正态分布的随机数。即 X~N(0,1);

如果我们想产生自定义的正态分布呢 X~N(μ,b) b=σ^2;

方差 * 正态分布数据 + 正态分布中心位置 产生N(a,b)的数:

java 复制代码
Math.sqrt(b)*random.nextGaussian()+a;

即均值为a,方差为b的随机数

代码实现

整体思想: 其实就是将数据分为两部分,先用最小值min铺平,上面使用正态分布分配数值。那么如何使用正态分布分配呢?首先可以先用上面提到的公式产生n份服从正态分布的0~1之间的随机数,然后对这些随机数做一个概率计算,即求出每份的占比,使用占比与剩余总数相乘的到红包大小。当然,如果有最大值限制,这个过程需要循环进行,最终会把峰值切掉,补充到后面的红包上。

java 复制代码
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;

/**
 * @Description 红包生成器
 * @Date 2021/1/22
 * @Author yuvenhol
 */
@Slf4j
public class RedPacketGenerator {

    public static final Random random = new Random();

    public static BigDecimal[] genPackets(Param param) {
        ExceptionUtil.check(!check(param), "红包参数不正常");
        BigDecimal leftMoney = param.total;//可分配的金额
        BigDecimal min = param.min;// 最小
        BigDecimal max = param.max;// 最大
        Integer count = param.count.intValue();// 份数
        BigDecimal[] result = new BigDecimal[count];
        //数组所有元素都赋最小值 铺底
        IntStream.range(0, count).forEach(x -> result[x] = min);
        leftMoney = leftMoney.subtract(min.multiply(param.count));
        //定义正态分布的随机数组
        BigDecimal[] normalNumbers = new BigDecimal[count];
        //给数组赋值
        IntStream.range(0, count).forEach(x -> normalNumbers[x] = NormalDistribution(param.variance));
        //统计随机数总合
        BigDecimal countNormalNumbers = Arrays.stream(normalNumbers).reduce((x1, x2) -> x1.add(x2)).get();
        //每份金额, 这里提高精度减小误差
        BigDecimal perAmount = leftMoney.divide(countNormalNumbers, 10, BigDecimal.ROUND_HALF_UP);
        //此时 perAmount * countNormalNumbers = leftMoney,
        int i = -1;
        while (leftMoney.compareTo(BigDecimal.ZERO) > 0) {
            i = ++i % count;
            // 取随机安全值
            BigDecimal addAmount = perAmount.multiply(normalNumbers[i]).setScale(2, BigDecimal.ROUND_HALF_UP);
            //超越上限
            if (addAmount.add(result[i]).compareTo(max) > 0) {
                continue;
            }
            //防止剩余钱数减超
            if (leftMoney.subtract(addAmount).compareTo(BigDecimal.ZERO) <= 0) {
                addAmount = leftMoney;
                leftMoney = BigDecimal.ZERO;
            } else {
                leftMoney = leftMoney.subtract(addAmount);
            }
            result[i] = result[i].add(addAmount);
        }
        return result;
    }

    public static boolean check(Param param) {
        if (param == null) {
            log.error("参数为空");
            return false;
        }
        if (param.max.compareTo(param.min) < 0) {
            log.error("最大最小值不匹配");
            return false;
        }
        if (param.max.multiply(param.count).compareTo(param.total) < 0) {
            log.error("最大值不能超过总值");
            return false;
        }
        return true;
    }

    public static boolean check(RedPacket redPacket) {
        return check(new RedPacketGenerator.Param(redPacket.getRealTotalMoney(), redPacket.getRealCount(), redPacket.getMax(), redPacket.getMin()));
    }


    /**
     * 获取 0-1正态分布的随机值
     *
     * @param v 方差
     * @return 随机值
     */
    public static BigDecimal NormalDistribution(double v) {
        double r;
        int varianceRange = 3; //这里正态分布N中三倍标准差 (99.7%)。
        do {
            r = Math.sqrt(v) * random.nextGaussian();
        }
        while (r < -varianceRange || r > varianceRange);//过滤掉超过规定范围的特殊值
        r = (r / varianceRange + 1) / 2;//整理成 0-1之间的数
        return new BigDecimal(r);
    }

    public static class Param {
        BigDecimal total;// 总金额
        BigDecimal min;// 最小
        BigDecimal max;// 最大
        BigDecimal count;// 份数
        double variance;//方差


        public Param(BigDecimal totalMoney, int count, BigDecimal max, BigDecimal min) {
            this(totalMoney, count, 10, max, min);
        }

        public Param(BigDecimal totalMoney, int count, double variance, BigDecimal max, BigDecimal min) {
            this.total = totalMoney;
            this.min = min;
            this.max = max;
            this.count = BigDecimal.valueOf(count);
            this.variance = variance;
        }
    }
}
相关推荐
字节跳动数据库6 分钟前
文章分享——相似函数处理方法
人工智能·后端·程序员
云技纵横6 分钟前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户67570498850224 分钟前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan40 分钟前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户67570498850244 分钟前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia1 小时前
Geo Scene域名修改引起的一些问题
后端
用户298698530141 小时前
Java 实现 Word 文档加密与权限解除
java·后端
vanuan1 小时前
给你的A2A-Agent加把锁-认证鉴权实战指南
后端
Yeats_Liao1 小时前
14:Servlet中的页面跳转-Java Web
java·后端·架构