抽獎 & 隨機數

最近修改了網站的抽獎算法,使得抽獎看起來更加『公平』,爲此我整理了下,談談在抽獎系統設計中的『坑』。javascript

抽獎分爲兩種:java

  1. 知道總人數mysql

  2. 不知道總人數算法

舉栗子

1. 已知人數

14 個獎品分給 500 我的:sql

  1. 獎品分爲一等獎、二等獎、三等獎;數據庫

  2. 總人數 500 人。網絡

獎品 數量
一等獎 1
二等獎 3
三等獎 10

設計思路:dom

  1. 爲 500 人設計序號,1 - 500;函數

  2. 生成中獎序列(僞代碼)網站

// 中獎序號, 大獎爲最後一個
awardIds = List()
// random 出序號 
while (awardIds.size() < 14) {
  rand = Math.ceil(random() * 500); // 取整
  // 限不限制均可以,機率過低
  awardIds.contains(rand) ? awardIds.push(rand);
}

這種設計下,每一個人的中間機率都是 1/500 之一。

這樣就選出了中獎列表了,然而有『坑』,後面再說這個問題。

2. 開放人數

需求栗子

設計一個簡單的開放人數抽獎系統。

  1. 獎品分爲一等獎、二等獎、三等獎;

  2. 保證每一個人的抽獎機率一致。

獎品 數量 機率
一等獎 1 0.001
二等獎 3 0.005
三等獎 10 0.01
設計思路

利用程序自帶的 random 函數,很容易生成一個 0 ~ 1 之間的隨機數,能夠直接這樣設計:

獎品 中獎區間
一等獎 [0, 0.001)
二等獎 [0.001, 0.006)
三等獎 [0.006, 0.016)

只要 random() 落在了哪一個區間,就中了幾等獎。

這種狀況下由於是對區間取值,因此每次抽獎的機率都是同樣的。

說完栗子,下面來講說 random 函數。

random 函數

大部分語言都自帶 random 函數,然而都只是僞隨機,是由可肯定的函數(經常使用線性同餘),經過一個種子( 經常使用當前時間),產生的僞隨機數。

這意味着:若是知道了種子(seed),或者已經產生的隨機數,均可能得到接下來隨機數序列的信息。便可以預測。

種子

因爲各個語言實現 random 的方式不同,因此栗子 一、2 會有不一樣的問題存在。

栗子 1 中可能會出現種子沒有更改的狀況,這種狀況下生成的序列能夠預測,設計沒有問題,而結果變成了『坑』。而若是種子時間相關就能夠避免這個問題。

栗子 2 中若是同一時間有多個用戶同時開始抽獎,這個時候時間相關的種子又會帶來問題,即:只要這幾個用戶有一箇中獎,就意味着這個時間點的用戶都中獎了。

真隨機

程序並不能產生真正的隨機數,可是能夠產生理論上的真隨機數。

如 UNIX 中的 /dev/random 能夠當作是一個真隨機的生成器。

具體來說就是生成器有一個容納噪聲數的熵池,在讀取時,/dev/random 設備會返回小於熵池噪聲總數的隨機字節,好比任何硬件的狀態變化,IO 響應時間、磁盤讀寫速度、中斷、網絡變化等等,都會做爲熵反饋給生成器,因此理論上就產生了不可預測性。

若是程序中 random 並不能知足隨機要求,能夠爲種子設計一個熵,將不可預知的事件收集起來,做爲種子產生隨機數。

從新設計栗子 2

如今咱們有了真隨機函數,而後栗子 2 依然不是一個可用的抽獎系統,依然有一些問題。

問題

重複中獎

  1. 同一個用戶重複中一種獎品?

    必然要限制,否則很容易被認爲有 py 交易。

  2. 同一個用戶重複中多個獎品?

    限制,虛擬獎品(積分等)無所謂,實體獎品限制。

獎品放出

  1. 抽獎開始就放出全部獎品?

    因爲沒法預估人數,機率很難調整。

  2. 抽獎開始後獎品一段時間一段時間的放出。

    防止大量的人都在放出的點抽取,而送光獎品。

區間清理

  1. 若是某個獎品被抽光了,是否剔除該獎品的區間?

    剔除不剔除都不要緊,爲了簡化處理(偷懶),其實是能夠不用理會的。

從新設計

在保持核心中獎邏輯不變(機率區間)的狀況下,爲栗子 2 添加限制。

惟一中獎

設計了獎品(award)以後,再添加一張 具體獎品(cdkey),使用 user 表示中獎用戶,默認值 null。

字段 類型 默認值
... ... ...
user int、UNIQUE null
  1. 若是使用 MySQL ,MySQL 的 UNIQUE 限制並不校驗 null 值,因此在限制惟一中獎上並不用單獨處理。

  2. 若是不作 UNIQUE 限制,那麼就須要手動查詢了:

# SQL 示例, 一次選出兩條,檢查用戶是否中獎過
SELECT * FROM ckdey WHERE award = ? AND (user = ? OR user = null) LIMIT 2;

獎品放出時間

爲了限制獎品的放出時間,能夠在 cdkey 上再添加 ready 表示放出時間:

獎品 ready
一等獎 1 08:00:00
二等獎 1 08:00:00
二等獎 2 12:00:00
二等獎 3 16:00:00
三等獎 1 08:00:00
... ...

這種方法在獎品數少的時候特別好使,並且還能夠指定開獎時間,然而當獎品數不少的時候,就沒那麼好使了。

當不須要使用指定時間時,徹底能夠利用 random 的特性來隨機生成時間,不須要寫入數據庫。

  1. 在 award 上增長 count(總數) 和 remain (剩餘數量)

  2. 以 remain 的線性函數做爲隨機數種子,則每次隨機數的序列都是同樣的。

# 等分抽獎時間
average = Math.ceil((expirationTime - startTime) / count);
# 相同的種子獲得生成器
rand = seedrand(transform(remain))
# 獲取獎品放出時間
readyTime = expirationTime + Math.ceil(average * rand()) - average * remain;

這樣對於每一個用戶,每次放出的時間都是相同的(不看源碼也不知道是何時),每次放出一個獎品,第一段時間沒送出去,就累積到第二段時間,依次後推。

PS: 這種設計下,中獎的人真的是運氣爆表,萬年非洲人表示很憂傷!!

相關文章
相關標籤/搜索