1. 程式人生 > >紅包分配演算法(年後寫的)

紅包分配演算法(年後寫的)

今天正月十一,春節已經過去了10天,紅包想必大家都已經搶很多了,不知道大家有沒有想過這個紅包怎麼實現分配的呢?

為什麼有的人分這麼多,有的人分那麼多?

到底是怎麼實現的呢?

答案是,我也不知道。

現在最火的就是微信紅包了,當然我們也不可能知道微信紅包究竟採用了什麼樣的具體演算法,只能通過觀察和分析,加上網路上的眾人之力,給出一個可能的紅包分配方案,紅包隨機分配演算法可以很簡單,也可以很複雜,下面就從簡入難著手,力圖實現一個靠譜的紅包隨機分配演算法版本。

簡單的開始

先簡化問題:紅包總金額為money,紅包個數為n,求各個紅包的金額,程式碼如下:

def redPacketsRandom(money, n):
    divide_table = []
    for i in range(0,n):
        divide_table.append(random.uniform(1,10))
    sum_ = sum(divide_table)
    result = []
    cur_sum = 0.0
    for i in range(0,n-1):
        cur = round((divide_table[i]*money/sum_),2)
        result.append(cur)
        cur_sum = cur_sum + cur
    result.append(money-cur_sum)
    return result

程式碼很簡單,但是有幾個重要細節需要說明:

1.利用random生成隨機數時,要避免生成的隨機數中存在負數和0,如果有0的話該紅包將為控紅包,顯然是不行,因此,上面程式碼中將隨機數固定在1-10的範圍內。

2.最後每個紅包的金額數都要精確到小數點後兩位,對應上述程式碼中的:cur = round((divide_table[i]*money/sum_),2)。這樣做有兩個原因是小數點後3位的錢沒有意義。

3,計算最後一個紅包的金額時,要用總金額減去前面紅包的金額總和,而不能跟前面的紅包金額計算方法一樣(根據比例來算),這是因為之前精確到小數點後兩位的操作可能造成精度的丟失,而使最後所有紅包的金額總和略小於money的值。其實,及時在前面沒有進行青雀到小數點後兩位的操作,也同樣可能造成精度的丟失,因為程式語言智慧儲存有限長度的浮點數,當遇到小數點後的位數較多甚至是無限小數的時候,還是會自動丟失後面的值。

總之,在這個最簡單的版本里,彷彿也能看到程式設計師的修養。

紅包金額滿足固定分佈

很高興網路上有不少童鞋跟我一樣對紅包的分配演算法很感興趣,並且對此神佑研究。這也讓我寫這篇博文的時候省了很多事兒,我選擇了站在巨人的甲板上,例如,之湖上就有人專門對此提問:微信紅包金額分配的演算法是怎樣的?誰比較容易到最佳手氣?誰有機會拿到較大的金額?問題下面有很多有意義的回答,其中知乎使用者“馬景鋮”的回答最為詳細。他認為“錢包錢數滿足截尾正態隨機數分佈”,並給出了自己的調研資料。

當然,我不能保證這一結論的正確性,因為我沒有自己調研過真實資料,只是單純地覺得他說的好像很有道理(^__^) 。說實話,即使我自己做過這樣的調研,也不能保證調研結果的正確性,畢竟我所能調研的資料量不會很大。不過沒有關係,這裡暫且認為這是一個可靠的假設。下面的任務就是如何修改上面的程式碼,使錢包的金額滿足這樣的分佈。下面給出參考程式碼(可以對比前面的程式碼,著眼於新增和修改的部分):

def redPacketsRandom(money, n):
#指定均值和標準差,均值和標準差的指定方法不確定,這裡只是給出一種可能
mu = float(money)/n
sigma = mu/2.0
divide_table = []
while len(divide_table) < n:
    random_float = random.normalvariate(mu, sigma)
    if random_float > 0:
        divide_table.append(random_float)
sum_ = sum(divide_table)
result = []
cur_sum = 0.0
for i in range(0,n-1):
    cur = round((divide_table[i]*money/sum_),2)
    result.append(cur)
    cur_sum = cur_sum + cur
result.append(money-cur_sum)
return result

越是後面的錢包,價值普遍越高

這一點非常有趣,而且經過多次觀察確實如此。以前以為紅包在於“搶”,即手速越快越好,但是其實開始搶到的往往都是小紅包。這就要求搶紅包的時候拿捏好尺度,太快了只能搶到小紅包,太晚了紅包則被搶完了,連小紅包都沒有。突然想說,人生就像搶紅包,有些人願意冒險一試,成功了建萬事功勳,失敗了一無所有;有些人則願意作保底打算,穩妥為上。
切入正題,如果簡單地將生成的紅包列表按大小進行排序,依照嚴格的大小順序從高往低出紅包,肯定是不行的,因為小紅包在前大紅包在後只是一種總體上的統計規律,而不是嚴格的規則。也就是說,在後面領到小紅包,以及在前面領到大紅包,都是有可能的,只是可能性比較小而已。這個時候,就需要對出紅包的順序進行隨機安排。參考程式碼如下:

def redPacketsRandom(money, n):
#指定均值和標準差,均值和標準差的指定方法不確定,這裡只是給出一種可能
mu = float(money)/n
sigma = mu/2.0
divide_table = []
while len(divide_table) < n:
    random_float = random.normalvariate(mu, sigma)
    if random_float > 0:
        divide_table.append(random_float)
sum_ = sum(divide_table)
result = []
cur_sum = 0.0
for i in range(0,n-1):
    cur = round((divide_table[i]*money/sum_),2)
    result.append(cur)
    cur_sum = cur_sum + cur
result.append(money-cur_sum)
#確定出紅包的順序
result = sorted(result)
sort_dict = {}
for i in range(0,n):
    mu = i+1
    sigma = 2
    random_float = random.normalvariate(mu, sigma)
    sort_dict[i] = random_float
sort_dict = sorted(sort_dict.items(), key=lambda d: d[1])
result_sort = [0.0]*n
for i in range(0,n):
    result_sort[sort_dict[i][0]] = result[i]
return result_sort

這裡對上面確定紅包順序的程式碼解釋一下:
一開始,將求得的紅包金額按照從小打到進行排序;其次,生成一個確定紅包大小順序的隨機數序列,第一個隨機數滿足均值為1,標準差為2;第二個隨機數滿足均值為2.標準差為2;第三個隨機數滿足均值為3,標準差為2;……
;第n個隨機數滿足均值為n,標準差為2。這樣,就構成了一個隨機數序列,這個序列服從這樣一個規律:前面的數的總體均值小於後面的數的總體均值。然後,將生成的隨機數序列進行從小達到排序,排序的時候要保留該隨機數在排序之前的下標。假設第三個隨機數在所有隨機數中最小,則說明第三個紅包應該是所有紅包中最小的紅包,以此類推……