每日算法 — 使用java实现彩票:组合数学概率计算与蒙特卡洛期望模拟

游戏简介

彩票是一种基于随机抽选的博彩游戏。以双色球为例,玩家从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)

蒙特卡洛模拟

当概率模型复杂或需要估计期望值(如奖金期望)时,蒙特卡洛模拟是有效工具:

  1. 重复进行大量随机开奖实验(如100万次)
  2. 统计各奖项命中次数
  3. 用频率近似概率,计算期望收益

期望值分析

期望值 = Σ(各奖项概率 × 对应奖金) – 投注成本
若期望值 < 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)是核心工具
  • 蒙特卡洛模拟用频率近似概率,适用于复杂场景和期望收益估计
  • 期望值分析揭示长期投注的数学本质
  • 生日问题模型分析大规模选号中的重复概率

读者可以扩展:实现大乐透等其他彩种、加入追加投注计算、用并行流加速蒙特卡洛模拟。