基於概率的公平抽獎、公開開獎演算法
阿新 • • 發佈:2019-02-13
背景
最近,由於專案需要,在產品同事的合作下專門設計並開發了一個基於概率的抽獎、開獎程式。我們先看下需求:
需求
基於現有的使用者積分資訊開發一套世界盃抽獎、開獎程式。首先,每個使用者每天完成日常任務後可參與現金紅包抽獎,中獎概率為隨機的,不做人為隱形設定。但是系統可以配置現金獎池大小,根據獎池大小,每天最多發出相應金額的紅包。其次,現金紅包有不同的金額範圍限制,不同的範圍中獎概率不同。另外,根據使用者操作等,積分榜是實時變動的。活動結束後積分榜前三名和積分榜4-50中隨機抽取的一名,共4名小夥伴可獲得系統大獎。設計一個公開、公正的演算法從第4名至第50名中隨機抽取一個幸運兒。
設計
由於獎池和紅包部分相對複雜,所以編寫程式處理;隨機抽取幸運兒相對簡單,利用第三方資料來源和獨立公開演算法計算出幸運兒名次即可。
幸運的寶寶
這部分相對簡單,先說下規則:
LuckyNumber = Parameter % (50 - 3) + 4
Parameter取值為 中國銀行美元外匯牌價 2018/07/16 12:00前(世界盃結束後,平臺於07/16中午公開開獎)最後一條記錄的現匯買入價去小數點後的值;為了保證公平性或防止意外,可從多平臺取值,即
LuckyNumber = (Parameter1 + Parameter2 + Parameter3) % (50 - 3) + 4
50 - 3的意思是隻有47人蔘與抽獎
餘數是從0開始的,而中獎者序號從4開始,所以+4
- 舉例
假如2018/07/16查詢得到如下外匯資訊:
則:LuckyNumber = Parameter % (50 - 3) + 4 = 66132 % 47 + 4 = 3 + 4 = 7
榜單第7名即為幸運使用者。
紅包抽獎
配置
- level=N # 0-9
- level.prize=p0, p1, p2, … , pn
- level.rate=r0, r1 … , rn-1
配置示例
- level=3
- level.amount=100, 1000, 9000, 10000
- level.rate=100, 20, 10
示例含義
紅包分為三個等級,[1.00, 10.00)的概率是1%(100/10000),[10.00-90.00)的概率是0.2%,[90.00-100.00]的概率是0.1%。
示例程式
package test;
import java.util.Random;
import com.alibaba.fastjson.JSONObject;
public class T {
private static final int LEVEL = 3;
private static final int[] LEVEL_AMOUNT = new int[] { 100, 1000, 9000, 10000 };
private static final int[] LEVEL_RATE = new int[] { 100, 20, 10 };
public static void main(String[] args) {
System.out.println(prize());
}
/**
* {"lucky":9282,"prize":false}
* {"lucky":50,"amount":980,"level":0,"prize":true}
* lucky:幸運數
* prize:中獎標識,true-中獎,false-沒中獎
* level:中獎等級
* amount:中獎金額
* @return
*/
public static JSONObject prize() {
JSONObject result = new JSONObject();
Random r = new Random();
// 伺服器每天第一次收到抽獎請求或相關請求時初始化當日獎池資訊,
// 或定時器每天自動初始化獎池資訊(凌晨高併發請求時可能查不到當日獎池資訊)
// 生成幸運數
int lucky = r.nextInt(10000);
result.put("lucky", lucky);
// 宣告中獎等級,預設不中獎,值為-1;或中獎值為0,1,2
int level = -1;
// 若lucky < level.rate0,則level=0;
// 否則,若lucky < level.rate0 + level.rate1,則level=1
// ...
int rate = 0;
for (int i = 0; i < LEVEL_RATE.length; i++) {
rate += LEVEL_RATE[i];
if (lucky < rate) {
level = i;
break;
}
}
if (level > -1) {
result.put("prize", true);
result.put("level", level);
int amount = 0;
// 最後一箇中獎等級的獎金範圍是閉區間,其他都是左閉右開
if (level + 1 == LEVEL) {
// nextInt(N)的範圍是[0,N),nextInt(N * 10) / 10的範圍是[0,N]
int target = (int) (LEVEL_AMOUNT[level + 1] - LEVEL_AMOUNT[level] + 1) * 10;
amount = r.nextInt(target) / 10 + (int) LEVEL_AMOUNT[level];
} else {
// nextInt(N-M)的範圍是[0, N-M),nextInt(N-M) + M的範圍是[M,N)
int target = (int) (LEVEL_AMOUNT[level + 1] - LEVEL_AMOUNT[level]);
amount = r.nextInt(target) + (int) LEVEL_AMOUNT[level];
}
int pool_unusedAmount = 1000000; // 查詢當日獎池可用金額
// 獎池可用金額不足時,按獎池剩餘金額支付
if (amount > pool_unusedAmount) {
amount = pool_unusedAmount;
}
// 更新當日獎池資訊,儲存使用者中獎資料
result.put("amount", amount);
} else {
result.put("prize", false);
}
return result;
}
}