概率算法

最近做了一個活動抽獎需求,項目需要控制預算,概率需要分布均勻,這樣才能獲得所需要的概率結果。
例如抽獎得到紅包獎金,而每個獎金的分布都有一定概率:

紅包/(單位元) 概率
0.01-1 40%
1-2 25%
2-3 20%
3-5 10%
5-10 5%

現在的問題就是如何根據概率分配給用戶一定數量的紅包。

一、一般算法

算法思路:生成一個列表,分成幾個區間,例如列表長度100,1-40是0.01-1元的區間,41-65是1-2元的區間等,然后隨機從100取出一個數,看落在哪個區間,獲得紅包區間,最后用隨機函數在這個紅包區間內獲得對應紅包數。

//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;
        //獲取概率對應的數組下標
        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中隨機生成一個數
        Random random = new Random();
        int num = random.nextInt(100);

        int probability = 0;
        int i = 0;
        for (int p : per){
            probability += p;
            //獲取落在該區間的對應key
            if (num < probability){
                key = i;
            }
            
            i++;
        }
        
        return key;

    }
    

時間復雜度:預處理O(MN),隨機數生成O(1),空間復雜度O(MN),其中N代表紅包種類,M則由最低概率決定。

優缺點:該方法優點是實現簡單,構造完成之后生成隨機類型的時間復雜度就是O(1),缺點是精度不夠高,占用空間大,尤其是在類型很多的時候。

二、離散算法

算法思路:離散算法通過概率分布構造幾個點[40, 65, 85, 95,100],構造的數組的值就是前面概率依次累加的概率之和。在生成1~100的隨機數,看它落在哪個區間,比如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;
        //獲取概率對應的數組下標
        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中隨機生成一個數
        Random random = new Random();
        int num = random.nextInt(100);

        int i = 0;
        for (int p : per){
            //獲取落在該區間的對應key
            if (num < p){
                key = i;
            }
        }
        
        return key;

    }  

算法復雜度:比一般算法減少占用空間,還可以采用二分法找出R,這樣,預處理O(N),隨機數生成O(logN),空間復雜度O(N)。

優缺點:比一般算法占用空間減少,空間復雜度O(N)。

三、Alias Method

算法思路:Alias Method將每種概率當做一列,該算法最終的結果是要構造拼裝出一個每一列合都為1的矩形,若每一列最后都要為1,那么要將所有元素都乘以5(概率類型的數量)。

Alias Method

此時會有概率大于1的和小于1的,接下來就是構造出某種算法用大于1的補足小于1的,使每種概率最后都為1,注意,這里要遵循一個限制:每列至多是兩種概率的組合。

最終,我們得到了兩個數組,一個是在下面原始的prob數組[0.75,0.25,0.5,0.25,1],另外就是在上面補充的Alias數組,其值代表填充的那一列的序號索引,(如果這一列上不需填充,那么就是NULL),[4,4,0,1,NULL]。當然,最終的結果可能不止一種,你也可能得到其他結果。

prob[] = [0.75,0.25,0.5,0.25,1]
Alias[] = [4,4,0,1,NULL] (記錄非原色的下標)
根據Prob和Alias獲取其中一個紅包區間。
隨機產生一列C,再隨機產生一個數R,通過與Prob[C]比較,R較大則返回C,反之返回Alias[C]。

//原概率與紅包區間
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]的值與一個隨機小數f比較,如果f小于prob[1],那么結果就是2-3元,否則就是Alias[1],即4。

我們可以來簡單驗證一下,比如隨機到第二列的概率是0.2,得到第三列下半部分的概率為0.2 * 0.25,記得在第四列還有它的一部分,那里的概率為0.2 * (1-0.25),兩者相加最終的結果還是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),隨機數生成O(1),空間復雜度O(2N)。

優缺點:這種算法初始化較復雜,但生成隨機結果的時間復雜度為O(1),是一種性能非常好的算法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容