抽獎算法理論
在一組獎品中,每個獎品有自己的概率,總概率為 1.0
,也就是說在庫存充足的情況下,必然能抽中其中的一個。
通過「謝謝參與」來作為無獎的獎品(也是一種獎品)。
需要注意的是:如果一組中所有的獎品,總概率之和不為 1.0
,那么數值代表的概率就不是真實概率了,需要用所占比例來作為新的概率:新概率值=獎品概率/總概率
。
舉個例子:只有 A 和 B 兩個獎品,A 概率是 0.1
,B 概率是 0.3
,那么總概率就是 0.4,A 的真實概率就是0.1/0.4=0.25
,B 的真實概率是0.3/0.4=0.75
,真實的總概率依然為1
。
所以如果想要配置的獎品概率正好是抽獎時的概率值,那么就需要為這一組獎品列表的總概率配置成1.0
;
實現
首先定義一個有概率的獎品基類,所有繼承這個基類的子類,都可以用調用 LotteryTool.draw
算法(draw 中的參數類型使用了java泛型)
import lombok.Getter;
import lombok.Setter;
/**
* 獎品基類
*
*/
@Setter
@Getter
public class BaseAward {
/**
* 抽中這個獎品的概率
*/
private Double probability;
}
接着是具體的獎品類
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 獎品實現類
*/
@Setter
@Getter
@ToString
public class Award extends BaseAward {
private Integer id;
private String name;
private Double price;
private Integer stock;
public Award(Integer id, String name, Double price, Double probability, Integer stock) {
super();
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
setProbability(probability);
}
}
看看抽獎的工具類
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import java.util.List;
/**
* 抽獎工具類
*/
public class LotteryTool {
/**
* 抽獎方法
*
* @param awardList 獎品列表,這些是備選的獎品(一定有庫存的)
* @param <T> 具體獎品類型
* @return 返回一個抽中的獎品
*/
public static <T extends BaseAward> T draw(List<T> awardList) {
if (CollUtil.isEmpty(awardList)) {
return null;
}
// 獲取總概率,當獎品總概率正好為1時,獎品的 probability 就是真實的概率,否則會按新的比例作為概率
double sumProbability = awardList.stream()
.map(BaseAward::getProbability)
.reduce(0.0, Double::sum);
// 一共會嘗試 awardList.size() 次,確保能返回一個獎品
for (T t : awardList) {
// 使用隨機值,左閉右開(包含0,不包含1)
if (t.getProbability() > RandomUtil.randomDouble(0.0, 1.0) * sumProbability) {
return t;
}
sumProbability = sumProbability - t.getProbability();
}
// 其它情況,會到這里(理論上,一定到不了這里的。)
return null;
}
}
最后來個測試
import cn.hutool.core.util.StrUtil;
import java.util.ArrayList;
import java.util.List;
/**
* 測試
*/
public class Main {
public static void main(String[] args) {
// 獎品列表,庫存一共36
final List<Award> awardList = new ArrayList<>(4);
awardList.add(new Award(1, "蘋果手機", 7000.0, 0.05, 1));
awardList.add(new Award(2, "5元金幣", 5.0, 0.1, 5));
awardList.add(new Award(3, "15元天堂雨傘", 15.0, 0.25, 10));
awardList.add(new Award(4, "謝謝參與", 0.0, 0.6, 20));
System.out.println("開始抽獎:");
// 抽獎50次
for (int i = 0; i < 50; i++) {
String msg;
final Award draw = LotteryTool.draw(awardList);
if (draw == null) {
msg = "獎品抽完了,下次早點來吧~";
} else {
msg = StrUtil.format("抽到了價值「{}」的獎品「{}」", draw.getPrice(), draw.getName());
// 抽到獎品了,需要減庫存,庫存不足了,要從列表中剔除
draw.setStock(draw.getStock() - 1);
if (draw.getStock() <= 0) {
awardList.remove(draw);
}
}
System.out.println(StrUtil.format("第{}次抽獎,結果為:{}", i+1, msg));
}
System.out.println("抽獎結束.");
}
}
/*output~
開始抽獎:
第1次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第2次抽獎,結果為:抽到了價值「5.0」的獎品「5元金幣」
第3次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第4次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第5次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第6次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第7次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第8次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第9次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第10次抽獎,結果為:抽到了價值「5.0」的獎品「5元金幣」
第11次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第12次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第13次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第14次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第15次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第16次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第17次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第18次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第19次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第20次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第21次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第22次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第23次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第24次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第25次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第26次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第27次抽獎,結果為:抽到了價值「0.0」的獎品「謝謝參與」
第28次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第29次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第30次抽獎,結果為:抽到了價值「5.0」的獎品「5元金幣」
第31次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第32次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第33次抽獎,結果為:抽到了價值「5.0」的獎品「5元金幣」
第34次抽獎,結果為:抽到了價值「15.0」的獎品「15元天堂雨傘」
第35次抽獎,結果為:抽到了價值「5.0」的獎品「5元金幣」
第36次抽獎,結果為:抽到了價值「7000.0」的獎品「蘋果手機」
第37次抽獎,結果為:獎品抽完了,下次早點來吧~
第38次抽獎,結果為:獎品抽完了,下次早點來吧~
第39次抽獎,結果為:獎品抽完了,下次早點來吧~
第40次抽獎,結果為:獎品抽完了,下次早點來吧~
第41次抽獎,結果為:獎品抽完了,下次早點來吧~
第42次抽獎,結果為:獎品抽完了,下次早點來吧~
第43次抽獎,結果為:獎品抽完了,下次早點來吧~
第44次抽獎,結果為:獎品抽完了,下次早點來吧~
第45次抽獎,結果為:獎品抽完了,下次早點來吧~
第46次抽獎,結果為:獎品抽完了,下次早點來吧~
第47次抽獎,結果為:獎品抽完了,下次早點來吧~
第48次抽獎,結果為:獎品抽完了,下次早點來吧~
第49次抽獎,結果為:獎品抽完了,下次早點來吧~
第50次抽獎,結果為:獎品抽完了,下次早點來吧~
抽獎結束.
Process finished with exit code 0
*/
參考資料
「閱讀原文」