红包分配:指定金额指定上下限后随机分发成若干个红包,随机抽

        公司最近策划一个红包活动,指定金额指定上下限后随机分发成若干个红包,以抽奖机制抽给员工。拿到这活,首先想到的当然是度娘哈,偷师学艺之后,初步进行代码实现,下面讲述下原理。

        主要分两步:红包随机分割和随机抽奖

  • 红包随机分割  依据红包个数,分割出可供随机分配的剩余金额,计算出剩余金额每个红包可以分配的最小金额、剩余金额每个红包可以分配的最大金额,获取每个红包可分配的随机值,计算出每个红包的金额:红包下限+可分配随机值,最后打乱红包集   复杂度 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. 将整个概率分布拉平成为一个1*N的长方形即为Alias Table。储存两个数组,一个数组里面存着第ii列对应的事件ii矩形站的面积百分比即概率,另一个数组里面储存着第ii列不是事件ii的另外一个事件的标号
  2. 产生两个随机数,第一个产生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);
			}
		}

有啥问题希望大家留言,一起学习~

转载于:https://my.oschina.net/syuson/blog/1633786

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值