0x00 前言#  起因是这样的,马上就要回所了,计算所的宿舍选择有三项,苏州街、青年公寓和科一招,其中科一招应该是较差的,苏州街住宿条件最好,但是 6 人间,并且通勤时间较长,青年公寓就显得比较 OK。
而青年公寓和苏州街的名额有限,往往通过抽签的方式,而最传统的抽签方式就是抢红包,笔者所在的实验室采取的策略是赢家通吃,也就是获得微信红包的 Top K 的获得更好的住宿。
0x01 分析#  毕导在 2020 年就做过了微信抢红包的分析 BV1z7411e7qB ,得到的结论是,所有人的期望都是相同的,但是越往后越容易抽到“大红包”,方差会变大。
由此也有了获得运气王的概率:
而在这个条件下,我们希望获得了可以不是运气王,Top K 就可以了,所以这算是毕导工作的一个 Incremental work。
0x02 模拟#  从毕导的视频中可以看到,微信红包的金额是[0.01, 剩余金额平均值的 2 倍],于是乎,笔者用 ChatGPT 写了个程序模拟,自己修改了一下 BUG,这里我们只计算至多 20 个人的至多 Top10,模拟了 100000 次。
但是有一些坑需要注意,首先,抢到的金额应当是保留两位小数的,同时,如果是最后一个人,那么他抢到的金额应当是剩余的金额。
代码如下:
 1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 import  random 
import  matplotlib.pyplot  as  plt 
 # 模拟参数 
num_trials = 100000 
 
 
 def  simulate_red_envelope (num_users):
    total_amount = 100.0 
     red_envelope = [0.0 ] * num_users
 
     for  i in  range (num_users):
         remaining_envelope = num_users - i
         remaining_amount = total_amount - sum (red_envelope)
         avg_amount = remaining_amount / remaining_envelope
         max_amount = min (avg_amount * 2 , remaining_amount)
         amount = (
             random.uniform(0.01 , max_amount)
             if  remaining_envelope > 1 
             else  remaining_amount
         )
         amount = round (amount * 100 ) / 100.0 
         red_envelope[i] = amount
 
     return  red_envelope
 
 
 def  calculate_topk_probability (num_users, num_trials):
    probabilities = []
     for  k in  range (1 , min (num_users, 10 )):
         results = [0 ] * num_users
         for  _ in  range (num_trials):
             red_envelope = simulate_red_envelope(num_users)
             sorted_envelope = sorted (
                 range (num_users), key=lambda  x: red_envelope[x], reverse=True 
             )
             topk = sorted_envelope[:k]
             # 统计获得 topk 的概率 
             for  i in  range (k):
                 results[topk[i]] += 1 
 
         probabilities.append([result / num_trials for  result in  results])
 
     return  probabilities
 
 
 def  plot_probability (probabilities):
    num_users = len (probabilities[0 ]) if  len (probabilities) > 0  else  0 
     k_values = list (range (1 , min (num_users, 10 )))
 
     # 绘制 k 个子图 
     plt.figure(figsize=(4  * len (k_values) + 4 , 4 ))
 
     for  i, k in  enumerate (k_values):
         ax = plt.subplot(1 , len (k_values), i + 1 )
 
         ax.plot(range (1 , num_users + 1 ), probabilities[i], label="Probability" )
         print (probabilities[i], k)
 
         ax.set_xlabel("Rank" )
         ax.set_xlim(1 , num_users)
         # 只显示整数坐标 
         ax.set_xticks(range (1 , num_users + 1 ))
         ax.set_ylabel("Probability" )
         ax.set_ylim(0 , 1 )
 
         # Title 
         ax.set_title("Probability of Top  {}  Amounts" .format(k))
 
     # plt.xlabel("Rank") 
     # plt.xlim(1, num_users) 
     # # 只显示整数坐标 
     # plt.xticks(range(1, num_users + 1)) 
     # plt.ylabel("Probability") 
     # plt.ylim(0, 1) 
 
     # plt.title("Probability of Top K Amounts") 
     # plt.legend() 
 
     plt.savefig("red_envelope_probability_N {} _K {} .png" .format(num_users, k))
 
 
 for  N in  range (2 , 21 ):
    # 模拟抢红包并计算概率 
     probabilities = calculate_topk_probability(num_users=N, num_trials=num_trials)
 
     # 绘制概率图表 
     plot_probability(probabilities)
 
0x03 结果 & 结论#  得到的结果有点多,笔者仅展示有特点的几个( $N = 2,3,4,5,10,20$ )。
N=2
N=3
N=4
N=5
N=10
N=20
首先 Top1 其实就是运气王啦,用来和毕导得到的结果比较,验证我的结果是否是正确的。
关于运气王,也正如毕导所得到的结论,当人数越多时,后两个人获得运气王的概率越大。
关于 Top K,当 K 增加时,这个曲线会逐渐从一个下凹的曲线变成一个上凸的曲线,直到最后会变成一个单调下降的曲线。这也就是所谓的方差变大,当 K 增加时,最后一名获得较低金额的概率会变大,因而获得 Top K 的概率会变小。同时,这个 Top K 的概率也会随着 K 的增长,概率的均值变大,毕竟你 10 个人获得 Top 10 的概率始终为 1。
也就是说,在一定范围内,你获得 Top K 应当是越晚抽越好,但是当 K 超过一定值时,应当是越早抽越好。
那么这个阈值应当存在于什么地方呢?接下来来探究一下这个问题。
这里其实是有一些小 trick,首先最后一个抽红包的一定是一个非常特殊的,因为他获得的金额并不是通过采样得到的。笔者这里计算转折点时,如果考虑让整个序列单调下降,那么这个序列是极其不稳定的,甚至没有太多的规律(非递增),可能需要更多的理论计算才能支撑得到这个结论,同时也受到了很大的采样误差的影响,因为最后两个的概率差异并不大,这里笔者受概率论知识限制,将这个难题留给读者思考。但如果不考虑最后一个,只考虑前面的序列递减的话,那么这个阈值就变成了随着 N 的增加递增的了。
绘制的 N 关于阈值 k 的图如下所示:
曲线变化阈值 k 关于 N的曲线
一个猜测的结论就是这个阈值 k 满足下面的公式:
$$
k = \left \lfloor \frac{N-1}{4} \right \rfloor
$$
至于为什么是 4,应该是与微信红包的金额分布有关,但这里笔者缺少理论分析。
0x04 局限性#  这个其实是一个大家对立的 setting,在抽红包的过程中,大家不进行信息共享,但是是在真实环境中,你可以通过询问已抽过红包的同学获得当前的人数以及剩余金额,那么这种条件下决策则会更为复杂。比如说,前面的人抽到的都是小红包,此时决策应当是什么样子;前面有人抽到了一个很大的红包,此时决策应当是什么样子。这个问题还有待探究,留给读者思考。
这其中其实也还有很多未研究透彻的点,同时受笔者概率知识限制,难以给出更多的概率理论计算,读者若感兴趣,欢迎在评论区讨论和交流。