一、概述
平時,經常會遇到權重隨機算法,從不同權重的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()即可找到目標元素。
當然,也可以利用數組+二分查找來實現。
二、源碼
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) { Preconditions.checkArgument(pair.getValue().doubleValue() > 0, String.format("非法權重值:pair=%s", pair)); 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+樹的原理。葉子結點存放元素,非葉子結點用於索引。非葉子結點有兩個屬性,分別保存左右子樹的累加權重。如下圖:
看到這個圖,聰明的你應該知道怎么隨機了吧。
此方法的優點是:更改一個元素,只須修改該元素到根結點那半部分的權值即可。
【項目實戰】——Java根據獎品權重計算中獎概率實現抽獎(適用於砸金蛋、大轉盤等抽獎活動)
雙蛋節(聖誕+元旦)剛剛過去,前幾天項目上線的砸金蛋活動也圓滿結束。
現在在許多網站上都會有抽獎的活動,抽獎的算法也是多種多樣,這里介紹一下如何根據每種獎品的權重來抽獎,適用於多種抽獎形式。
獎品設置
比如現在舉行一次砸金蛋活動中,獎品如下:
獎品夠豐富的哇,香車美女豪宅都有了~不過由於法律的原因,活人是不能贈送的,所以一等獎只能送海報了~
獎品在數據庫中的存儲情況
抽獎實現
獎品實體 Prize.java
public class Prize { private int id;//獎品id private String prize_name;//獎品名稱 private int prize_amount;//獎品(剩余)數量 private int prize_weight;//獎品權重 //getter、setter
這里只考慮最簡單的抽獎實現,所以暫時只為獎品設計如上4個字段。
見注釋,prize_name表示獎品名稱;prize_amount表示獎品數量,即本次抽獎活動計划發放此獎品的數量;prize_weight表示獎品權重,表示獎品被抽到的幾率的比重,權重越大,被抽到的幾率越大,比如本次砸金蛋活動有4種獎品,權重分別是1、2、3、4,總權重是10,那么每種獎品被抽到的幾率就是1/10,2/10,3/10,4/10。
核心算法:
/** * 根據Math.random()產生一個double型的隨機數,判斷每個獎品出現的概率 * @param prizes * @return random:獎品列表prizes中的序列(prizes中的第random個就是抽中的獎品) */ public int getPrizeIndex(List<Prize> prizes) { DecimalFormat df = new DecimalFormat("######0.00"); int random = -1; try{ //計算總權重 double sumWeight = 0; for(Prize p : prizes){ sumWeight += p.getPrize_weight(); } //產生隨機數 double randomNumber; randomNumber = Math.random(); //根據隨機數在所有獎品分布的區域並確定所抽獎品 double d1 = 0; double d2 = 0; for(int i=0;i<prizes.size();i++){ d2 += Double.parseDouble(String.valueOf(prizes.get(i).getPrize_weight()))/sumWeight; if(i==0){ d1 = 0; }else{ d1 +=Double.parseDouble(String.valueOf(prizes.get(i-1).getPrize_weight()))/sumWeight; } if(randomNumber >= d1 && randomNumber <= d2){ random = i; break; } } }catch(Exception e){ System.out.println("生成抽獎隨機數出錯,出錯原因:" +e.getMessage()); } return random; }
抽獎的邏輯可以用下面這張圖表示:
分析:如上圖,為了便於計算和理解,設置每種獎品的權重分別為1,2,3,4,所以被抽到的概率分別為0.1,0.2,0.3,0.4(本次活動中獎概率為100%)。
先生成一個隨機數randomNumber,然后根據隨機數所處區域判斷獎品:
0<randomNumber<=0.1 表示抽中一等獎 0.1<randomNumber<=0.3 表示抽中二等獎 0.3<randomNumber<=0.6 表示抽中三等獎 0.6<randomNumber<=1.0 表示抽中四等獎
抽獎測試
public static void main(String[] agrs) { int i = 0; PrizeMathRandom a = new PrizeMathRandom(); int[] result=new int[4]; List<Prize> prizes = new ArrayList<Prize>(); Prize p1 = new Prize(); p1.setPrize_name("范冰冰海報"); p1.setPrize_weight(1);//獎品的權重設置成1 prizes.add(p1); Prize p2 = new Prize(); p2.setPrize_name("上海紫園1號別墅"); p2.setPrize_weight(2);//獎品的權重設置成2 prizes.add(p2); Prize p3 = new Prize(); p3.setPrize_name("奧迪a9"); p3.setPrize_weight(3);//獎品的權重設置成3 prizes.add(p3); Prize p4 = new Prize(); p4.setPrize_name("雙色球彩票"); p4.setPrize_weight(4);//獎品的權重設置成4 prizes.add(p4); System.out.println("抽獎開始"); for (i = 0; i < 10000; i++)// 打印100個測試概率的准確性 { int selected=a.getPrizeIndex(prizes); System.out.println("第"+i+"次抽中的獎品為:"+prizes.get(selected).getPrize_name()); result[selected]++; System.out.println("--------------------------------"); } System.out.println("抽獎結束"); System.out.println("每種獎品抽到的數量為:"); System.out.println("一等獎:"+result[0]); System.out.println("二等獎:"+result[1]); System.out.println("三等獎:"+result[2]); System.out.println("四等獎:"+result[3]); }
嘗試抽獎10000次的結果如下:
一等獎:962 二等獎:2007 三等獎:3043 四等獎:3988
類獎品獲獎次數比例剛好大約為1:2:3:4,學過概率的你肯定知道抽獎次數越多,測試結果越准確~
Tips:
如果計划中獎率是100%的話,那么10個獎品只能抽獎10次,所以還要根據實際情況設置每種獎品數量和權重。
如果需要設置中獎率不為100%,可以添加一個“偽獎品”,並為其設置權重,那么抽到這個“偽獎品”的概率就是不中獎的概率。
如果在抽獎過程中某類獎品抽完了,可以做個判斷,如果此獎品的剩余數量為0,則重新抽取獎品,直到抽到其他獎品位置。
參考文章: