權重隨機算法的java實現


一、概述

  平時,經常會遇到權重隨機算法,從不同權重的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()即可找到目標元素。

當然,也可以利用數組+二分查找來實現。

二、源碼

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) {
            Preconditions.checkArgument(pair.getValue().doubleValue() > 0, String.format("非法權重值:pair=%s", pair));

            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%以上。

四、另一種實現

利用B+樹的原理。葉子結點存放元素,非葉子結點用於索引。非葉子結點有兩個屬性,分別保存左右子樹的累加權重。如下圖:

 

 

看到這個圖,聰明的你應該知道怎么隨機了吧。

此方法的優點是:更改一個元素,只須修改該元素到根結點那半部分的權值即可。


【項目實戰】——Java根據獎品權重計算中獎概率實現抽獎(適用於砸金蛋、大轉盤等抽獎活動)

雙蛋節(聖誕+元旦)剛剛過去,前幾天項目上線的砸金蛋活動也圓滿結束。

現在在許多網站上都會有抽獎的活動,抽獎的算法也是多種多樣,這里介紹一下如何根據每種獎品的權重來抽獎,適用於多種抽獎形式。 

獎品設置

比如現在舉行一次砸金蛋活動中,獎品如下:

獎品夠豐富的哇,香車美女豪宅都有了~不過由於法律的原因,活人是不能贈送的,所以一等獎只能送海報了~

獎品在數據庫中的存儲情況

抽獎實現

獎品實體 Prize.java

    public class Prize {    
    private int id;//獎品id
    private String prize_name;//獎品名稱
    private int prize_amount;//獎品(剩余)數量
    private int prize_weight;//獎品權重
    //getter、setter

這里只考慮最簡單的抽獎實現,所以暫時只為獎品設計如上4個字段。

  見注釋,prize_name表示獎品名稱;prize_amount表示獎品數量,即本次抽獎活動計划發放此獎品的數量;prize_weight表示獎品權重,表示獎品被抽到的幾率的比重,權重越大,被抽到的幾率越大,比如本次砸金蛋活動有4種獎品,權重分別是1、2、3、4,總權重是10,那么每種獎品被抽到的幾率就是1/10,2/10,3/10,4/10。

核心算法:

/**
     * 根據Math.random()產生一個double型的隨機數,判斷每個獎品出現的概率
     * @param prizes
     * @return random:獎品列表prizes中的序列(prizes中的第random個就是抽中的獎品)
     */
    public int getPrizeIndex(List<Prize> prizes) {
        DecimalFormat df = new DecimalFormat("######0.00");  
        int random = -1;
        try{
            //計算總權重
            double sumWeight = 0;
            for(Prize p : prizes){
                sumWeight += p.getPrize_weight();
            }

            //產生隨機數
            double randomNumber;
            randomNumber = Math.random();

            //根據隨機數在所有獎品分布的區域並確定所抽獎品
            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;
                    break;
                }
            }
        }catch(Exception e){
            System.out.println("生成抽獎隨機數出錯,出錯原因:" +e.getMessage());
        }
        return random;
    }

抽獎的邏輯可以用下面這張圖表示:

 

  分析:如上圖,為了便於計算和理解,設置每種獎品的權重分別為1,2,3,4,所以被抽到的概率分別為0.1,0.2,0.3,0.4(本次活動中獎概率為100%)。

  先生成一個隨機數randomNumber,然后根據隨機數所處區域判斷獎品:

0<randomNumber<=0.1   表示抽中一等獎
0.1<randomNumber<=0.3 表示抽中二等獎
0.3<randomNumber<=0.6 表示抽中三等獎
0.6<randomNumber<=1.0 表示抽中四等獎

抽獎測試

public static void main(String[] agrs) {
        int i = 0;
        PrizeMathRandom a = new PrizeMathRandom();
        int[] result=new int[4];
        List<Prize> prizes = new ArrayList<Prize>();

        Prize p1 = new Prize();
        p1.setPrize_name("范冰冰海報");
        p1.setPrize_weight(1);//獎品的權重設置成1
        prizes.add(p1);

        Prize p2 = new Prize();
        p2.setPrize_name("上海紫園1號別墅");
        p2.setPrize_weight(2);//獎品的權重設置成2
        prizes.add(p2);

        Prize p3 = new Prize();
        p3.setPrize_name("奧迪a9");
        p3.setPrize_weight(3);//獎品的權重設置成3
        prizes.add(p3);

        Prize p4 = new Prize();
        p4.setPrize_name("雙色球彩票");
        p4.setPrize_weight(4);//獎品的權重設置成4
        prizes.add(p4);

        System.out.println("抽獎開始");
        for (i = 0; i < 10000; i++)// 打印100個測試概率的准確性
        {
            int selected=a.getPrizeIndex(prizes);
            System.out.println("第"+i+"次抽中的獎品為:"+prizes.get(selected).getPrize_name());
            result[selected]++;
            System.out.println("--------------------------------");
        }
        System.out.println("抽獎結束");
        System.out.println("每種獎品抽到的數量為:");
        System.out.println("一等獎:"+result[0]);
        System.out.println("二等獎:"+result[1]);
        System.out.println("三等獎:"+result[2]);
        System.out.println("四等獎:"+result[3]);       
    }

嘗試抽獎10000次的結果如下:

一等獎:962
二等獎:2007
三等獎:3043
四等獎:3988

類獎品獲獎次數比例剛好大約為1:2:3:4,學過概率的你肯定知道抽獎次數越多,測試結果越准確~

Tips:

  如果計划中獎率是100%的話,那么10個獎品只能抽獎10次,所以還要根據實際情況設置每種獎品數量和權重。

  如果需要設置中獎率不為100%,可以添加一個“偽獎品”,並為其設置權重,那么抽到這個“偽獎品”的概率就是不中獎的概率。

  如果在抽獎過程中某類獎品抽完了,可以做個判斷,如果此獎品的剩余數量為0,則重新抽取獎品,直到抽到其他獎品位置。

 

 

參考文章:

https://www.cnblogs.com/waterystone/p/5708063.html

https://blog.csdn.net/huyuyang6688/article/details/50480687


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM