一個簡單抽獎算法的實現以及如何預防超中

一個簡單抽獎算法的實現以及如何預防超中
需求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

可見 實際不用到理論抽獎次數 便可抽完全部獎品

相關文章
相關標籤/搜索