使用别名算法(Alias Method)实现抽奖

使用别名算法(Alias Method)实现抽奖

别名算法是一种高效的概率抽样算法,特别适合处理具有不同概率的离散事件。下面我将基于你提供的奖品数据,实现一个完整的别名算法抽奖方案。

1. 别名算法实现

import java.util.*;

public class AliasMethod {
    private final double[] probability;
    private final int[] alias;
    private final Random random;
    private final List<Prize> prizes;

    public AliasMethod(List<Prize> prizes) {
        this.random = new Random();
        this.prizes = prizes;
        int size = prizes.size();
        this.probability = new double[size];
        this.alias = new int[size];
        
        // 1. 计算总概率(应该为1,这里做校验)
        double totalProb = prizes.stream().mapToDouble(p -> p.getProbability()).sum();
        if (Math.abs(totalProb - 1.0) > 1e-6) {
            throw new IllegalArgumentException("概率总和必须为1");
        }
        
        // 2. 初始化工作队列
        Queue<Integer> small = new ArrayDeque<>();
        Queue<Integer> large = new ArrayDeque<>();
        
        // 3. 计算每个元素的概率乘以N
        double[] scaledProb = new double[size];
        for (int i = 0; i < size; i++) {
            scaledProb[i] = prizes.get(i).getProbability() * size;
            if (scaledProb[i] < 1.0) {
                small.add(i);
            } else {
                large.add(i);
            }
        }
        
        // 4. 构建别名表
        while (!small.isEmpty() && !large.isEmpty()) {
            int less = small.poll();
            int more = large.poll();
            
            probability[less] = scaledProb[less];
            alias[less] = more;
            
            scaledProb[more] = (scaledProb[more] + scaledProb[less]) - 1.0;
            
            if (scaledProb[more] < 1.0) {
                small.add(more);
            } else {
                large.add(more);
            }
        }
        
        // 处理剩余元素
        while (!small.isEmpty()) {
            probability[small.poll()] = 1.0;
        }
        while (!large.isEmpty()) {
            probability[large.poll()] = 1.0;
        }
    }
    
    public Prize draw() {
        // 随机选择一列
        int column = random.nextInt(probability.length);
        
        // 随机决定是选择当前列还是别名列
        boolean coinToss = random.nextDouble() < probability[column];
        int prizeIndex = coinToss ? column : alias[column];
        
        return prizes.get(prizeIndex);
    }
}

2. 奖品实体类

public class Prize {
    private String id;
    private String name;
    private double probability;
    
    // 构造方法、getter和setter
    public Prize(String id, String name, double probability) {
        this.id = id;
        this.name = name;
        this.probability = probability;
    }
    
    public String getId() { return id; }
    public String getName() { return name; }
    public double getProbability() { return probability; }
}

3. 使用示例

public class LotteryDemo {
    public static void main(String[] args) {
        // 初始化奖品数据
        List<Prize> prizes = Arrays.asList(
            new Prize("1", "1元话费", 0.2),
            new Prize("2", "5元话费", 0.2),
            new Prize("3", "5元话费", 0.1),
            new Prize("5", "感谢参与", 0.5)
        );
        
        // 创建别名算法实例
        AliasMethod am = new AliasMethod(prizes);
        
        // 模拟抽奖100次并统计结果
        Map<String, Integer> stats = new HashMap<>();
        for (int i = 0; i < 100; i++) {
            Prize prize = am.draw();
            stats.put(prize.getName(), stats.getOrDefault(prize.getName(), 0) + 1);
        }
        
        // 打印统计结果
        System.out.println("抽奖结果统计:");
        stats.forEach((name, count) -> {
            System.out.printf("%s: %d次 (%.1f%%)%n", 
                name, count, count / 100.0 * 100);
        });
    }
}

4. 算法说明

  1. 初始化阶段:

    • 计算每个奖品的概率乘以奖品数量(N),得到缩放概率
    • 将缩放概率小于1的放入small队列,大于等于1的放入large队列
  2. 构建别名表:

    • 从small和large队列各取一个元素
    • small元素的概率设为它的缩放概率
    • small元素的别名设为large元素的索引
    • 调整large元素的缩放概率
    • 重复直到队列为空
  3. 抽奖阶段:

    • 随机选择一列
    • 根据概率决定是选择当前列还是别名列
    • 返回对应的奖品

5. 性能分析

  • 初始化时间复杂度: O(N)
  • 单次抽奖时间复杂度: O(1)
  • 空间复杂度: O(N)

相比简单的轮盘赌算法(每次抽奖O(N)),别名算法在多次抽奖时性能优势明显。

6. 测试结果示例

运行上述代码可能的输出(因随机性每次不同):

抽奖结果统计:
1元话费: 18次 (18.0%)
5元话费: 28次 (28.0%)  # 两个5元话费合计
感谢参与: 54次 (54.0%)

这个结果基本符合设定的概率分布(20%, 30%, 50%)。

posted @ 2025-06-11 21:29  VipSoft  阅读(77)  评论(0)    收藏  举报