问题描述
双色球是中国福利彩票的经典玩法:从1-33中选取6个红球(不重复),再从1-16中选取1个蓝球,组成一注号码。开奖时随机摇出6个红球和1个蓝球,根据命中数量确定奖级。
本文将用Java实现双色球的完整模拟系统,包括组合数计算、各奖级概率精确求解,以及蒙特卡洛方法验证理论概率。
核心算法思路
组合数计算
从 $n$ 个元素中选取 $k$ 个的组合数为:
$$C(n,k) = \frac{n!}{k!(n-k)!}$$
双色球一等奖(6+1)的中奖概率为:
$$P = \frac{1}{C(33,6) \times 16} = \frac{1}{17,721,088}$$
各奖级概率模型
| 奖级 | 红球命中 | 蓝球命中 | 组合数计算 |
|---|---|---|---|
| 一等奖 | 6 | 1 | $C(6,6) \times C(27,0) \times C(1,1)$ |
| 二等奖 | 6 | 0 | $C(6,6) \times C(27,0) \times C(15,1)$ |
| 三等奖 | 5 | 1 | $C(6,5) \times C(27,1) \times C(1,1)$ |
| 四等奖 | 5/4 | 0/1 | 分别计算 |
| 五等奖 | 4/3 | 0/1 | 分别计算 |
| 六等奖 | 2/1/0 | 1 | 分别计算 |
蒙特卡洛模拟
通过大量随机模拟(如1000万次)统计各奖级出现频率,验证理论计算的正确性。
Java实现
import java.util.*;
/**
* 双色球概率分析与蒙特卡洛模拟
* 精确计算各奖级中奖概率并通过大数定律验证
*/
public class DoubleColorBall {
// 红球池大小、选取数量,蓝球池大小
private static final int RED_POOL = 33;
private static final int RED_SELECT = 6;
private static final int BLUE_POOL = 16;
// 奖级名称
private static final String[] PRIZE_NAMES = {
"一等奖", "二等奖", "三等奖", "四等奖", "五等奖", "六等奖", "未中奖"
};
private Random random;
public DoubleColorBall() {
this.random = new Random();
}
public DoubleColorBall(long seed) {
this.random = new Random(seed);
}
/**
* 计算组合数 C(n, k)
* 使用递推避免大数阶乘溢出
*/
public long combination(int n, int k) {
if (k < 0 || k > n) return 0;
if (k == 0 || k == n) return 1;
if (k > n - k) k = n - k; // 对称性优化
long result = 1;
for (int i = 1; i <= k; i++) {
result = result * (n - k + i) / i;
}
return result;
}
/**
* 计算理论总注数
*/
public long totalCombinations() {
return combination(RED_POOL, RED_SELECT) * BLUE_POOL;
}
/**
* 计算特定奖级的理论注数
* @param redHit 红球命中数
* @param blueHit 蓝球命中数 (0或1)
* @return 该奖级的组合数
*/
public long calculatePrizeCombinations(int redHit, int blueHit) {
// 从6个中奖红球中命中redHit个
long redWays = combination(RED_SELECT, redHit);
// 从27个未中奖红球中选出剩余的红球
long redMissWays = combination(RED_POOL - RED_SELECT, RED_SELECT - redHit);
// 蓝球命中方式
long blueWays = blueHit == 1 ? 1 : (BLUE_POOL - 1);
return redWays * redMissWays * blueWays;
}
/**
* 判定奖级
* @param redHit 红球命中数
* @param blueHit 蓝球是否命中
* @return 奖级索引 (0-5为中奖, 6为未中奖)
*/
public int determinePrize(int redHit, boolean blueHit) {
if (redHit == 6 && blueHit) return 0; // 一等奖
if (redHit == 6 && !blueHit) return 1; // 二等奖
if (redHit == 5 && blueHit) return 2; // 三等奖
if ((redHit == 5 && !blueHit) || (redHit == 4 && blueHit)) return 3; // 四等奖
if ((redHit == 4 && !blueHit) || (redHit == 3 && blueHit)) return 4; // 五等奖
if (blueHit) return 5; // 六等奖 (2+1, 1+1, 0+1)
return 6; // 未中奖
}
/**
* 打印理论概率表
*/
public void printTheoreticalProbabilities() {
long total = totalCombinations();
System.out.println("=== 双色球理论概率分析 ===");
System.out.printf("总注数: %,d%n", total);
System.out.println();
// 各奖级组合数
long[] prizeCounts = new long[7];
prizeCounts[0] = calculatePrizeCombinations(6, 1);
prizeCounts[1] = calculatePrizeCombinations(6, 0);
prizeCounts[2] = calculatePrizeCombinations(5, 1);
prizeCounts[3] = calculatePrizeCombinations(5, 0) + calculatePrizeCombinations(4, 1);
prizeCounts[4] = calculatePrizeCombinations(4, 0) + calculatePrizeCombinations(3, 1);
prizeCounts[5] = calculatePrizeCombinations(2, 1) + calculatePrizeCombinations(1, 1)
+ calculatePrizeCombinations(0, 1);
long sum = 0;
for (int i = 0; i < 6; i++) sum += prizeCounts[i];
prizeCounts[6] = total - sum;
System.out.printf("%-8s %-15s %-18s %-15s%n", "奖级", "命中条件", "组合数", "概率");
System.out.println("-".repeat(65));
for (int i = 0; i < 7; i++) {
double prob = (double) prizeCounts[i] / total;
String condition;
switch (i) {
case 0: condition = "6+1"; break;
case 1: condition = "6+0"; break;
case 2: condition = "5+1"; break;
case 3: condition = "5+0/4+1"; break;
case 4: condition = "4+0/3+1"; break;
case 5: condition = "2+1/1+1/0+1"; break;
default: condition = "其他"; break;
}
System.out.printf("%-8s %-15s %,15d %.8f%% (%s)%n",
PRIZE_NAMES[i], condition, prizeCounts[i],
prob * 100, formatOdds(prob));
}
double winProb = 0;
for (int i = 0; i < 6; i++) winProb += (double) prizeCounts[i] / total;
System.out.println("-".repeat(65));
System.out.printf("总中奖概率: %.4f%% (约 %.2f 分之一)%n", winProb * 100, 1.0 / winProb);
}
private String formatOdds(double prob) {
if (prob <= 0) return "不可能";
double odds = 1.0 / prob;
if (odds > 10000000) return String.format("约 %.0f万分之一", odds / 10000);
if (odds > 10000) return String.format("约 %.0f千分之一", odds / 1000);
return String.format("约 %.0f分之一", odds);
}
/**
* 生成一注随机号码
*/
public Bet generateRandomBet() {
Set<Integer> reds = new TreeSet<>();
while (reds.size() < RED_SELECT) {
reds.add(random.nextInt(RED_POOL) + 1);
}
int blue = random.nextInt(BLUE_POOL) + 1;
return new Bet(reds, blue);
}
/**
* 计算两注号码的匹配情况
*/
public MatchResult match(Bet bet, Bet winning) {
int redHit = 0;
for (int r : bet.redBalls) {
if (winning.redBalls.contains(r)) redHit++;
}
boolean blueHit = (bet.blueBall == winning.blueBall);
int prize = determinePrize(redHit, blueHit);
return new MatchResult(redHit, blueHit, prize);
}
/**
* 蒙特卡洛模拟
* @param iterations 模拟次数
*/
public void monteCarloSimulation(long iterations) {
System.out.printf("\n=== 蒙特卡洛模拟 (%,d 次) ===%n", iterations);
Bet winningBet = generateRandomBet();
long[] prizeCounts = new long[7];
for (long i = 0; i < iterations; i++) {
Bet randomBet = generateRandomBet();
MatchResult result = match(randomBet, winningBet);
prizeCounts[result.prizeLevel]++;
}
System.out.printf("%-8s %-15s %-15s %-15s%n", "奖级", "命中条件", "模拟次数", "模拟概率");
System.out.println("-".repeat(60));
for (int i = 0; i < 7; i++) {
double prob = (double) prizeCounts[i] / iterations;
String condition;
switch (i) {
case 0: condition = "6+1"; break;
case 1: condition = "6+0"; break;
case 2: condition = "5+1"; break;
case 3: condition = "5+0/4+1"; break;
case 4: condition = "4+0/3+1"; break;
case 5: condition = "2+1/1+1/0+1"; break;
default: condition = "其他"; break;
}
System.out.printf("%-8s %-15s %,12d %.6f%%%n",
PRIZE_NAMES[i], condition, prizeCounts[i], prob * 100);
}
}
// 数据类
public static class Bet {
public final Set<Integer> redBalls;
public final int blueBall;
public Bet(Set<Integer> redBalls, int blueBall) {
this.redBalls = redBalls;
this.blueBall = blueBall;
}
@Override
public String toString() {
return "红球: " + redBalls + " 蓝球: " + blueBall;
}
}
public static class MatchResult {
public final int redHits;
public final boolean blueHit;
public final int prizeLevel;
public MatchResult(int redHits, boolean blueHit, int prizeLevel) {
this.redHits = redHits;
this.blueHit = blueHit;
this.prizeLevel = prizeLevel;
}
}
public static void main(String[] args) {
DoubleColorBall dcb = new DoubleColorBall(42);
dcb.printTheoreticalProbabilities();
// 蒙特卡洛验证
dcb.monteCarloSimulation(1_000_000);
// 生成一注随机号码
System.out.println("\n随机一注: " + dcb.generateRandomBet());
}
}
算法复杂度分析
- 组合数计算:$O(k)$,其中 $k \leq 6$ 为选取数量,通过对称性优化后最多计算6次乘除。
- 理论概率求解:$O(1)$,所有奖级均可通过闭式公式直接计算。
- 蒙特卡洛模拟:$O(n \times k \log k)$,$n$ 为模拟次数,每次需要对比6个红球。
扩展思考
- 大数定律:当蒙特卡洛模拟次数趋于无穷时,模拟频率收敛于理论概率。通常100万次模拟即可得到3位有效数字的精度。
- 期望值计算:根据各奖级奖金,可计算单注彩票的期望收益。以现行规则计算,期望收益率约为50%左右。
- 复式投注:若一次选取7个红球,组合数变为 $C(7,6) \times 16 = 112$ 注,可用相同方法计算成本与覆盖率。
总结
双色球问题完美展示了组合数学在实际中的应用。通过精确计算 $C(33,6)$ 种红球组合与16种蓝球的笛卡尔积,可以严格推导出各奖级的理论概率。蒙特卡洛模拟则作为验证手段,直观展示了大数定律的收敛过程。