1. 程式人生 > >Java實現遊戲抽獎演算法

Java實現遊戲抽獎演算法

常用抽獎演算法對比

基礎的遊戲抽獎演算法通常要求實現在指定獎品的集合中,每個獎品根據對對應概率進行抽取。個人瞭解的主要有以下幾中抽獎演算法:

隨機數一一對應

演算法思想

這種演算法思想最為簡單。將n個獎品編號0~n-1,其中各類獎品的概率通過其數量體現,最後程式產生0~n-1之間的隨機數便是抽中的獎品編號。例如:
蘋果手機概率1%,網站會員20%,折扣券20%,很遺憾59%。這樣,編號0是蘋果手機,1~20是會員,21~40是折扣券,41~100是 很遺憾。產生的隨機數落在那個區間,就代表那個獎品被抽中。

存在問題

  1. 總數N快速膨脹
    概率通過數量來體現在各個獎品概率較大的情況下,總數n可以較小。但如果在精度很高的情況下,總數必須按比例成倍擴大。
    例如,所有獎品概率都是10%,那麼n只需要取10就可以。但是如果某個獎品概率是0.01%,按照這種演算法,總數要擴大到100*100。
  2. 平衡性影響
    在Java中,Math.random()方法本身基本可以保證大量測試的情況下避免高重複,且概率分佈比較平均。但是需要注意的是,該方法預設返回0-1之間的資料。
    在當前演算法中,必須擴大指定倍數並且強制使用int進行型別轉換。在這樣的擴大和轉換過程中,必然會對資料精度進行修改,轉換後的資料也不能保證概率分佈平均。
    因此,該演算法實際可能達不到預期的概率要求。
  3. 演算法複雜度
    資料準備階段,為每個獎品確定編號與獎品資訊的關係集合需要O(n);
    產生隨機數階段並轉換,O(1);
    從集合中查詢,不同的資料結構實現不同,最差需要O(n);

離散法

演算法思想

(高中數學裡幾何概形的思想)
將獎品集合的概率劃分區段放入陣列中。概率區段通過該概率累計相加確定。利用隨機數產生隨機概率,加入陣列並排序,該資料的下標,就是對應獎品集合中獎品的索引。例如,獎品的集合有X1,X2,X3,X4,對應概率為P1=0.2,P2=0.2,P3=0.3,P4=0.3。
那麼,產生的概率區段陣列為[0.2,0.4,0.7,1.0]。
0.2以下代表X1,0.2~0.4代表X2,0.4~0.7代表X3,0.7~1代表X4。
這樣,如果產生一個隨機概率為0.5,加入陣列排序後,0.4~0.7之間,是X3相加所在的概率區間,返回index=2。
由於區間分佈的確定是按照X集合順序的,所以該索引也正是X3在原集合中的索引。
概率分佈示意圖

特點

  1. 利用幾何概形,概率陣列分佈在0到1之間,不再需要擴大倍數和取整操作,基本可以保證概率平均分佈,避免大量重複的情況
  2. 概率分配的排序過程,可以使用java預設的排序工具類,也可以自己實現。保證時間複雜度最小。
  3. 複雜度
    準備階段,O(m)。m遠小於n,因為概率只有幾個,不會大量膨脹。
    產生隨機數,O(1)
    排序取下標,根據排序演算法,O(logn)即可實現
    取值,根據下標,O(1);

Alias 演算法

這種演算法對數學要求比較高,沒有仔細研究。
感興趣的小夥伴可以自己研究一下和我分享

演算法實現

獎品實體類

/**
 * 抽獎獎品實體類
 * @author
irving * @since 2017年7月23日 下午9:41:33 * @version MARK 0.0.1 */
public class Gift { private int id; //獎品Id private String name; //獎品名稱 private double prob; //獲獎概率 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getProb() { return prob; } public void setProb(double prob) { this.prob = prob; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); } }

抽獎實現工具類

/**
 * 抽獎工具類<br/>
 * 整體思想:
 * 獎品集合 + 概率比例集合
 * 將獎品按集合中順序概率計算成所佔比例區間,放入比例集合。併產生一個隨機數加入其中,排序。</br>
 * 排序後,隨機數落在哪個區間,就表示那個區間的獎品被抽中。</br>
 * 返回的隨機數在集合中的索引,該索引就是獎品集合中的索引。</br>
 * 比例區間的計算通過概率相加獲得。
 * @author irving
 * @since 2017年7月23日 下午9:48:23
 * @version MARK 0.0.1
 */
public class DrawLotteryUtil {

    public static int drawGift(List<Gift> giftList){

        if(null != giftList && giftList.size()>0){
            List<Double> orgProbList = new ArrayList<Double>(giftList.size());
            for(Gift gift:giftList){
                //按順序將概率新增到集合中
                orgProbList.add(gift.getProb());
            }

            return draw(orgProbList);

        }
        return -1;
    }

    public static int draw(List<Double> giftProbList){

        List<Double> sortRateList = new ArrayList<Double>();

        // 計算概率總和
        Double sumRate = 0D;
        for(Double prob : giftProbList){
            sumRate += prob;
        }

        if(sumRate != 0){
            double rate = 0D;   //概率所佔比例
            for(Double prob : giftProbList){
                rate += prob;   
                // 構建一個比例區段組成的集合(避免概率和不為1)
                sortRateList.add(rate / sumRate);
            }

            // 隨機生成一個隨機數,並排序
            double random = Math.random();
            sortRateList.add(random);
            Collections.sort(sortRateList);

            // 返回該隨機數在比例集合中的索引
            return sortRateList.indexOf(random);
        }


        return -1;
    }

    public static void main(String[] args) {
        Gift iphone = new Gift();
        iphone.setId(101);
        iphone.setName("蘋果手機");
        iphone.setProb(0.1D);

        Gift thanks = new Gift();
        thanks.setId(102);
        thanks.setName("再接再厲");
        thanks.setProb(0.5D);

        Gift vip = new Gift();
        vip.setId(103);
        vip.setName("優酷會員");
        vip.setProb(0.4D);

        List<Gift> list = new ArrayList<Gift>();
        list.add(vip);
        list.add(thanks);
        list.add(iphone);

        for(int i=0;i<100;i++){
            int index = drawGift(list);
            System.out.println(list.get(index));
        }
    }

}