一、概述
平時,經常會遇到權重隨機算法,從不同權重的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%以上。
現在app就是雨后春筍,嗖嗖的往外冒啊,有經驗的、沒經驗的、有資歷的、沒資歷的都想着創業,創業的90%以上都要做一個app出來,好像成了創業的標配。
做了app就得推廣啊,怎么推,發券送錢是最多用的被不可少的了,現在好多產品或者運營都要求能夠隨機出優惠券的金額,但是呢又不能過於隨機,送出去的券都是錢嗎,投資人的錢,是吧。
所以,在隨機生成的金額中就要求,小額度的幾率要大,大額度的幾率要小,比如說3元的70%,5塊的25%,10塊的5%,這個樣子的概率去生成優惠券,這個怎么辦呢?
對於上述的問題,直接用我們的Random.next(Integer range);就不夠了。因為這個偽隨機不帶權重,3,5,10出現的概率都是一樣的。
實現思路
還是拿上述的例子,3出現的概率是70%,我們給他的權重賦值為70,5出現的概率為25%,我們給他的權重賦值為25,10出現的概率為5%,我們給他的權重賦值為5.
我們按照順序計算出權重的加和,把當前數字出現的權重加和前的值作為其權重范圍的起點值,把加和后的值作為其權重范圍的終點值。
這樣的話,我們就可以使用Random.next(100)來做隨機數,然后判斷隨機數落在的范圍,然后映射到對應的優惠券數值即可。
java實現
package com.nggirl.test.weight.random; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Random; public class WeightRandom { public static void main(String[] args){ WeightRandom wr = new WeightRandom(); wr.initWeight(new String[]{"1","2","3","4"}, new Integer[]{100,100,200,600}); Random r = new Random(); for(int i = 0; i < 10; i++){ Integer rv = r.nextInt(wr.getMaxRandomValue()); System.out.println(rv); System.out.println(wr.getElementByRandomValue(rv).getKey() + " " + rv); } HashMap<String, Integer> keyCount = new HashMap<String, Integer>(); keyCount.put("1", 0); keyCount.put("2", 0); keyCount.put("3", 0); keyCount.put("4", 0); for(int i = 0; i < 10000; i++){ Integer rv = r.nextInt(wr.getMaxRandomValue()); String key = wr.getElementByRandomValue(rv).getKey(); keyCount.put(key, keyCount.get(key).intValue()+1); } System.out.println(""); } private List<WeightElement> weightElements; public void initWeight(String[] keys, Integer[] weights){ if(keys == null || weights == null || keys.length != weights.length){ return; } weightElements = new ArrayList<WeightElement>(); for(int i=0; i< keys.length; i++){ weightElements.add(new WeightElement(keys[i], weights[i])); } rangeWeightElemnts(); printRvs(); } private void rangeWeightElemnts(){ if(weightElements.size() == 0){ return; } WeightElement ele0 = weightElements.get(0); ele0.setThresholdLow(0); ele0.setThresholdHigh(ele0.getWeight()); for(int i = 1; i < weightElements.size(); i++){ WeightElement curElement = weightElements.get(i); WeightElement preElement = weightElements.get(i - 1); curElement.setThresholdLow(preElement.getThresholdHigh()); curElement.setThresholdHigh(curElement.getThresholdLow() + curElement.getWeight()); } } public WeightElement getElementByRandomValue(Integer rv){ //因為元素權重范圍有序遞增,所以這里可以改為二分查找 for(WeightElement e:weightElements){ if(rv >= e.getThresholdLow() && rv < e.getThresholdHigh()){ return e; } } return null; } public Integer getMaxRandomValue(){ if(weightElements == null || weightElements.size() == 0){ return null; } return weightElements.get(weightElements.size() - 1).getThresholdHigh(); } public void printRvs(){ for(WeightElement e:weightElements){ System.out.println(e.toString()); } } static class WeightElement{ /** * 元素標記 */ private String key; /** * 元素權重 */ private Integer weight; /** * 權重對應隨機數范圍低線 */ private Integer thresholdLow; /** * 權重對應隨機數范圍高線 */ private Integer thresholdHigh; public WeightElement(){ } public WeightElement(Integer weight){ this.key = weight.toString(); this.weight = weight; } public WeightElement(String key, Integer weight){ this.key = key; this.weight = weight; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public Integer getWeight() { return weight; } public void setWeight(Integer weight) { this.weight = weight; } public Integer getThresholdLow() { return thresholdLow; } public void setThresholdLow(Integer thresholdLow) { this.thresholdLow = thresholdLow; } public Integer getThresholdHigh() { return thresholdHigh; } public void setThresholdHigh(Integer thresholdHigh) { this.thresholdHigh = thresholdHigh; } public String toString(){ return "key:"+this.key + " weight:" + this.weight + " low:"+this.thresholdLow+" heigh:"+this.thresholdHigh; } } }
- 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
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
二分法的實現
public WeightElement getElementByRandomValue(Integer rv){ if(rv < 0 || rv > getMaxRandomValue()-1){ return null; } //此時rv必然在0 - getMaxRandomValue()-1范圍內, //也就是必然能夠命中某一個值 int start = 0, end = weightElements.size() - 1; int index = weightElements.size()/2; while(true){ if(rv < weightElements.get(index).getThresholdLow()){ end = index - 1; }else if(rv >= weightElements.get(index).getThresholdHigh()){ start = index + 1; }else{ return weightElements.get(index); } index = (start + end)/2; } }
基本算法描述如下:
1、每個廣告增加權重
2、將所有匹配廣告的權重相加sum,
3、以相加結果為隨機數的種子,生成1~sum之間的隨機數rd
4、.接着遍歷所有廣告,訪問順序可以隨意.將當前節點的權重值加上前面訪問的各節點權重值得curWt,判斷curWt >= rd,如果條件成立則返回當前節點,如果不是則繼續累加下一節點. 直到符合上面的條件,由於rd<=sum 因此一定存在curWt>=rd。
特別說明:
此算法和廣告的順序無關
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
import
java.util.ArrayList;
import
java.util.Collections;
import
java.util.Comparator;
import
java.util.LinkedHashMap;
import
java.util.List;
import
java.util.Map;
public
class
Test {
/**
* @param args
*/
@SuppressWarnings
(
"unchecked"
)
public
static
void
main(String[] args) {
List<Node> arrNodes =
new
ArrayList<Node>();
Node n =
new
Node(
10
,
"測試1"
);
arrNodes.add(n);
n =
new
Node(
20
,
"測試2"
);
arrNodes.add(n);
n =
new
Node(
30
,
"測試3"
);
arrNodes.add(n);
n =
new
Node(
40
,
"測試4"
);
arrNodes.add(n);
//Collections.sort(arrNodes, new Node());
Map<String, Integer> showMap =
null
;
int
sum = getSum(arrNodes);
int
random =
0
;
Node kw =
null
;
for
(
int
k =
0
; k <
20
; k++) {
showMap =
new
LinkedHashMap<String, Integer>();
for
(
int
i =
0
; i <
100
; i++) {
random = getRandom(sum);
kw = getKW(arrNodes, random);
if
(showMap.containsKey(kw.kw)) {
showMap.put(kw.kw, showMap.get(kw.kw) +
1
);
}
else
{
showMap.put(kw.kw,
1
);
}
//System.out.println(i + " " +random + " " + getKW(arrNodes, random));
}
System.out.print(k +
" "
);
System.out.println(showMap);
}
}
public
static
Node getKW(List<Node> nodes,
int
rd) {
Node ret =
null
;
int
curWt =
0
;
for
(Node n : nodes){
curWt += n.weight;
if
(curWt >= rd) {
ret = n;
break
;
}
}
return
ret;
}
public
static
int
getSum(List<Node> nodes) {
int
sum =
0
;
for
(Node n : nodes)
sum += n.weight;
return
sum;
}
public
static
int
getRandom(
int
seed) {
return
(
int
)Math.round(Math.random() * seed);
}
}
class
Node
implements
Comparator{
int
weight =
0
;
String kw =
""
;
public
Node() {}
public
Node(
int
wt, String kw) {
this
.weight = wt;
this
.kw = kw;
}
public
String toString(){
StringBuilder sbBuilder =
new
StringBuilder();
sbBuilder.append(
" weight="
).append(weight);
sbBuilder.append(
" kw"
).append(kw);
return
sbBuilder.toString();
}
public
int
compare(Object o1, Object o2) {
Node n1 = (Node)o1;
Node n2 = (Node)o2;
if
(n1.weight > n2.weight)
return
1
;
else
return
0
;
}
}
|
根據權重進行抽取的算法應用比較廣泛,其中抽獎便是主要用途之一。正好這幾天也正在進行抽獎模塊的開發,整個抽獎模塊涉及到的地方大概有三處,分別是后台進行獎品的添加(同時設置權重和數量),前台根據后台配置生成抽獎隊列並根據指令開始抽獎活動,最后一部分是后台統計中獎情況並設置物流狀態。本文主要針對前台抽獎算法進行介紹如何根據權重設置每個獎品被抽到的概率。
抽獎算法的核心是根據權重設置隨機數出現的概率,在此我將它封裝成一個生成隨機數的隨機類,代碼如下:
- /**
- * JAVA 返回隨機數,並根據概率、比率
- *
- */
- public class MathRandom {
- private static Log logger = LogFactory.getLog(MathRandom.class);
- /**
- * Math.random()產生一個double型的隨機數,判斷一下 每個獎品出現的概率
- *
- * @return int
- *
- */
- public int PercentageRandom(List<RewardPrize> prizes) {
- DecimalFormat df = new DecimalFormat("######0.00");
- int random = -2;
- try{
- double sumWeight = 0;
- //計算總權重
- for(RewardPrize rp_1 : prizes){
- sumWeight += rp_1.getPrize_weight();
- }
- double randomNumber;
- randomNumber = Math.random();
- System.out.println("randomNumber是:" + randomNumber);
- 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;
- System.out.println("d1是:" + d1);
- System.out.println("d2是:" + d2);
- break;
- }
- }
- }catch(Exception e){
- System.out.println(e.getMessage());
- logger.error("生成抽獎隨機數出錯,出錯原因:" + e.getMessage());
- random = -1;
- }
- return random;
- }
- /**
- * 測試主程序
- *
- * @param agrs
- */
- public static void main(String[] agrs) {
- int i = 0;
- MathRandom a = new MathRandom();
- List<RewardPrize> prizes = new ArrayList();
- for(int m=0;m<100;m++){
- RewardPrize rp = new RewardPrize();
- rp.setPrize_amount(10);//每個獎品數量設置10個
- rp.setPrize_weight(1);//每個獎品的權重都設置成1,也就是每個獎品被抽到的概率相同(可根據情況自行設置權重)
- prizes.add(rp);
- }
- for (i = 0; i <= 100; i++)// 打印100個測試概率的准確性
- {
- System.out.println(a.PercentageRandom(prizes));
- }
- }
- }
簡單介紹一下上面的代碼含義,首先計算出待選獎品的總權重,這樣做的目的是可以隨意設置獎品權重,不必再考慮權重之和是否等於100。隨機規則是首先生成一個隨機數randomNumber(生成的隨機數位於0到1的左開右閉區間),然后分別計算出當前獎品前前面所有有獎品(不包括當前獎品)的概率和d1和當前獎品后面(包括當前獎品)所有獎品的概率和d2,然后判斷生成的隨機數randomNumber是否已處於d1和d2之間,如果處於該區間之內則當前獎品將被抽中。
權重隨機算法在抽獎,資源調度等系統中應用還是比較廣泛的,一個簡單的按照權重來隨機的實現,權重為幾個隨機對象(分類)的命中的比例,權重設置越高命中越容易,之和可以不等於100;
簡單實現代碼如下:
import java.util.ArrayList; import java.util.List; import java.util.Random; public class WeightRandom { static List<WeightCategory> categorys = new ArrayList<WeightCategory>(); private static Random random = new Random(); public static void initData() { WeightCategory wc1 = new WeightCategory("A",60); WeightCategory wc2 = new WeightCategory("B",20); WeightCategory wc3 = new WeightCategory("C",20); categorys.add(wc1); categorys.add(wc2); categorys.add(wc3); } public static void main(String[] args) { initData(); Integer weightSum = 0; for (WeightCategory wc : categorys) { weightSum += wc.getWeight(); } if (weightSum <= 0) { System.err.println("Error: weightSum=" + weightSum.toString()); return; } Integer n = random.nextInt(weightSum); // n in [0, weightSum) Integer m = 0; for (WeightCategory wc : categorys) { if (m <= n && n < m + wc.getWeight()) { System.out.println("This Random Category is "+wc.getCategory()); break; } m += wc.getWeight(); } } } class WeightCategory { private String category; private Integer weight; public WeightCategory() { super(); } public WeightCategory(String category, Integer weight) { super(); this.setCategory(category); this.setWeight(weight); } public Integer getWeight() { return weight; } public void setWeight(Integer weight) { this.weight = weight; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } }