https://blog.csdn.net/huyuyang6688/article/details/50480687
如有4個元素A、B、C、D,權重分別為1、2、3、4,隨機結果中A:B:C:D的比例要為1:2:3:4。
總體思路:累加每個元素的權重A(1)-B(3)-C(6)-D(10),則4個元素的的權重管轄區間分別為[0,1)、[1,3)、[3,6)、[6,10)。
然后隨機出一個[0,10)之間的隨機數。落在哪個區間,則該區間之后的元素即為按權重命中的元素。
實現方法:
利用TreeMap,則構造出的一個樹為:
B(3)
/ \
/ \
A(1) D(10)
/
/
C(6)
然后,利用treemap.tailMap().firstKey()即可找到目標元素。
(3落在70%權重,7落在70-95的權重,10落在95-100的權重)
https://www.bbsmax.com/A/LPdo4pk2z3/
三、依賴不可控的物理隨機數
什么意思呢,先看個圖,看完你就知道了
明白了吧,呵呵,這就是現如今灰常流行的一種抽獎算法,絕對公平、絕對透明、絕對木有暗箱(除非偷偷給你換了抽獎號碼)!但是這種方法唯一的缺點是無法實時抽獎,只能事后抽獎。也就是只能拿個抽獎號等着上帝的眷顧,阿門。。。
例如游戲中打敗一個boss,會掉落下面其中一個物品,而每個物品都有一定概率: 1. 靴子 20% 2. 披風 25% 3. 飾品 10% 4. 雙手劍 5% 5. 金幣袋 40% 現在的問題就是如何根據概率掉落一個物品給玩家。
一. 一般算法:生成一個列表,分成幾個區間,例如列表長度100,1-20是靴子的區間,21-45是披風的區間等,然后隨機從100取出一個數,看落在哪個區間。算法時間復雜度:預處理O(MN),隨機數生成O(1),空間復雜度O(MN),其中N代表物品種類,M則由最低概率決定。
二、離散算法:也就是上面的改進,竟然1-20都是靴子,21-45都是披風,那抽象成小於等於20的是靴子,大於20且小於等於45是披風,就變成幾個點[20,45,55,60,100],然后也是從1到99隨機取一個數R,按順序在這些點進行比較,知道找到第一個比R大的數的下標,比一般算法減少占用空間,還可以采用二分法找出R,這樣,預處理O(N),隨機數生成O(logN),空間復雜度O(N)。 請點擊查看詳細:http://www.cnblogs.com/miloyip/archive/2010/04/21/1717109.html
三、Alias Method Alias Method就不太好理解,實現很巧妙,推薦先看看這篇文章:http://www.keithschwarz.com/darts-dice-coins/ 大致意思:把N種可能性拼裝成一個方形(整體),分成N列,每列高度為1且最多兩種可能性,可能性抽象為某種顏色,即每列最多有兩種顏色,且第n列中必有第n種可能性,這里將第n種可能性稱為原色。 想象拋出一個硬幣,會落在其中一列,並且是落在列上的一種顏色。這樣就得到兩個數組:一個記錄落在原色的概率是多少,記為Prob數組,另一個記錄列上非原色的顏色名稱,記為Alias數組,若該列只有原色則記為null。
之前的例子,為了便於演示換成分數 1. 靴子 20% -> 1/4 2. 披風 25% -> 1/5 3. 飾品 10% -> 1/10 4. 雙手劍 5% -> 1/20 5. 金幣袋 40% -> 2/5 然后每個都乘以5(使每列高度為1),再拼湊成方形 拼湊原則:每次都從大於等於1的方塊分出一小塊,與小於1的方塊合成高度為1
由上圖方形可得到兩個數組: Prob: [3/4, 1/4, 1/2, 1/4, 1] Alias: [4, 4, 0, 1, null] (記錄非原色的下標)
之后就根據Prob和Alias獲取其中一個物品 隨機產生一列C,再隨機產生一個數R,通過與Prob[C]比較,R較大則返回C,反之返回Alias[C]。
Alias Method 復雜度:預處理O(NlogN),隨機數生成O(1),空間復雜度O(2N)
https://www.cnblogs.com/younggun/p/3249772.html
簡介
最近閑的無聊,將以前做的一個微信抽獎小demo拿來分享一下,以便加深印象。
效果圖

業務要求
在一個獎池中放一堆獎品,分別給它們設置不同的數量,比如一等獎A10個,二等獎B,30個,三等獎C10個,然后設置參與人數C人
條件是:
當獎品數大於參與人數,100%中獎。
當獎品A發放完是,不能被抽中。
當獎品發放完畢是,該抽獎活動自動下架。
同一個用戶如果中獎了,將不能繼續參與該活動。
這里只討論下其中的核心算法的設計及一個示例函數,算法之外的系統控制暫不提及。
實現抽獎的方法應該有很多,沒有仔細去考察和搜索那些非常復雜的算法,這里僅做了一個簡單的假設,並在此基礎上推出后面所有的控制邏輯。
實現方法
java核心抽獎代碼如下:
public class LotteryUtil { /** * * @param orignalRates 傳人每一個獎品概率的集合,(集合的第一個參數是0.0 表示百分比中獎) * @return */ public static int lottery(List<Double> orignalRates) { if (orignalRates == null || orignalRates.isEmpty()) { return -1; } int size = orignalRates.size(); // 計算總概率,這樣可以保證不一定總概率是1 double sumRate = 0d; for (double rate : orignalRates) { sumRate += rate; } // 計算每個物品在總概率的基礎下的概率情況 List<Double> sortOrignalRates = new ArrayList<Double>(size); Double tempSumRate = 0d; /*遍歷獎品概率的集合,計算每一個獎品的中間區間*/ for (double rate : orignalRates) { tempSumRate += rate; sortOrignalRates.add(tempSumRate/sumRate); } // 根據區塊值來獲取抽取到的物品索引 double nextDouble = Math.random(); sortOrignalRates.add(nextDouble); Collections.sort(sortOrignalRates); return sortOrignalRates.indexOf(nextDouble); } }
抽獎的業務邏輯代碼如下
/*awardItems獲取獎品的一個集合*/ if (activityUserDao.getCountByOpenId(Award.WHEEL_AWARD_TYPE, wid, open_id) <= 0) { /* awardItems獲取獎品的一個集合 */ List<Award> awardItems = awardDao.getByActivity(aw.getWheel_id(), Award.WHEEL_AWARD_TYPE); /* lotterys存放每一個獎品的中獎概率集合 */ List<Double> lotterys = new ArrayList<Double>(); /* 獲取總的獎品數量 */ int count = 0; for (Award a : awardItems) { count += a.getProvide_count(); } if (aw.getPeople_count() <= count) { lotterys.add(0.0); // 100%中獎 } else { /* 預計參與人數減去獎品數 除以參與人數 = 未中獎概率 */ lotterys.add((double) (aw.getPeople_count() - count) / (double) aw.getPeople_count()); } /* 遍歷獎品集合,獲取每一個獎品中獎概率 */ for (Award a : awardItems) { if (a.getOver_count() > 0) { lotterys.add((double) a.getProvide_count() / (double) aw.getPeople_count()); } else { lotterys.add(0.0); } } // 計算中獎概率 int index = LotteryUtil.lottery(lotterys); if (index > 0) {// 中獎 Award a = awardItems.get(index - 1); long key = Math.round(Math.random() * (999999 - 100000) + 100000); // 6位數中獎序列號 // 修改商品剩余數量 + 記錄序列號 if (awardDao.doLowerOverCount(a.getAward_id()) > 0 && activityUserDao.doInsert(new ActivityUser(aw.getPublic_id(), Award.WHEEL_AWARD_TYPE, wid, a.getAward_id(), key + "", open_id)) > 0) { rb.setCode(index); rb.setData(key); rb.setMessage(a.getAward_name()); } else { rb.setCode(0); } } // 抽獎記錄 activityRecordDao.doInsert(new ActivityRecord(open_id, Award.WHEEL_AWARD_TYPE, wid, request.getRemoteAddr()));
前端抽獎工具類
/** * 注意:本插件運用了rem屏幕適配方案,一律采用rem作為單位,若項目中不是采用這種方案的,此段代碼不會影響功能使用,僅會影響控件樣式 */ (function(win, doc, $) { var defaultOpt = { rotateNum: 5, //轉盤轉動圈數 body: "", //大轉盤整體的選擇符或zepto對象 disabledHandler: function() {}, //禁止抽獎時回調 clickCallback: function() {}, //點擊抽獎按鈕,再次回調中實現訪問后台獲取抽獎結果,拿到抽獎結果后顯示抽獎畫面 KinerLotteryHandler: function(deg) {} //抽獎結束回調 }; function KinerLottery(opts) { this.opts = $.extend(true, {}, defaultOpt, opts); this.doing = false; this.init(); } KinerLottery.prototype.setOpts = function(opts) { this.opts = $.extend(true, {}, defaultOpt, opts); this.init(); }; KinerLottery.prototype.init = function() { var self = this; this.defNum = this.opts.rotateNum * 360; //轉盤需要轉動的角度 // console.log(this.defNum); // alert(this.defNum); //點擊抽獎 $('#box').on('click', ".KinerLotteryBtn", function() { if($(this).hasClass('start') && !self.doing) { self.opts.clickCallback.call(self); } else { var key = $(this).hasClass('no_start') ? "noStart" : $(this).hasClass('completed') ? "completed" : "illegal"; self.opts.disabledHandler(key); } }); $(this.opts.body).find('.KinerLotteryContent').get(0).addEventListener('webkitTransitionEnd', function() { self.doing = false; var deg = $(self.opts.body).attr('data-deg'); if(self.opts.direction == 0) { $(self.opts.body).attr('data-deg', 360 - deg); $(self.opts.body).find('.KinerLotteryContent').css({ '-webkit-transition': 'none', 'transition': 'none', '-webkit-transform': 'rotate(' + (deg) + 'deg)', 'transform': 'rotate(' + (deg) + 'deg)' }); self.opts.KinerLotteryHandler(360 - deg); } else { $(self.opts.body).attr('data-deg', deg); $(self.opts.body).find('.KinerLotteryContent').css({ '-webkit-transition': 'none', 'transition': 'none', '-webkit-transform': 'rotate(' + (-deg) + 'deg)', 'transform': 'rotate(' + (-deg) + 'deg)' }); self.opts.KinerLotteryHandler(deg); } }); }; KinerLottery.prototype.goKinerLottery = function(_deg) { if(this.doing) { return; } var deg = _deg + this.defNum; var realDeg = this.opts.direction == 0 ? deg : -deg; this.doing = true; $(this.opts.body).find('.KinerLotteryBtn').addClass('doing'); $(this.opts.body).find('.KinerLotteryContent').css({ '-webkit-transition': 'all 5s', 'transition': 'all 5s', '-webkit-transform': 'rotate(' + (realDeg) + 'deg)', 'transform': 'rotate(' + (realDeg) + 'deg)' }); $(this.opts.body).attr('data-deg', _deg); }; win.KinerLottery = KinerLottery; })(window, document, $);
前端js調用抽獎類
/** * @author wjb * @description * @version 1.0.0 2017/2/11 */ app.controller("wheelOneController", ['$scope', '$stateParams', '$neu_', 'awardService', '$filter', '$timeout', 'util.alert', 'cfg', 'wxService', function($scope, $stateParams, $neu_, awardService, $filter, $timeout, alert, cfg, wxService) { /*中獎開始時間*/ $scope.wheelStatu = { start: true, noStart: false, completed: false } /*錯誤信息提示*/ $scope.errorMsg = ""; /*定義獎品數據變量*/ $scope.awards = []; /*活動的id和活動的微信公眾號ID*/ var activity_id = $neu_.isEmpty($stateParams.activity_id) ? 1 : $stateParams.activity_id; var public_id = $neu_.isEmpty($stateParams.public_id) ? 1 : $stateParams.public_id; var open_id = $neu_.isEmpty($stateParams.open_id) ? cfg.openId : $stateParams.open_id; cfg.public_id = public_id; cfg.open_id = open_id; cfg.activity_id = activity_id; //alert(cfg.public_id+"=="+cfg.activity_id+"=="+cfg.open_id ); /*獲取活動信息*/ wxService.setConfig(); awardService.getWheelInfo(activity_id, public_id).then(function(res) { //console.dir(res) if(res.success) { $scope.wheelStatu.start = true; } else { if(res.code == 1 || res.code == 3) { $scope.wheelStatu.noStart = true; } else if(res.code == 2) { $scope.wheelStatu.completed = true; } $scope.errorMsg = res.msg; } $scope.wheelInfo = res.data; }); awardService.getAwards(activity_id, public_id).then(function(res) { $scope.awards = res.data; }); /*獎品預覽*/ var result = []; $scope.showPic = function(pic) { if(result.length == 0) { $neu_.each($scope.awards, function(item) { result.push(cfg.resourcePath + item.img); }); } wxService.previewImage(cfg.resourcePath + pic, result); } /*中獎結果集*/ $scope.result = []; $scope.user = { user_name: '', phone: '', activity_id: activity_id, user_openid: open_id }; /** * 提交中獎人信息 */ $scope.submit = function() { $(".actBody_close").click(); $scope.isLoading = true; awardService.updateLotteryUser($scope.user).then(function(res) { $scope.isLoading = false; if(res.success) { alert('您的中獎信息已備案,我們的客服人員稍后會聯系您,如何領取獎品'); } else { alert('提交失敗'); } }) //alert('哈哈上當了吧^_^_^_^') } $scope.load = function() { $timeout(function() { ActBounced(); $("#layer").hide(); new KinerLottery({ rotateNum: 5, //轉盤轉動圈數 body: "#box", //大轉盤整體的選擇符或zepto對象 direction: 0, //0為順時針轉動,1為逆時針轉動 disabledHandler: function(key) { switch(key) { case "noStart": $scope.$apply(function() { alert($scope.errorMsg); }); break; case "completed": $scope.$apply(function() { alert($scope.errorMsg); }); break; } }, //禁止抽獎時回調 clickCallback: function() { var this_ = this; //此處訪問接口獲取獎品 $scope.isLoading = true; awardService.startAweel(activity_id, open_id).then(function(res) { $scope.isLoading = false; if(isDebug){ this_.goKinerLottery(0); }else{ if(res.success) { var index = cfg.isDebug ? Math.floor(Math.random() * 5) : res.code; $scope.result = $filter('awardToAngle')(index, $scope.awards); if(index == 0) { this_.opts.disabledHandler("noStart"); } else { this_.goKinerLottery($scope.result[1]); } } else { alert(res.msg); } } }) }, //點擊抽獎按鈕,再次回調中實現訪問后台獲取抽獎結果,拿到抽獎結果后顯示抽獎畫面 KinerLotteryHandler: function(deg) { $("#smallActAdv").click(); } }); }, 500); /*分享授權的地址*/ $timeout(function(){ var share ={shareUrl:"weixin/oauth.html?isBase=true&type=6&a_id="+activity_id+"&p_id="+public_id+"&appid="+cfg.appId}; wxService.onMenuShareAppMessage(share); },3000) } }])
以上是抽獎的主要代碼
作者:薪火設計
鏈接:https://www.jianshu.com/p/8e6c43f90faa
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。
一個簡單抽獎算法的實現以及如何預防超中
需求
每個用戶每天有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 |
抽獎的時候 先生成一個隨機值
randNum = new Random().nextInt(totalWeight); // totalWeight = 上面權重列之和
判斷該隨機值在哪一個區間 如
randNum = 8944 落在未中獎區間 未中獎 randNum = 944 落在1元區間 中了一元
如果想增大中iphone6s的概率 調整權重值即可 如將權重改為1000, 則區間變為[6600,7600)
同時會為每種獎品設置庫存 如
日期 | 獎品 | 庫存 |
---|---|---|
3.1 | 一元 | 5000 |
中獎后 會減庫存 但假如庫存只剩1個了 有10個用戶同時落入一元區間 如何避免1-10=-9
的情況呢?
解決方法
update award_stock set stock = stock - 1 where award_id = ? and stock > 0;
即是否中獎除了落入區間外 還需判斷減庫存是否成功
如果減庫存失敗 仍當做未中獎
一旦一種獎品庫存為0 下次計算區間的時候 將它排除 如一元獎品庫存已為0 這時各獎品的區間變化為
獎品 | 權重 | 區間 | |
---|---|---|---|
2元 | 1000 | [0,1000) | |
3元 | 500 | [1000,1500) | |
5元 | 100 | [1500, 1600) | |
iphone6s | 1 | [1600, 1601) | |
未中獎 | 59409 | [1601,61010) | 61010/1601=38 此時中獎概率變小了 相當於抽38次中一次 |
驗證上述算法
看是否能抽完所有獎品 如某天的獎品配置如下 (權重默認等於庫存)
日期 | 獎品 | 權重 | 庫存 |
---|---|---|---|
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
可見 實際不用到理論抽獎次數 即可抽完所有獎品
Java抽獎概率算法
序號 | 獎品名稱 | 獎品編號 | 抽到的概率 |
1 | 再來一次 | P1 | 0.2 |
2 | 本站VIP一年 | P2 | 0.1 |
3 | 謝謝參與 | P3 | 0.4 |
4 | 50金幣 | P4 | 0.3 |
5 | Iphone 6 | P5 | 0.0 |
6 | Ipad Air2 | P6 | -0.1 |
7 | 100元手機話費 | P7 | 0.008 |
數據很簡單,那么就直接看代碼了
/** * 獎品類 * @author:rex * @date:2014年10月20日 * @version:1.0 */ public class Gift { private int index; private String gitfId; private String giftName; private double probability; public Gift(int index, String gitfId, String giftName, double probability) { this.index = index; this.gitfId = gitfId; this.giftName = giftName; this.probability = probability; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public String getGitfId() { return gitfId; } public void setGitfId(String gitfId) { this.gitfId = gitfId; } public String getGiftName() { return giftName; } public void setGiftName(String giftName) { this.giftName = giftName; } public double getProbability() { return probability; } public void setProbability(double probability) { this.probability = probability; } @Override public String toString() { return "Gift [index=" + index + ", gitfId=" + gitfId + ", giftName=" + giftName + ", probability=" + probability + "]"; } }
/** * 不同概率抽獎工具包 * @author:rex * @date:2014年10月20日 * @version:1.0 */ public class LotteryUtil { /** * 抽獎 * * @param orignalRates 原始的概率列表,保證順序和實際物品對應 * @return 物品的索引 */ public static int lottery(List<Double> orignalRates) { if (orignalRates == null || orignalRates.isEmpty()) { return -1; } int size = orignalRates.size(); // 計算總概率,這樣可以保證不一定總概率是1 double sumRate = 0d; for (double rate : orignalRates) { sumRate += rate; } // 計算每個物品在總概率的基礎下的概率情況 List<Double> sortOrignalRates = new ArrayList<Double>(size); Double tempSumRate = 0d; for (double rate : orignalRates) { tempSumRate += rate; sortOrignalRates.add(tempSumRate / sumRate); } // 根據區塊值來獲取抽取到的物品索引 double nextDouble = Math.random(); sortOrignalRates.add(nextDouble); Collections.sort(sortOrignalRates); return sortOrignalRates.indexOf(nextDouble); } public static int getJD(List<Double> orignalRates) { if (orignalRates == null || orignalRates.isEmpty()) { return -1; } int size = orignalRates.size(); // 計算總概率,這樣可以保證不一定總概率是1 double sumRate = 0d; for (double rate : orignalRates) { sumRate += rate; } // 計算每個物品在總概率的基礎下的概率情況 List<Double> sortOrignalRates = new ArrayList<Double>(size); Double tempSumRate = 0d; for (double rate : orignalRates) { tempSumRate += rate; sortOrignalRates.add(tempSumRate / sumRate); } // 根據區塊值來獲取抽取到的物品索引 double nextDouble = Math.random(); sortOrignalRates.add(nextDouble); Collections.sort(sortOrignalRates); return sortOrignalRates.indexOf(nextDouble); } }
/** * 不同概率抽獎 * @author:rex * @date:2014年10月20日 * @version:1.0 */ public class LotteryTest { public static void main(String[] args) { List<Gift> gifts = new ArrayList<Gift>(); // 序號==物品Id==物品名稱==概率 gifts.add(new Gift(1, "P1", "物品1", 0.2d)); gifts.add(new Gift(2, "P2", "物品2", 0.2d)); gifts.add(new Gift(3, "P3", "物品3", 0.4d)); gifts.add(new Gift(4, "P4", "物品4", 0.3d)); gifts.add(new Gift(5, "P5", "物品5", 0d)); gifts.add(new Gift(6, "P6", "物品6", -0.1d)); gifts.add(new Gift(7, "P7", "物品7", 0.008d)); List<Double> orignalRates = new ArrayList<Double>(gifts.size()); for (Gift gift : gifts) { double probability = gift.getProbability(); if (probability < 0) { probability = 0; } orignalRates.add(probability); } // statistics Map<Integer, Integer> count = new HashMap<Integer, Integer>(); double num = 1000000; for (int i = 0; i < num; i++) { int orignalIndex = LotteryUtil.lottery(orignalRates); Integer value = count.get(orignalIndex); count.put(orignalIndex, value == null ? 1 : value + 1); } for (Entry<Integer, Integer> entry : count.entrySet()) { System.out.println(gifts.get(entry.getKey()) + ", count=" + entry.getValue() + ", probability=" + entry.getValue() / num); } } }
輸出
Gift [index=1, gitfId=P1, giftName=物品1, probability=0.2], count=180854, probability=0.180854 Gift [index=2, gitfId=P2, giftName=物品2, probability=0.2], count=180789, probability=0.180789 Gift [index=3, gitfId=P3, giftName=物品3, probability=0.4], count=361198, probability=0.361198 Gift [index=4, gitfId=P4, giftName=物品4, probability=0.3], count=269950, probability=0.26995 Gift [index=7, gitfId=P7, giftName=物品7, probability=0.008], count=7209, probability=0.007209
不同概率的抽獎原理很簡單
就是把0到1的區間分塊,而分塊的依據就是物品占整個的比重,再根據隨機數種子來產生0-1中間的某個數,來判斷這個數是落在哪個區間上,而對應的就是抽到了那個物品。隨機數理論上是概率均等的,產生的每個數理論上也應該概率均等,那么相應的區間所含數的多少就體現了抽獎物品概率的不同。(p.s. 當然數目是數不清楚的,具體抽象話了點)
這個實例的數據可以說明
1. 概率可以是負數和0,當然實際上中應該不會(p.s. 正常情況下可能真的有0,比如抽個iphone5,當然是抽不到的了,這個時候,構建禮物(List gifts)的時候最好就不要加這個進去),還有可以把負數的處理放到抽獎工具類(LotteryUtil)中;
2. 所有禮物加起來的概率可以不是1,可以認為這里的概率是一個權重。
轉載至:http://www.blogjava.net/lishunli/archive/2012/10/17/389763.html
java簡單的抽獎方法——配置概率(100以內隨機數的一個解決方案)
用戶抽獎的大致思路就是,當用戶點擊抽獎后,后台隨機算出100以內的一個隨機數,然后查看該隨機數是否在中獎概率范圍內。
抽獎概率寫死的方法:
/** * 抽獎概率 * * @return rand */ public static int randomInt() { int randomNum = new Random().nextInt(100) + 1; if (randomNum == 1) { return 1; } else if (randomNum >= 2 && randomNum <= 5) { return 2; } else if (randomNum >= 6 && randomNum <= 10) { return 3; } else if (randomNum >= 11 && randomNum <= 50) { return 4; } else { return 5; } }
從數據庫讀取概率配置(本次抽獎分五等獎)
public static int randomInt(LotteryPrizeEntry lotteryPrize) { //概率不能為空,至少為0 if (lotteryPrize.getFirstPrize() + lotteryPrize.getSecondPrize() + lotteryPrize.getThirdPrize() + lotteryPrize.getFourthPrize() + lotteryPrize.getFifthPrize() > 100) { return 0; } int randomNum = new Random().nextInt(100) + 1; List<Integer> list = new ArrayList<Integer>(); list.add(lotteryPrize.getFirstPrize()); list.add(lotteryPrize.getSecondPrize()); list.add(lotteryPrize.getThirdPrize()); list.add(lotteryPrize.getFourthPrize()); list.add(lotteryPrize.getFifthPrize()); int prize = 1; //獎品級數 int num = 0; //存中獎概率數 int num2 = 1;//存中獎概率數 for (Integer i : list) { num = +i; if (i >= 1) { if (randomNum >= num2 && randomNum <= num) { return prize; } } prize++; //獎級加一 num2 = +i; } return prize;
中獎概率配置表:
BEGIN_TIME DATE 開始時間
END_TIME DATE 結束時間
LOTTERY_ISENABLE NUMBER 是否開啟 0關閉1開啟
FIRST_PRIZE NUMBER 一等獎概率
SECOND_PRIZE NUMBER 二等獎概率
THIRD_PRIZE NUMBER 三等獎概率
FOURTH_PRIZE NUMBER 四等獎概率
FIFTH_PRIZE NUMBER 五等獎概率
http://www.zui da ima.com/share/2933089644039168.htm
權重隨機算法的java實現
一、概述
平時,經常會遇到權重隨機算法,從不同權重的N個元素中隨機選擇一個,並使得總體選擇結果是按照權重分布的。如廣告投放、負載均衡等。
如有4個元素A、B、C、D,權重分別為1、2、3、4,隨機結果中A:B:C:D的比例要為1:2:3:4。
總體思路:累加每個元素的權重A(1)-B(3)-C(6)-D(10),則4個元素的的權重管轄區間分別為[0,1)、[1,3)、[3,6)、[6,10)。然后隨機出一個[0,10)之間的隨機數。落在哪個區間,則該區間之后的元素即為按權重命中的元素。
實現方法:
利用TreeMap,則構造出的一個樹為:
B(3)
/ \
/ \
A(1) D(10)
/
/
C(6)
然后,利用treemap.tailMap().firstKey()即可找到目標元素。
當然,也可以利用數組+二分查找來實現。
二、源碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package
com.xxx.utils;
import
com.google.common.base.Preconditions;
import
org.apache.commons.math3.util.Pair;
import
org.slf4j.Logger;
import
org.slf4j.LoggerFactory;
import
java.util.List;
import
java.util.SortedMap;
import
java.util.TreeMap;
public
class
WeightRandom<K,V
extends
Number> {
private
TreeMap<Double, K> weightMap =
new
TreeMap<Double, K>();
private
static
final
Logger logger = LoggerFactory.getLogger(WeightRandom.
class
);
public
WeightRandom(List<Pair<K, V>> list) {
Preconditions.checkNotNull(list,
"list can NOT be null!"
);
for
(Pair<K, V> pair : list) {
double
lastWeight =
this
.weightMap.size() ==
0
?
0
:
this
.weightMap.lastKey().doubleValue();
//統一轉為double
this
.weightMap.put(pair.getValue().doubleValue() + lastWeight, pair.getKey());
//權重累加
}
}
public
K random() {
double
randomWeight =
this
.weightMap.lastKey() * Math.random();
SortedMap<Double, K> tailMap =
this
.weightMap.tailMap(randomWeight,
false
);
return
this
.weightMap.get(tailMap.firstKey());
}
}
|
三、性能
4個元素A、B、C、D,其權重分別為1、2、3、4,運行1億次,結果如下:
元素 | 命中次數 | 誤差率 |
A | 10004296 | 0.0430% |
B | 19991132 | 0.0443% |
C | 30000882 | 0.0029% |
D | 40003690 | 0.0092% |
從結果,可以看出,准確率在99.95%以上。
四、另一種實現
利用B+樹的原理。葉子結點存放元素,非葉子結點用於索引。非葉子結點有兩個屬性,分別保存左右子樹的累加權重。如下圖:
看到這個圖,聰明的你應該知道怎么隨機了吧。
此方法的優點是:更改一個元素,只須修改該元素到根結點那半部分的權值即可。
end
兩類,第一類是常見的有等級的抽獎活動,如一等、二等、三等獎等等,廢話不多說,直接貼代碼:
推薦:簡單抽獎用的算法
[/* 每種獎品的概率 * 總概率為333 * */ int a1 = 1; int a4 = 200; int a6 = 50; int a8 = 1; int a9
// 分別為一、二、三、四等將的獎品數量,最后一個為未中獎的數量。
private static final Integer[] lotteryList = {5, 10, 20, 40, 100}; private int getSum() { int sum = 0; for (int v : lotteryList) { sum += v; } return sum; } private int getLotteryLevel() { Random random = new Random(System.nanoTime()); int sum = getSum(); for (int i = 0; i < lotteryList.length; ++i) { int randNum = Math.abs(random.nextInt()) % sum; if (randNum <= lotteryList[i]) { return i; } else { sum -= lotteryList[i]; } } return -1; }
另一類是不分等級的抽獎活動,僅需要參與人數與獎品總數,各獎品中獎概率相等。代碼如下:
//另一種抽獎算法,用於公司抽獎,即總參與人數與獎品數固定。
private static final int lotteryNum = 75; private static final int total = 175; private static Set<Integer> lotterySet = new HashSet<Integer>(); static { for (int i=1; i <= lotteryNum; ++i) { lotterySet.add(total*i/lotteryNum); } } private int getLotteryNum2() { Random rand = new Random(System.nanoTime()); int randNum = Math.abs(rand.nextInt()) % total; if (lotterySet.contains(randNum)) { return randNum*lotteryNum/total; } return -1; }
http://www.itboth.com/d/6R3YJr/java