本文要解决什么问题?
微信群发红包的红包接龙游戏,抢到手气最佳的人要继续发下去。那么是抢到手气最佳好呢,还是不抢到手气最佳好呢?当然当不当运气王不是我们能决定的,但是抢红包的时机是我们能决定的,所以问题改为:红包接龙游戏每次都是先抢好还是后抢好?为了解决这个问题,我决定用python仿真一下。仿真的前提是知道微信红包随机算法,于是去网上搜索,大多数人仅知道所谓的“二倍平均算法“,即认为微信红包每个红包的值最小为0.01元,最大为当前红包平均值的二倍,如当年最火的毕导的一篇推送
微信红包先抢和后抢差距居然这么大!春节抢红包的大数据分析:https://mp.weixin.qq.com/s/ltsMTmz9krBNswleUyXFwQ
看起来有趣,其实不够全面。最为全面的微信红包随机算法为2016年4月份,时任微信支付后台架构负责人的方乐明发表了一篇文章讲述了微信红包的架构、并发控制、红包算法以及柔性降级方案。该文章在微信公众号的原文链接已被删除,现在最早的为2016.4.12的这篇
《揭秘微信红包:架构、抢红包算法、高并发和降级方案》https://www.cnblogs.com/8hao/p/5383143.html
以防该链接失效失效,下面是多篇转载该文章的链接,
https://blog.csdn.net/code52/article/details/51168854
https://blog.csdn.net/csdn265/article/details/51424138
https://blog.csdn.net/wuliusir/article/details/51496031
https://blog.csdn.net/linkin1989/article/details/81940143
https://mp.weixin.qq.com/s/HVrovIPSHNnZ8au7yetS5g
https://mp.weixin.qq.com/s/H2heF8u8heWSCU-k7M7Gsw
http://www.javashuo.com/article/p-frgrlslr-bs.html
https://www.pianshen.com/article/9719579422/
https://www.shangmayuan.com/a/8ec62fd225f149be9e4606ae.html
红包算法原文如下
首先,如果红包只有一个,本轮直接使用全部金额,确保红包发完。
然后,计算出本轮红包最少要领取多少,才能保证红包领完,即本轮下水位;轮最多领取多少,才能保证每个人都领到,即本轮上水位。主要方式如下:
计算本轮红包金额下水位:假设本轮领到最小值1分,那接下来每次都领到200元红包能领完,那下水位为1分;如果不能领完,那按接下来每次都领200元,剩下的本轮应全部领走,是本轮的下水位。
计算本轮红包上水位:假设本轮领200元,剩下的钱还足够接下来每轮领1分钱,那本轮上水位为200元;如果已经不够领,那按接下来每轮领1分,计算本轮的上水位。
为了使红包金额不要太悬殊,使用红包均值调整上水位。如果上水位金额大于两倍红包均值,那么使用两倍红包均值作为上水位。换句话说,每一轮抢到的红包金额,最高为两倍剩下红包的均值。
最后,获取随机数并用上水位取余,如果结果比下水位还小,则直接使用下水位,否则使用随机金额为本轮拆到金额。
将第二章中的算法用数学语言描写出来。
算法输入:当前红包剩余个数n,当前红包剩余金额a
第一步,计算下水位。若a<=200*(n-1)+0.01,则x_down=0.01,否则x_down=a-200*(n-1)
第二步,计算上水位。若a>=200+0.01*(n-1),则x_up=200,否则x_up=a-0.01*(n-1)
第三步,二倍均值法调整。若x_up>a/n2,则x_up=a/n2
第四步,在x_down和x_up中取一个随机数x_rand,包括这两个数,要求精确到小数点两位
第五步,若x_up%x_rand
import random
import numpy as np
import sys
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
def getRedpacket(n,a):#当前红包剩余个数n,当前红包剩余金额a
#第一步
if a<=200*(n-1)+0.01:
x_down=0.01
else:
x_down=a-200*(n-1)
#第二步
if a>=200+0.01*(n-1):
x_up=200
else:
x_up=a-0.01*(n-1)
x_up=round(x_up,2) #计算机以二进制存储,浮点数算法存在误差,故必须加这步
#第三步
if x_up>a/n*2:
x_up=a/n*2
#第四步
x_rand=random.uniform(x_down,x_up)
x_rand=round(x_rand,2)
#第五步
if x_up%x_rand<x_down :
x=x_down
else:
x=x_rand
#最后返回此次红包拆出的金额
return x
序言中所提的两个问题,第一个是抢红包的顺序对红包收益有无影响,第二个是抢红包的顺序对当运气王的概率有无影响。其实质是一个问题,毕竟当上运气王就相当于红包收益的最大化。因此这两个问题放在同一节解决,并使用同一段代码。事实上根据网络资料及参考文献,我们可以预见,大数据统计分析的情况一定是运气王在各种次序都有可能,但是越晚抢红包,抢到的金额的方差越大,也就是说更容易出现极端情况,也就是说运气王的概率和倒霉蛋(抢到最少钱数)的概率都比先抢红包的概率高。
数据结构: 红包个数n, 红包金额a, 重复发红包的次数times, 临时存储单次红包分配情况packet [n], 存储某个次序成为运气王的次数king [n], 存储某个次序抢到的总金额数money [n]。
算法思路如下:先输入n,a,times,然后初始化packet[n]=0,king[n]=0, money [n]=0,然后使用for循环n次,循环内部使用getRedpacket函数,并在每次调用后都要更新a和n以及packet[i]和money [i]。最后循环结束后在packet中找到最大值的下标存储到king[ ]中。这样就完整的完成了一次抢红包模拟。将上述流程重复times次,就相当于发了times次红包。代码如下。
def Redpacket(n,a,times):#红包个数n,红包金额a,重复发红包的次数times
if(a>n*200):#错误检验
print('出现错误!a_bigger_200n_error')
sys.exit
packet = np.zeros(n)#临时存储单次红包分配情况
king = np.zeros(n)#存储某个次序成为运气王的次数
money = np.zeros(n)#存储某个次序抢到的总金额数
for i in range(times):
packet = np.zeros(n)#每次开抢之前要初始化packet
a_now=a
n_now=n
for j in range(n_now):
packet[j]=getRedpacket(n_now,a_now)
a_now=a_now-packet[j]
n_now=n_now-1
money[j]=money[j]+packet[j]
king_i=np.where(packet == np.max(packet))[0][0]#找到运气王的下标,即找到packet数组中最大值的下标
king[king_i]=king[king_i]+1
if(king.sum()-20000>1e-2):#错误检验
print('出现错误!king_sum_error')
sys.exit
if(money.sum()-a*times>1e-2):#错误检验
print('出现错误!money_sum_error')
sys.exit
plt.figure(1)
plt.title('不同次序抢得的金额数')
plt.plot(money)
plt.figure(2)
plt.title('不同次序当上运气王的次数')
plt.plot(king)
return king,money
结果如下所示。500块钱分成100个红包,发20000次这样的红包。则金额波动很大,无明显统计规律。运气王则明显存在于最后20个才抢到红包的人。
当5块钱分成10个红包,发20000次这样的红包,结果则变为:后抢红包的金额大,先抢红包的运气王可能性更高。注意,这两者并不矛盾,请读者自行领悟,毕导的文章有详细解读。
若15块钱分成100个红包,发20000次这样的红包,结论又变为:先抢后抢差不多,只有最后一个抢的钱可能更多。运气王的概率则为后抢的大。
最终总结为:抢红包的顺序对红包收益及当运气王的概率有很大影响,但是并不是简单的线性关系。具体关系我们会在第八节分析。
参考文献:https://blog.csdn.net/Mrzhoug/article/details/51367118
群主先发S元红包,随机分成n份(n为群里人数),手气最佳者(即抢得红包最大者)继续发红包,也为S元,n份,下一个手气最佳者继续……从长期来看,每玩一次,每人抢得金额的最大可能性为S/n元(编者注:应为期望值);而每次抢,都有可能成为手气最佳者,可能性为1/n,需要接着发出S元红包。这样看来,每人每次收益的期望值是:S/n-1/n×S=0。长期来看,这样玩法属于零和游戏,大家没输没赢。
本节我们将重点解决序言中提到的第三和第四个问题,即红包接龙游戏每次是先抢好还是后抢好?红包接龙游戏运气王当的越多越好还是越少越好?这两个问题本质也是一样的。我们不能决定能否当运气王,但是能决定自己先抢红包还是后抢红包。所以接下来我们就来探讨红包接龙游戏每次是先抢好还是后抢好。
抢红包的顺序很可能会影响抢到的红包金额以及当运气王的概率,我们要探讨两个问题,一个是当运气王对红包接龙游戏收益的影响一个是抢红包顺序对红包接龙游戏收益的影响,但是无论是哪个问题,每次抢红包都要把按顺序抢红包,而不能是乱序抢红包。不然的话,红包金额是随机的,抢红包顺序也是随机的,那么最后的大数据统计结果也一定是随机的了,没有参考意义。
算法开始。
先筛选无效输入:如果a_packet>n_group*200,则立即返回error。
数据结构: 红包个数n,红包金额a,红包接龙的轮数times,临时存储运气王下标king_now,临时存储单次红包分配情况packet [n],存储某个次序成为运气王的次数king [n],存储某个次序抢到的总金额数money [n]
第一步:先输入n,a,times,然后初始化packet[n]=0,king[n]=0, money [n]=0。刚开始还没有运气王,因此从n中随机选择一个人序号赋值给king_i,发a元钱红包,此时应更新money[king_i]-= a。
第二步:所有人开始抢红包。使用for循环n次,循环内部使用getRedpacket函数,并在每次调用后都要更新a和n以及packet[i]和money [i]。最后循环结束后在packet[ ]中找到最大值的下标,记作king_i,存储到king[ ]中。
第三步:运气王继续发红包。money[king_now]-= a。然后执行第二步。
循环二三步。直至红包接龙结束。
代码如下。
def RedpacketChain(n,a,times):#红包个数n,红包金额a,红包接龙的轮数times
if(a>n*200):#错误检验
print('出现错误!a_bigger_200n_error')
sys.exit
king_i=0,#临时存储运气王下标
packet = np.zeros(n)#临时存储单次红包分配情况
king = np.zeros(n)#存储某个次序成为运气王的次数
money = np.zeros(n)#存储某个次序抢到的总金额数
king_i = random.randint(0,n-1)
for i in range(times):
money[king_i]=money[king_i]-a#运气王发红包,因此其金额减少
packet = np.zeros(n)#每次开抢之前要初始化packet
a_now=a
n_now=n
for j in range(n_now):
packet[j]=getRedpacket(n_now,a_now)
a_now=a_now-packet[j]
n_now=n_now-1
money[j]=money[j]+packet[j]
king_i=np.where(packet == np.max(packet))[0][0]#找到运气王的下标,即找到packet数组中最大值的下标
king[king_i]=king[king_i]+1
if(king.sum()-20000>1e-2):#错误检验
print('出现错误!king_sum_error')
sys.exit
if(money.sum()-a*times>1e-2):#错误检验
print('出现错误!money_sum_error')
sys.exit
plt.figure(1)
plt.title('不同次序抢得的金额数')
plt.plot(money)
plt.figure(2)
plt.title('不同次序当上运气王的次数')
plt.plot(king)
return king,money
结果如下所示. 300块钱分成30个红包,进行20000轮次的红包接龙,明显后抢红包的当运气王概率高,则收益明显下降甚至成为了负数。
100块钱分成10个红包,进行20000轮次的红包接龙,结论不变,后抢红包的当运气王概率高,收益会下降成为了负数。
我想探寻红包总额、红包个数与运气王的真正关系!目前的思路就是画一个三维图像,x轴为红包总额,y轴为红包个数,纵轴为拿到运气王的次序,这样的3D图像就能形象展现出这三者的关系,但是时间复杂度将很大!比如红包个数,从2个到100个,间隔1,则有99个取值,红包总金额,从1元到1000元,间隔1,则有1000个取值,在每个交叉点执行一次Redpacket()函数,该函数内部本身又有一个times次循环和红包个数n嵌套的双重循环,若times=1000,则时间复杂度为10010001000*100=10的10次方!相关绘图代码我放在下面,等我有空或者其他感兴趣的读者自己运行一下。
def Redpacket_king(n,a,times=10000):#红包个数n,红包金额a,重复发红包的次数times
#本函数由Redpacket函数改编,仅用于返回获得运气王次数最多的次序数
if(a>n*200):#错误检验
print('出现错误!a_bigger_200n_error')
sys.exit
packet = np.zeros(n)#临时存储单次红包分配情况
king = np.zeros(n)#存储某个次序成为运气王的次数
money = np.zeros(n)#存储某个次序抢到的总金额数
for i in range(times):
packet = np.zeros(n)#每次开抢之前要初始化packet
a_now=a
n_now=n
for j in range(n_now):
packet[j]=getRedpacket(n_now,a_now)
a_now=a_now-packet[j]
n_now=n_now-1
money[j]=money[j]+packet[j]
king_i=np.where(packet == np.max(packet))[0][0]#找到运气王的下标,即找到packet数组中最大值的下标
king[king_i]=king[king_i]+1
if(king.sum()-20000>1e-2):#错误检验
print('出现错误!king_sum_error')
sys.exit
if(money.sum()-a*times>1e-2):#错误检验
print('出现错误!money_sum_error')
sys.exit
king_a=np.where(king == np.max(king))[0][0]
return king_a
#本代码时间复杂度很大
n = np.arange(2, 100, 1)#红包个数,从2个到100个,间隔1
a = np.arange(1, 1000, 1)#红包总金额,从1元到1000元,间隔1
x,y = np.meshgrid(n, a)
king=np.zeros(shape=x.shape)
for i in range(1, 1000, 1):
for j in range(2, 100, 1):
if(i>200*j):
continue
print("i:",i," j:",j)
king[i-1][j-2] = Redpacket_king(j,i)
fig = plt.figure()
ax = Axes3D(fig)
surf=ax.plot_surface(x,y,king, rstride=1, cstride=1, cmap=cm.viridis)
# Add a color bar which maps values to colors.
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()
仅需标注出处即可随意对本文进行转载、参考或使用。