Java 选择随机加权元素,带样本,不替换

Java 选择随机加权元素,带样本,不替换,java,random,probability,Java,Random,Probability,给定一个表示战利品表中奖励的结构,其中a是奖励类型,2是整数权重,这意味着a被拉出的可能性是d的两倍 Map{ "a" -> 2 "b" -> 2 "c" -> 2 "d" -> 1 "e" -> 1 "f" -> 1 } 如何生成用于显示目的的样本+赢家 我当前的(伪)代码: 然后创建用于显示的示例 Collections.shuffle(out); List display = out.stream() .distinct()

给定一个表示战利品表中奖励的结构,其中a是奖励类型,2是整数权重,这意味着a被拉出的可能性是d的两倍

Map{
  "a" -> 2
  "b" -> 2
  "c" -> 2
  "d" -> 1
  "e" -> 1
  "f" -> 1
}
如何生成用于显示目的的样本+赢家

我当前的(伪)代码:

然后创建用于显示的示例

Collections.shuffle(out);
List display = out.stream()
  .distinct()
  .limit(8)
  .collect(Collectors.toList());
有了这个密码,我能相信吗?不同的是,如果我选择胜利者

winner = display.get(0);
我意识到添加最后一个元素可能会扭曲结果,因为在发生不同的调用之后,它更可能选择权重较低的数字


但是选择流的第一个元素应该是值得信任的,对吗?因为它是以前选择的。distinct有它的状态诱导效应吗?

您的数据结构实现似乎有点奇怪。我会这样做:

Map{
  0 -> "a"
  2 -> "b"
  4 -> "c"
  5 -> "d"
  6 -> "e"
  7 -> "f"
}
然后,为了让事情变得更快(或者允许一个非常大的战利品表),我需要一个类似
intmaxvalue=7
的值。现在,要从表中获取一个loot项,我可以调用
0
maxValue
(包括)之间的随机整数
lootDrop
。然后我可以遍历我的表,找到小于或等于
lootdrop
的最大值。如果需要将映射保持为
字符串到整数的映射,并控制整数映射,那么这样做也相当简单

如果您不想走那么远,只需在解决方案中获得一个介于0和8之间的随机整数,这仍然有效

你坚持使用这个公式有什么原因吗?

看看下面。根据权重获取一个样本的简单方法可以通过将每个元素表示为长度与其权重成比例的间隔来解释。例如:

Map{
  "a" -> 2 // weight 2
  "b" -> 2
  "c" -> 2
  "d" -> 1
  "e" -> 1
  "f" -> 1
}
=>
Map{
  "a" -> (0,2) // weight 2 -- is now length of the interval
  "b" -> (2,4) // ...
  "c" -> (4,6)
  "d" -> (6,7)
  "e" -> (7,8)
  "f" -> (8,9)
}
然后从0到9中选取随机数
9*Math.random()
(作为指向该范围的指针),并检查它所属的区间——这是随机样本w.r.t输入权重。重复此操作,直到获得所需数量的样本(如果愿意,可以忽略重复的样本)

当然,这是一个有点惯用的解释,在实际代码中,您将只保留上界,因为下界只是前一个元素的上界。然后,您将选择第一个在随机指针上方具有边界的元素



更新:从数学的角度来看,您最初重复元素的方法是可行的(使用双重重量拾取项圈的概率是双倍),但当重量较高时,这将是一个问题:
Map{“a”->1000“b”->100000}
。此外,它也不能很好地处理实值权重。

我喜欢Martin的答案,但我也会根据他提出的性能问题发布我自己的警告/备选方案。使用Map可以实现与他自己的实现非常相似的实现(我将使用HashMap,因为它是我最喜欢的)

private final AtomicLong idxCounter=新的AtomicLong(0);
private final Map dropTable=new HashMap();
公共void addDrop(项项,长相对频率){
while(相对频率-->0){
Long nextix=idxCounter.getAndIncrement();
dropTable.put(nextIdx,item);
}
}
私有静态最终随机rng=新随机(System.currentTimeMillis());
公共项getRandomDrop(){
Long size=idxCounter.get();
//randomValue将是间隔[0,size]中的某个值,该值
//应该覆盖整个升降台。
//看http://stackoverflow.com/questions/2546078 公平地
//nextLong的实现。
长随机值=下一个长(rng,大小);
返回dropTable.get(随机值);
}
从HashMap中按键获取值非常快。您可以通过指定
dropTable
初始容量和负载因子(请参阅)对其进行进一步优化,但这取决于您自己的判断


它也是线程安全的,只要没有其他东西在玩弄
dropTable

,主要原因是它使用户配置更容易。问题还在于简化了数据结构,而不是映射,它实际上是一个具有直接在配置中定义的多个属性的映射持续时间文件。其中Reward.weight是属性之一。这与OPs解决方案存在相同的问题:当
相对频率
很大时,您必须在内存中保留巨大的映射。它也不支持实值权重。最好保留一个连续
的列表[low,high)
bounds;在这里,您可以使用二进制搜索找到目标样本。感谢您确认数学正确,我不希望表变得非常大。我的主要问题是我是否可以信任Java 8流发出第一个选择的响应。我很想转换到您的方法,只是为了提供更多的f灵活性与机会。
Map{
  "a" -> 2 // weight 2
  "b" -> 2
  "c" -> 2
  "d" -> 1
  "e" -> 1
  "f" -> 1
}
=>
Map{
  "a" -> (0,2) // weight 2 -- is now length of the interval
  "b" -> (2,4) // ...
  "c" -> (4,6)
  "d" -> (6,7)
  "e" -> (7,8)
  "f" -> (8,9)
}
private final AtomicLong idxCounter = new AtomicLong(0);
private final Map<Long, Item> dropTable = new HashMap<>();
public void addDrop(Item item, long relativeFrequency) {
    while (relativeFrequency-- > 0) {
        Long nextIdx = idxCounter.getAndIncrement();
        dropTable.put(nextIdx, item);
    }
}

private static final Random rng = new Random(System.currentTimeMillis());
public Item getRandomDrop() {
    Long size = idxCounter.get();
    // randomValue will be something in the interval [0, size), which 
    // should cover the whole dropTable.
    // See http://stackoverflow.com/questions/2546078 for a fair
    // implementation of nextLong.
    Long randomValue = nextLong(rng, size); 
    return dropTable.get(randomValue); 
}