最近做了一個活動抽獎需求,項目需要控制預算,概率需要分布均勻,這樣才能獲得所需要的概率結(jié)果。
例如抽獎得到紅包獎金,而每個獎金的分布都有一定概率:
紅包/(單位元) | 概率 |
---|---|
0.01-1 | 40% |
1-2 | 25% |
2-3 | 20% |
3-5 | 10% |
5-10 | 5% |
現(xiàn)在的問題就是如何根據(jù)概率分配給用戶一定數(shù)量的紅包。
一、一般算法
算法思路:生成一個列表,分成幾個區(qū)間,例如列表長度100,1-40是0.01-1元的區(qū)間,41-65是1-2元的區(qū)間等,然后隨機從100取出一個數(shù),看落在哪個區(qū)間,獲得紅包區(qū)間,最后用隨機函數(shù)在這個紅包區(qū)間內(nèi)獲得對應紅包數(shù)。
//per[] = {40,25,20,10,5}
//moneyStr[] = {0.01-1,1-2,2-3,3-5,5-10}
//獲取紅包金額
public double getMoney(List<String> moneyStr,List<Integer> per){
double packet = 0.01;
//獲取概率對應的數(shù)組下標
int key = getProbability(per);
//獲取對應的紅包值
String[] moneys = moneyStr.get(key).split("-");
if (moneys.length < 2){
return packet;
}
double min = Double.valueOf(moneys[0]);//紅包最小值
double max = Double.valueOf(moneys[1]);//紅包最大值
Random random = new Random();
packet = min + (max - min) * random.nextInt(10) * 0.1;
return packet;
}
//獲得概率對應的key
public int getProbability(List<Integer> per){
int key = 0;
if (per == null || per.size() == 0){
return key;
}
//100中隨機生成一個數(shù)
Random random = new Random();
int num = random.nextInt(100);
int probability = 0;
int i = 0;
for (int p : per){
probability += p;
//獲取落在該區(qū)間的對應key
if (num < probability){
key = i;
}
i++;
}
return key;
}
時間復雜度:預處理O(MN),隨機數(shù)生成O(1),空間復雜度O(MN),其中N代表紅包種類,M則由最低概率決定。
優(yōu)缺點:該方法優(yōu)點是實現(xiàn)簡單,構(gòu)造完成之后生成隨機類型的時間復雜度就是O(1),缺點是精度不夠高,占用空間大,尤其是在類型很多的時候。
二、離散算法
算法思路:離散算法通過概率分布構(gòu)造幾個點[40, 65, 85, 95,100],構(gòu)造的數(shù)組的值就是前面概率依次累加的概率之和。在生成1~100的隨機數(shù),看它落在哪個區(qū)間,比如50在[40,65]之間,就是類型2。在查找時,可以采用線性查找,或效率更高的二分查找。
//per[] = {40, 65, 85, 95,100}
//moneyStr[] = {0.01-1,1-2,2-3,3-5,5-10}
//獲取紅包金額
public double getMoney(List<String> moneyStr,List<Integer> per){
double packet = 0.01;
//獲取概率對應的數(shù)組下標
int key = getProbability(per);
//獲取對應的紅包值
String[] moneys = moneyStr.get(key).split("-");
if (moneys.length < 2){
return packet;
}
double min = Double.valueOf(moneys[0]);//紅包最小值
double max = Double.valueOf(moneys[1]);//紅包最大值
Random random = new Random();
packet = min + (max - min) * random.nextInt(10) * 0.1;
return packet;
}
//獲得概率對應的key
public int getProbability(List<Integer> per){
int key = -1;
if (per == null || per.size() == 0){
return key;
}
//100中隨機生成一個數(shù)
Random random = new Random();
int num = random.nextInt(100);
int i = 0;
for (int p : per){
//獲取落在該區(qū)間的對應key
if (num < p){
key = i;
}
}
return key;
}
算法復雜度:比一般算法減少占用空間,還可以采用二分法找出R,這樣,預處理O(N),隨機數(shù)生成O(logN),空間復雜度O(N)。
優(yōu)缺點:比一般算法占用空間減少,空間復雜度O(N)。
三、Alias Method
算法思路:Alias Method將每種概率當做一列,該算法最終的結(jié)果是要構(gòu)造拼裝出一個每一列合都為1的矩形,若每一列最后都要為1,那么要將所有元素都乘以5(概率類型的數(shù)量)。
此時會有概率大于1的和小于1的,接下來就是構(gòu)造出某種算法用大于1的補足小于1的,使每種概率最后都為1,注意,這里要遵循一個限制:每列至多是兩種概率的組合。
最終,我們得到了兩個數(shù)組,一個是在下面原始的prob數(shù)組[0.75,0.25,0.5,0.25,1],另外就是在上面補充的Alias數(shù)組,其值代表填充的那一列的序號索引,(如果這一列上不需填充,那么就是NULL),[4,4,0,1,NULL]。當然,最終的結(jié)果可能不止一種,你也可能得到其他結(jié)果。
prob[] = [0.75,0.25,0.5,0.25,1]
Alias[] = [4,4,0,1,NULL] (記錄非原色的下標)
根據(jù)Prob和Alias獲取其中一個紅包區(qū)間。
隨機產(chǎn)生一列C,再隨機產(chǎn)生一個數(shù)R,通過與Prob[C]比較,R較大則返回C,反之返回Alias[C]。
//原概率與紅包區(qū)間
per[] = {0.25,0.2,0.1,0.05,0.4}
moneyStr[] = {1-2,2-3,3-5,5-10,0.01-1}
舉例驗證下,比如取第二列,讓prob[1]的值與一個隨機小數(shù)f比較,如果f小于prob[1],那么結(jié)果就是2-3元,否則就是Alias[1],即4。
我們可以來簡單驗證一下,比如隨機到第二列的概率是0.2,得到第三列下半部分的概率為0.2 * 0.25,記得在第四列還有它的一部分,那里的概率為0.2 * (1-0.25),兩者相加最終的結(jié)果還是0.2 * 0.25 + 0.2 * (1-0.25) = 0.2,符合原來第二列的概率per[1]。
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
public class AliasMethod {
/* The random number generator used to sample from the distribution. */
private final Random random;
/* The probability and alias tables. */
private final int[] alias;
private final double[] probability;
/**
* Constructs a new AliasMethod to sample from a discrete distribution and
* hand back outcomes based on the probability distribution.
* <p/>
* Given as input a list of probabilities corresponding to outcomes 0, 1,
* ..., n - 1, this constructor creates the probability and alias tables
* needed to efficiently sample from this distribution.
*
* @param probabilities The list of probabilities.
*/
public AliasMethod(List<Double> probabilities) {
this(probabilities, new Random());
}
/**
* Constructs a new AliasMethod to sample from a discrete distribution and
* hand back outcomes based on the probability distribution.
* <p/>
* Given as input a list of probabilities corresponding to outcomes 0, 1,
* ..., n - 1, along with the random number generator that should be used
* as the underlying generator, this constructor creates the probability
* and alias tables needed to efficiently sample from this distribution.
*
* @param probabilities The list of probabilities.
* @param random The random number generator
*/
public AliasMethod(List<Double> probabilities, Random random) {
/* Begin by doing basic structural checks on the inputs. */
if (probabilities == null || random == null)
throw new NullPointerException();
if (probabilities.size() == 0)
throw new IllegalArgumentException("Probability vector must be nonempty.");
/* Allocate space for the probability and alias tables. */
probability = new double[probabilities.size()];
alias = new int[probabilities.size()];
/* Store the underlying generator. */
this.random = random;
/* Compute the average probability and cache it for later use. */
final double average = 1.0 / probabilities.size();
/* Make a copy of the probabilities list, since we will be making
* changes to it.
*/
probabilities = new ArrayList<Double>(probabilities);
/* Create two stacks to act as worklists as we populate the tables. */
Stack<Integer> small = new Stack<Integer>();
Stack<Integer> large = new Stack<Integer>();
/* Populate the stacks with the input probabilities. */
for (int i = 0; i < probabilities.size(); ++i) {
/* If the probability is below the average probability, then we add
* it to the small list; otherwise we add it to the large list.
*/
if (probabilities.get(i) >= average)
large.push(i);
else
small.push(i);
}
/* As a note: in the mathematical specification of the algorithm, we
* will always exhaust the small list before the big list. However,
* due to floating point inaccuracies, this is not necessarily true.
* Consequently, this inner loop (which tries to pair small and large
* elements) will have to check that both lists aren't empty.
*/
while (!small.isEmpty() && !large.isEmpty()) {
/* Get the index of the small and the large probabilities. */
int less = small.pop();
int more = large.pop();
/* These probabilities have not yet been scaled up to be such that
* 1/n is given weight 1.0. We do this here instead.
*/
probability[less] = probabilities.get(less) * probabilities.size();
alias[less] = more;
/* Decrease the probability of the larger one by the appropriate
* amount.
*/
probabilities.set(more,
(probabilities.get(more) + probabilities.get(less)) - average);
/* If the new probability is less than the average, add it into the
* small list; otherwise add it to the large list.
*/
if (probabilities.get(more) >= 1.0 / probabilities.size())
large.add(more);
else
small.add(more);
}
/* At this point, everything is in one list, which means that the
* remaining probabilities should all be 1/n. Based on this, set them
* appropriately. Due to numerical issues, we can't be sure which
* stack will hold the entries, so we empty both.
*/
while (!small.isEmpty())
probability[small.pop()] = 1.0;
while (!large.isEmpty())
probability[large.pop()] = 1.0;
}
/**
* Samples a value from the underlying distribution.
*
* @return A random value sampled from the underlying distribution.
*/
public int next() {
/* Generate a fair die roll to determine which column to inspect. */
int column = random.nextInt(probability.length);
/* Generate a biased coin toss to determine which option to pick. */
boolean coinToss = random.nextDouble() < probability[column];
/* Based on the outcome, return either the column or its alias. */
/* Log.i("1234","column="+column);
Log.i("1234","coinToss="+coinToss);
Log.i("1234","alias[column]="+coinToss);*/
return coinToss ? column : alias[column];
}
public int[] getAlias() {
return alias;
}
public double[] getProbability() {
return probability;
}
public static void main(String[] args) {
TreeMap<String, Double> map = new TreeMap<String, Double>();
map.put("1-2", 0.25);
map.put("2-3", 0.2);
map.put("3-5", 0.1);
map.put("5-10", 0.05);
map.put("0.01-1", 0.4);
List<Double> list = new ArrayList<Double>(map.values());
List<String> gifts = new ArrayList<String>(map.keySet());
AliasMethod method = new AliasMethod(list);
for (double value : method.getProbability()){
System.out.println("," + value);
}
for (int value : method.getAlias()){
System.out.println("," + value);
}
Map<String, AtomicInteger> resultMap = new HashMap<String, AtomicInteger>();
for (int i = 0; i < 100000; i++) {
int index = method.next();
String key = gifts.get(index);
if (!resultMap.containsKey(key)) {
resultMap.put(key, new AtomicInteger());
}
resultMap.get(key).incrementAndGet();
}
for (String key : resultMap.keySet()) {
System.out.println(key + "==" + resultMap.get(key));
}
}
}
算法復雜度:預處理O(NlogN),隨機數(shù)生成O(1),空間復雜度O(2N)。
優(yōu)缺點:這種算法初始化較復雜,但生成隨機結(jié)果的時間復雜度為O(1),是一種性能非常好的算法。