一個簡單抽獎算法的實現以及如何預防超中
需求java
每一個用戶天天有3次抽獎機會;
抽獎獎池一共分爲6檔內容:現金紅包1元,2元,3元,5元,iphone6s,謝謝參與;
支持天天調整和配置抽獎的獲獎機率;算法
算法介紹
每種獎品都有一個權重 對應一個區間 若落入該區間就表示中獎 調整區間大小就可改變獲獎機率 即調整權重值便可併發
獎品 | 權重 | 區間 | ||
---|---|---|---|---|
1元 | 5000 | [0,5000) | ||
2元 | 1000 | [5000,6000) | ||
3元 | 500 | [6000,6500) | ||
5元 | 100 | [6500, 6600) | ||
iphone6s | 1 | [6600, 6601) | ||
未中獎 | 59409 | [6601,66010) | 假設設定抽10次中一次, 未中獎權重 = 抽檢機率導數獎品數-獎品數 = 106601-6601 = 59409 |
抽獎的時候 先生成一個隨機值dom
randNum = new Random().nextInt(totalWeight); // totalWeight = 上面權重列之和
判斷該隨機值在哪個區間 如iphone
randNum = 8944 落在未中獎區間 未中獎 randNum = 944 落在1元區間 中了一元
若是想增大中iphone6s的機率 調整權重值便可 如將權重改成1000, 則區間變爲[6600,7600)
同時會爲每種獎品設置庫存 如spa
日期 | 獎品 | 庫存 |
---|---|---|
3.1 | 一元 | 5000 |
中獎後 會減庫存 但假如庫存只剩1個了 有10個用戶同時落入一元區間 如何避免1-10=-9
的狀況呢?
解決方法線程
update award_stock set stock = stock - 1 where award_id = ? and stock > 0;
便是否中獎除了落入區間外 還需判斷減庫存是否成功
若是減庫存失敗 仍當作未中獎code
一旦一種獎品庫存爲0 下次計算區間的時候 將它排除 如一元獎品庫存已爲0 這時各獎品的區間變化爲ip
獎品 | 權重 | 區間 | |
---|---|---|---|
2元 | 1000 | [0,1000) | |
3元 | 500 | [1000,1500) | |
5元 | 100 | [1500, 1600) | |
iphone6s | 1 | [1600, 1601) | |
未中獎 | 59409 | [1601,61010) | 61010/1601=38 此時中獎機率變小了 至關於抽38次中一次 |
驗證上述算法
看是否能抽完全部獎品 如某天的獎品配置以下 (權重默認等於庫存)get
日期 | 獎品 | 權重 | 庫存 |
---|---|---|---|
3.1 | 1元 | 5000 | 5000 |
3.1 | 2元 | 1000 | 1000 |
3.1 | 3元 | 500 | 500 |
3.1 | 5元 | 100 | 100 |
3.1 | iphone6s | 1 | 1 |
3.1 | 未中獎 | 59409 | 59409 |
假設日活用戶數爲3萬 每一個用戶可抽3次
java代碼
final Map<String, Integer> awardStockMap = new ConcurrentHashMap<>(); // 獎品 <--> 獎品庫存 awardStockMap.put("1", 5000); awardStockMap.put("2", 1000); awardStockMap.put("3", 500); awardStockMap.put("5", 100); awardStockMap.put("iphone", 1); awardStockMap.put("未中獎", 59409); //6601*10 -6601 //權重默認等於庫存 final Map<String, Integer> awardWeightMap = new ConcurrentHashMap<>(awardStockMap); // 獎品 <--> 獎品權重 int userNum = 30000; // 日活用戶數 int drawNum = userNum * 3; // 天天抽獎次數 = 日活數*抽獎次數 Map<String, Integer> dailyWinCountMap = new ConcurrentHashMap<>(); // 天天實際中獎計數 for(int j=0; j<drawNum; j++){ // 模擬每次抽獎 //排除掉庫存爲0的獎品 Map<String, Integer> awardWeightHaveStockMap = awardWeightMap.entrySet().stream().filter(e->awardStockMap.get(e.getKey())>0).collect(Collectors.toMap(e->e.getKey(), e->e.getValue())); int totalWeight = (int) awardWeightHaveStockMap.values().stream().collect(Collectors.summarizingInt(i->i)).getSum(); int randNum = new Random().nextInt(totalWeight); //生成一個隨機數 int prev = 0; String choosedAward = null; // 按照權重計算中獎區間 for(Entry<String,Integer> e : awardWeightHaveStockMap.entrySet() ){ if(randNum>=prev && randNum<prev+e.getValue()){ choosedAward = e.getKey(); //落入該獎品區間 break; } prev = prev+e.getValue(); } dailyWinCountMap.compute(choosedAward, (k,v)->v==null?1:v+1); //中獎計數 if(!"未中獎".equals(choosedAward)){ //未中獎不用減庫存 awardStockMap.compute(choosedAward, (k,v)->v-1); //獎品庫存一 if(awardStockMap.get(choosedAward)==0){ System.out.printf("獎品:%s 庫存爲空%n",choosedAward); //記錄庫存爲空的順序 } } } System.out.println("各獎品中獎計數: "+dailyWinCountMap); //每日各獎品中獎計數
輸出
獎品:iphone 庫存爲空 獎品:5 庫存爲空 獎品:1 庫存爲空 獎品:2 庫存爲空 獎品:3 庫存爲空 每日各獎品中獎計數: {1=5000, 2=1000, 3=500, 5=100, iphone=1, 未中獎=83399}
可知 假如該天抽獎次數能有9萬次的話 能夠抽完全部的獎品 另外因是單線程未考慮減庫存
失敗的狀況 即併發減庫存的狀況
抽獎算法2 存在獎品庫存的前提下 保證每次中獎的機率恆定 如15% 抽100次有15次中獎
final Map<String, Integer> awardStockMap = new ConcurrentHashMap<>(); awardStockMap.put("1", 3000); awardStockMap.put("2", 2000); awardStockMap.put("3", 1500); awardStockMap.put("5", 1000); awardStockMap.put("10", 100); awardStockMap.put("20", 10); awardStockMap.put("50", 5); awardStockMap.put("100", 2); // 權重默認等於庫存 final Map<String, Integer> awardWeightMap = new ConcurrentHashMap<>(awardStockMap); final Map<String, Integer> initAwardStockMap = new ConcurrentHashMap<>(awardStockMap); int drawNum = 50780; // 理論能夠抽完全部獎品所需抽獎次數 = 獎品數×中獎機率導數 = 7617*100/15 final int threshold = 15; //中獎機率 15% Map<String, Integer> dailyWinCountMap = new ConcurrentHashMap<>(); // 天天實際中獎計數 for (int j = 0; j < drawNum; j++) { // 模擬每次抽獎 //肯定是否中獎 int randNum = new Random().nextInt(100); if(randNum>threshold){ dailyWinCountMap.compute("未中獎", (k,v)->v==null?1:v+1); continue; //未中獎 } //中獎 肯定是哪一個獎品 //排除掉庫存爲0的獎品 Map<String, Integer> awardWeightHaveStockMap = awardWeightMap.entrySet().stream().filter(e->awardStockMap.get(e.getKey())>0).collect(Collectors.toMap(e->e.getKey(), e->e.getValue())); if(awardWeightHaveStockMap.isEmpty()){ //獎池已爲空 System.out.printf("第%d次抽獎 獎品已被抽完%n",j); break; } int totalWeight = (int) awardWeightHaveStockMap.values().stream().collect(Collectors.summarizingInt(i->i)).getSum(); randNum = new Random().nextInt(totalWeight); int prev=0; String choosedAward = null; for(Entry<String,Integer> e : awardWeightHaveStockMap.entrySet() ){ if(randNum>=prev && randNum<prev+e.getValue()){ choosedAward = e.getKey(); //落入此區間 中獎 dailyWinCountMap.compute(choosedAward, (k,v)->v==null?1:v+1); break; } prev = prev+e.getValue(); } //減少庫存 awardStockMap.compute(choosedAward, (k,v)->v-1); } System.out.println("每日各獎品中獎計數: "); // 每日各獎品中獎計數 dailyWinCountMap.entrySet().stream().sorted((e1,e2)->e2.getValue()-e1.getValue()).forEach(System.out::println); awardStockMap.forEach((k,v)->{if(v>0){ System.out.printf("獎品:%s, 總庫存: %d, 剩餘庫存: %d%n",k,initAwardStockMap.get(k),v); }});
輸出
第47495次抽獎 獎品已被抽完 每日各獎品中獎計數: 未中獎=39878 1=3000 2=2000 3=1500 5=1000 10=100 20=10 50=5 100=2
可見 實際不用到理論抽獎次數 便可抽完全部獎品