关于java:选择一个随机加权元素,带样本,无替换

Pick a random weighted element, with sample, no replacement

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

1
2
3
4
5
6
7
8
Map{
 "a" -> 2
 "b" -> 2
 "c" -> 2
 "d" -> 1
 "e" -> 1
 "f" -> 1
}

如何生成用于显示目的的样本+获胜者?

我当前(伪)代码:

1
2
3
4
5
6
list out;
foreach(entry:map){
  for(entry.value){
    out.add(a)
  }
}

然后创建一个用于显示的示例。

1
2
3
4
5
Collections.shuffle(out);
List display = out.stream()
  .distinct()
  .limit(8)
  .collect(Collectors.toList());

有了这段代码,我能相信吗?如果我选择胜利者

1
winner = display.get(0);

我认识到,添加最后一个元素可能会使结果倾斜,因为在发生不同的调用之后,它将使选择权重较低的数字的可能性更大。

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}

从哈希图中按键获取值非常快。您可以通过指定dropTable的初始容量和加载因子(见hashmap的javadoc)进一步优化它,但这取决于您自己的判断。

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


看看随机通用抽样和适配比例选择。根据权重取一个样本的简单方法可以通过将每个元素表示为一个间隔来解释,其长度与其权重成比例。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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}。而且它也不能很好地处理实值权重。


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

1
2
3
4
5
6
7
8
Map{
  0 ->"a"
  2 ->"b"
  4 ->"c"
  5 ->"d"
  6 ->"e"
  7 ->"f"
}

然后,为了使事情更快(或允许一个非常大的抢劫表),我会有一个值,如int maxValue = 7。现在,为了从表中得到一个战利品,我可以调用0maxValue之间的随机整数lootDrop。然后我可以遍历我的表,找到小于或等于lootDrop的最大值。如果您需要将映射保持为string to integer映射,并控制整数映射,那么这样做也相当简单。

如果你不想走那么远,你可以在你的解决方案中得到一个0到8之间的随机整数,它仍然有效。

你坚持这个配方有什么原因吗?