使用别名算法(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. 算法说明
-
初始化阶段:
- 计算每个奖品的概率乘以奖品数量(N),得到缩放概率
- 将缩放概率小于1的放入small队列,大于等于1的放入large队列
-
构建别名表:
- 从small和large队列各取一个元素
- small元素的概率设为它的缩放概率
- small元素的别名设为large元素的索引
- 调整large元素的缩放概率
- 重复直到队列为空
-
抽奖阶段:
- 随机选择一列
- 根据概率决定是选择当前列还是别名列
- 返回对应的奖品
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%)。
本文来自博客园,作者:VipSoft 转载请注明原文链接:https://www.cnblogs.com/vipsoft/p/18924580
浙公网安备 33010602011771号