浅谈拼手气红包的算法


首先,在这里祝各位小伙伴们鼠年大吉!!

今天是大年初三,Harris在这里用一个简单的话题,开启鼠年的博客之旅。

大家每逢佳节,最少不了的活动,就是“抢红包”。自打微信推出了电子红包之后,中国人的节日,又多了一种庆祝方式,那就是红包雨。这种将传统活动用科技的方式封装的做法,是Harris比较赞成的。把传统溶于新时代,为我们的传统春节带来了新鲜的空气。

但是,我们在群里发红包的时候,可能有的小伙伴会纳闷儿,这个所谓的随机金额,到底是怎么算的呢?它到底是不是公平的呢?

有的小伙伴和我说,这还不简单么,每一个人打开红包的时候,都按照当前剩余的余额为范围,随机生成一个数字就好了。

但是呢,这样的话,随着红包被打开的越来越多,金额的总数也会越来越少。那么,可以抢到的红包也就越来越小,这虽然遵循我们常规所认为的“先来后到”,但是并不公平。

那么要实现绝对的公平,该怎么办呢?

1. 随机分割

其实,这种方法很直观就能想到。我们把一个红包想象成一个香肠,假设有N个人来分这个香肠,那么我们只要随机切N-1刀,把每一块分别分给每一个人即可。

红包也是一样的道理,我们可以在红包发出去的同时,就随机分配好金额。等大家来分的时候,再依次分给每一个人就好了。

这种算法思路并不难,而且也能够达到我们的目的,但是由于以下的两点,导致微信没有采用这种算法:

  1. 算法复杂度高
  2. 对服务器端的要求高

首先第一点,这个算法的复杂度,肯定是要比现行的微信随机红包算法的复杂度高很多。另外,由于在红包发出去的瞬间,服务器上就需要存储关于这个红包的分配信息,在春节这种红包高峰期的时候,势必会给服务器带来巨大的压力。在分布式系统没有普及之前,第二点都将是一个难以解决的问题。

因此,微信就退而求其次,采用第二种办法。

2. 二倍均值法

实际上,我们每个人抢到的红包,都是在下面这个区间里面去随机一个数值:

(0,2M];M=\left(0,2\overline {M}\right]\quad;\overline M=\frac{剩余金额}{剩余红包数}

微信,正是采用的这种方法。

那么,为什么这个是公平的呢?

假设,发一个100元的红包,10份。那么第一个人的金额,就会在(0,20]\left(0,20\right]元里面随机。假设第一个人抢到了平均值10块钱,那么还剩90.

第二个人的抢到的,应该是(0,2909](0,20]\left(0,2\cdot \frac{90}{9}\right]\Rightarrow \left(0,20\right],和之前的一样,以此类推,这个算法是公平的。

但是,并不是每一次都能够理想化地抢到平均值,毕竟我们是要在这个范围内取随机值。假设第一个人只抢到了1块钱,那么第二个人能够抢到的范围是(0,22]\left(0,22\right],范围就更大了点,假设第二个人也只抢到1块钱,那么第三个人的范围就是(0,24.5]\left(0,24.5\right],以此类推,假设前面的9个人都只抢到1块钱,那么最后一个人的范围,就是(0,91]\left(0,91\right],实际上就是91块。所以我们可以看到,如果每个人都抢到范围内平均值以下,那么后面的人的随机范围则会越来越大。

那么,我们来看看另外一个极端,如果第一个人抢到20块钱,那么第二个人的范围就是(0,17.76]\left(0,17.76\right],假设第二个人抢到17元,第三个人的范围就是(0,15.75]\left(0,15.75\right]……

范围会越来越小,但是每个人的红包都不大,都没有超过第一个人,因此第一个人是本次的运气王。

所以,其实这个算法这么看,貌似的确不那么公平,第一个人永远只能抢到二倍均值以下,也就是说,第一个人就是抢不到大红包,而越往后越有可能抢到大红包。

当然,运气王是有可能出现在任意的位置,运气王的位置靠前,则整体不会有很大的红包,运气王靠后的话,后面就有可能出现比前面大很多的红包。

那么这样的算法用代码如何实现呢?

Harris都给大家列出来了,如果感兴趣可以研究研究:

Java:

//代码来源于网络
//发红包算法,金额参数以分为单位
public static List<Integer> divideRedPackage(Integer totalAmount, Integer totalPeopleNum){
    List<Integer> amountList = new ArrayList<Integer>();
    Integer restAmount = totalAmount;
    Integer restPeopleNum = totalPeopleNum;
    Random random = new Random();
    for(int i=0; i<totalPeopleNum-1; i++){
        //随机范围:[1,剩余人均金额的两倍),左闭右开
        int amount = random.nextInt(restAmount / restPeopleNum * 2 - 1) + 1;
        restAmount -= amount;
        restPeopleNum --;
        amountList.add(amount);
    }
    amountList.add(restAmount);
    return amountList;
}
public static void main(String[] args){
    List<Integer> amountList = divideRedPackage(5000, 30);
    for(Integer amount : amountList){
        System.out.println("抢到金额:" + new BigDecimal(amount).divide(new BigDecimal(100)));
    }
}

C/C++:

#include <stdio.h>
#include <stdlib.h>

void money();
void money(double TotalAmount, int TotalPeople)
{
	double restAmount = TotalAmount;
	int restPeople = TotalPeople;
	for (int i = 0; i < TotalPeople - 1; ++i)
	{
		int amount = rand() % ((int)((restAmount / restPeople) * 2) * 100);
		restAmount -= amount / 100;
		--restPeople;
		printf("抢到金额:%.2f\n", (double)amount / 100);
	}
	printf("抢到金额:%.2f\n", (double)restAmount / 100);
}

int main(void)
{
	money(50, 30);
	return 0;
}

Python:

import random
def money(TotalMoney,TotalPeople):
    restMoney = TotalMoney
    restPeople = TotalPeople
    for i in range(TotalPeople - 1):
        mount = round(random.uniform(0.01,(restMoney / restPeople) * 2 - 0.01),2)
        restMoney -= mount
        restPeople -= 1
        print("抢到金额:"+str(mount))
    print("抢到金额:"+str(round(restMoney,2)))

money(50,30)

本篇博客就到此结束了,其实生活中处处充满着知识,就看你有没有一颗爱钻研的心!