游戏简介
彩票是一种基于随机抽选的博彩游戏。以双色球为例,玩家从1-33的红球中选6个,从1-16的蓝球中选1个,开奖时随机抽取对应数量的球,按命中个数决定奖项。本文用Java实现彩票系统的核心概率计算模块,重点讲解组合数学中的概率推导与蒙特卡洛模拟方法。
核心算法思路
组合数学概率计算
双色球中各奖项的中奖概率可通过组合数精确计算:
- 一等奖(6+1):命中全部6个红球和1个蓝球
-
概率 = 1 / C(33,6) × 1/16 = 1 / 17,721,088
-
二等奖(6+0):命中6个红球,蓝球未中
-
概率 = 1 / C(33,6) × 15/16
-
三等奖(5+1):命中5个红球和1个蓝球
- 概率 = C(6,5)×C(27,1) / C(33,6) × 1/16
一般地,命中k个红球和m个蓝球的概率为:
– P(k红,m蓝) = C(6,k)×C(27,6-k) / C(33,6) × C(1,m)×C(15,1-m) / C(16,1)
蒙特卡洛模拟
当概率模型复杂或需要估计期望值(如奖金期望)时,蒙特卡洛模拟是有效工具:
- 重复进行大量随机开奖实验(如100万次)
- 统计各奖项命中次数
- 用频率近似概率,计算期望收益
期望值分析
期望值 = Σ(各奖项概率 × 对应奖金) – 投注成本
若期望值 < 0,则长期玩下去必然亏损。
Java实现
import java.util.*;
/**
* 彩票概率计算与蒙特卡洛模拟
*/
public class LotteryProbability {
// 双色球参数
private static final int RED_TOTAL = 33;
private static final int RED_CHOOSE = 6;
private static final int BLUE_TOTAL = 16;
private static final int BLUE_CHOOSE = 1;
// 各奖项奖金(简化示例)
private static final Map<String, Integer> PRIZES = new LinkedHashMap<>() {{
put("一等奖(6+1)", 5_000_000);
put("二等奖(6+0)", 200_000);
put("三等奖(5+1)", 3_000);
put("四等奖(5+0或4+1)", 200);
put("五等奖(4+0或3+1)", 10);
put("六等奖(2+1或1+1或0+1)", 5);
}};
private Random rand = new Random();
/**
* 计算组合数 C(n,k)
*/
public long combination(int n, int k) {
if (k < 0 || k > n) return 0;
if (k == 0 || k == n) return 1;
k = Math.min(k, n - k);
long result = 1;
for (int i = 0; i < k; i++) {
result = result * (n - i) / (i + 1);
}
return result;
}
/**
* 计算精确的中奖概率
* @param matchRed 命中红球数
* @param matchBlue 是否命中蓝球
* @return 该结果的概率
*/
public double calculateProbability(int matchRed, boolean matchBlue) {
long redWays = combination(RED_CHOOSE, matchRed)
* combination(RED_TOTAL - RED_CHOOSE, RED_CHOOSE - matchRed);
long totalRed = combination(RED_TOTAL, RED_CHOOSE);
double redProb = (double) redWays / totalRed;
long blueWays = matchBlue ? 1 : (BLUE_TOTAL - 1);
long totalBlue = BLUE_TOTAL;
double blueProb = (double) blueWays / totalBlue;
return redProb * blueProb;
}
/**
* 计算并输出所有可能结果的概率分布
*/
public void printProbabilityDistribution() {
System.out.println("=== 双色球概率分布 ===\n");
System.out.printf("%-15s %-12s %-18s %-15s%n",
"中奖情况", "红球命中", "蓝球命中", "概率");
System.out.println("-".repeat(65));
double totalProb = 0;
for (int red = 0; red <= RED_CHOOSE; red++) {
for (int blue = 0; blue <= 1; blue++) {
double prob = calculateProbability(red, blue == 1);
totalProb += prob;
String desc = getPrizeName(red, blue == 1);
System.out.printf("%-15s %-12d %-18s %-15.8f%% (%s)%n",
desc, red, blue == 1 ? "命中" : "未中",
prob * 100, formatFraction(prob));
}
}
System.out.println("-".repeat(65));
System.out.printf("总概率验证:%.10f%n", totalProb);
}
/**
* 根据命中数量返回奖项名称
*/
private String getPrizeName(int matchRed, boolean matchBlue) {
if (matchRed == 6 && matchBlue) return "一等奖";
if (matchRed == 6) return "二等奖";
if (matchRed == 5 && matchBlue) return "三等奖";
if ((matchRed == 5) || (matchRed == 4 && matchBlue)) return "四等奖";
if ((matchRed == 4) || (matchRed == 3 && matchBlue)) return "五等奖";
if (matchBlue) return "六等奖";
return "未中奖";
}
/**
* 格式化概率为1/N的形式
*/
private String formatFraction(double prob) {
if (prob < 1e-10) return "极小";
long n = Math.round(1.0 / prob);
return "1/" + n;
}
/**
* 蒙特卡洛模拟
* @param trials 模拟次数
* @param userReds 用户选择的红球
* @param userBlue 用户选择的蓝球
*/
public void monteCarloSimulation(int trials, Set<Integer> userReds, int userBlue) {
System.out.println("\n=== 蒙特卡洛模拟 (" + trials + " 次) ===\n");
Map<String, Integer> counts = new LinkedHashMap<>();
for (String key : PRIZES.keySet()) {
counts.put(key, 0);
}
counts.put("未中奖", 0);
long totalPrize = 0;
for (int i = 0; i < trials; i++) {
// 随机开奖
Set<Integer> drawnReds = drawNumbers(RED_TOTAL, RED_CHOOSE);
int drawnBlue = rand.nextInt(BLUE_TOTAL) + 1;
// 计算命中数
int matchRed = countIntersection(userReds, drawnReds);
boolean matchBlue = (userBlue == drawnBlue);
String prize = getPrizeName(matchRed, matchBlue);
counts.merge(prize, 1, Integer::sum);
int prizeMoney = PRIZES.getOrDefault(prize, 0);
totalPrize += prizeMoney;
}
// 输出结果
System.out.printf("%-20s %-12s %-15s %-15s%n",
"奖项", "命中次数", "频率(%)", "理论概率(%)");
System.out.println("-".repeat(70));
for (Map.Entry<String, Integer> entry : counts.entrySet()) {
String prize = entry.getKey();
int count = entry.getValue();
double freq = (double) count / trials * 100;
// 计算理论概率
double theoryProb = 0;
if (prize.equals("未中奖")) {
theoryProb = 1.0 - calculateWinProbability();
} else {
theoryProb = getPrizeTheoryProbability(prize);
}
System.out.printf("%-20s %-12d %-15.4f %-15.4f%n",
prize, count, freq, theoryProb * 100);
}
double cost = trials * 2.0; // 每注2元
double expectedValue = (totalPrize - cost) / trials;
System.out.println("-".repeat(70));
System.out.printf("总投注:%.0f元,总奖金:%d元%n", cost, totalPrize);
System.out.printf("单注期望收益:%.4f元%n", expectedValue);
System.out.printf("返奖率:%.2f%%%n", (totalPrize / cost) * 100);
}
/**
* 随机抽取指定数量的不重复数字
*/
private Set<Integer> drawNumbers(int total, int count) {
Set<Integer> result = new HashSet<>();
while (result.size() < count) {
result.add(rand.nextInt(total) + 1);
}
return result;
}
/**
* 计算两个集合的交集大小
*/
private int countIntersection(Set<Integer> a, Set<Integer> b) {
int count = 0;
for (int x : a) {
if (b.contains(x)) count++;
}
return count;
}
/**
* 计算中奖总概率(任意奖项)
*/
public double calculateWinProbability() {
double prob = 0;
// 六等奖:命中蓝球
prob += calculateProbability(0, true);
prob += calculateProbability(1, true);
prob += calculateProbability(2, true);
// 五等奖
prob += calculateProbability(3, true);
prob += calculateProbability(4, false);
// 四等奖
prob += calculateProbability(4, true);
prob += calculateProbability(5, false);
// 三等奖
prob += calculateProbability(5, true);
// 二等奖
prob += calculateProbability(6, false);
// 一等奖
prob += calculateProbability(6, true);
return prob;
}
/**
* 获取特定奖项的理论概率
*/
private double getPrizeTheoryProbability(String prizeName) {
return switch (prizeName) {
case "一等奖(6+1)" -> calculateProbability(6, true);
case "二等奖(6+0)" -> calculateProbability(6, false);
case "三等奖(5+1)" -> calculateProbability(5, true);
case "四等奖(5+0或4+1)" -> calculateProbability(5, false) + calculateProbability(4, true);
case "五等奖(4+0或3+1)" -> calculateProbability(4, false) + calculateProbability(3, true);
case "六等奖(2+1或1+1或0+1)" ->
calculateProbability(2, true) + calculateProbability(1, true) + calculateProbability(0, true);
default -> 0;
};
}
/**
* 计算长期投注的期望值
*/
public void calculateExpectedValue() {
System.out.println("\n=== 期望值分析 ===\n");
double expectedPrize = 0;
for (Map.Entry<String, Integer> entry : PRIZES.entrySet()) {
double prob = getPrizeTheoryProbability(entry.getKey());
double contribution = prob * entry.getValue();
expectedPrize += contribution;
System.out.printf("%-20s 概率:%12.8f%% 期望贡献:%.4f元%n",
entry.getKey(), prob * 100, contribution);
}
System.out.println("-".repeat(60));
System.out.printf("单注期望奖金:%.4f元%n", expectedPrize);
System.out.printf("单注成本:2.00元%n");
System.out.printf("单注期望收益:%.4f元%n", expectedPrize - 2.0);
}
public static void main(String[] args) {
LotteryProbability lottery = new LotteryProbability();
// 1. 精确概率计算
lottery.printProbabilityDistribution();
// 2. 期望值分析
lottery.calculateExpectedValue();
// 3. 蒙特卡洛模拟
Set<Integer> myNumbers = new HashSet<>(Arrays.asList(3, 8, 15, 19, 26, 31));
int myBlue = 7;
lottery.monteCarloSimulation(1_000_000, myNumbers, myBlue);
}
}
进阶:生日问题与重复检测
在大量随机选号中,可以用哈希表检测是否有玩家选择了完全相同的号码组合:
/**
* 检测大量选号中的重复组合
*/
public void detectDuplicateTickets(List<Set<Integer>> tickets, List<Integer> blues) {
Map<String, Integer> frequency = new HashMap<>();
for (int i = 0; i < tickets.size(); i++) {
String key = tickets.get(i).toString() + "|" + blues.get(i);
frequency.merge(key, 1, Integer::sum);
}
int duplicates = 0;
for (Map.Entry<String, Integer> entry : frequency.entrySet()) {
if (entry.getValue() > 1) {
duplicates++;
}
}
System.out.println("总注数:" + tickets.size());
System.out.println("重复组合数:" + duplicates);
System.out.println("理论期望重复数(生日问题):" +
tickets.size() * (tickets.size() - 1) / (2.0 * combination(33,6) * 16));
}
复杂度分析
| 计算类型 | 时间复杂度 | 说明 |
|---|---|---|
| 精确概率 | O(k) | k为命中数范围,常数时间 |
| 组合数C(n,k) | O(k) | k较小(如6) |
| 蒙特卡洛 | O(T×n) | T为模拟次数,n为单次检查开销 |
| 重复检测 | O(N) | N为注数,哈希表平均O(1)查询 |
总结
本文通过Java实现了彩票系统的概率计算与蒙特卡洛模拟,核心要点包括:
- 组合数学精确计算各奖项概率,C(n,k)是核心工具
- 蒙特卡洛模拟用频率近似概率,适用于复杂场景和期望收益估计
- 期望值分析揭示长期投注的数学本质
- 生日问题模型分析大规模选号中的重复概率
读者可以扩展:实现大乐透等其他彩种、加入追加投注计算、用并行流加速蒙特卡洛模拟。