概率算法

最近做了一個活動抽獎需求,項目需要控制預算,概率需要分布均勻,這樣才能獲得所需要的概率結(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ù)量)。

Alias Method

此時會有概率大于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),是一種性能非常好的算法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內(nèi)容