在Java中,给定 n 项目,每个项目的权重 w ,如何从集合中选择一个等于 w的随机项目
假设每个权重是从0.0到1.0的双精度,并且集合中的权重总和为1. Item.getWeight()返回Item的权重。
答案 0 :(得分:36)
Item[] items = ...;
// Compute the total weight of all items together
double totalWeight = 0.0d;
for (Item i : items)
{
totalWeight += i.getWeight();
}
// Now choose a random item
int randomIndex = -1;
double random = Math.random() * totalWeight;
for (int i = 0; i < items.length; ++i)
{
random -= items[i].getWeight();
if (random <= 0.0d)
{
randomIndex = i;
break;
}
}
Item myRandomItem = items[randomIndex];
答案 1 :(得分:12)
一种优雅的方法是对指数分布http://en.wikipedia.org/wiki/Exponential_distribution进行抽样,其中权重将是分布的速率(lambda)。最后,您只需选择最小的采样值。
在Java中,这看起来像这样:
public static <E> E getWeightedRandom(Map<E, Double> weights, Random random) {
E result = null;
double bestValue = Double.MAX_VALUE;
for (E element : weights.keySet()) {
double value = -Math.log(random.nextDouble()) / weights.get(element);
if (value < bestValue) {
bestValue = value;
result = element;
}
}
return result;
}
我不确定这是否比其他方法更有效,但如果执行时间不是问题,那么这是一个很好看的解决方案。
这与使用Java 8和Streams的想法相同:
public static <E> E getWeightedRandomJava8(Stream<Entry<E, Double>> weights, Random random) {
return weights
.map(e -> new SimpleEntry<E,Double>(e.getKey(),-Math.log(random.nextDouble()) / e.getValue()))
.min((e0,e1)-> e0.getValue().compareTo(e1.getValue()))
.orElseThrow(IllegalArgumentException::new).getKey();
}
您可以通过将其转换为.entrySet().stream()
来获取地图中的输入权重流。
答案 2 :(得分:11)
TreeMap已经为您完成了所有工作。
创建一个TreeMap。根据您选择的方法创建权重。添加以0.0开头的权重,同时将最后一个元素的权重添加到正在运行的权重计数器中。
即。 (斯卡拉):
var count = 0.0
for { object <- MyObjectList } { //Just any iterator over all objects
map.insert(count, object)
count += object.weight
}
然后您只需生成rand = new Random(); num = rand.nextDouble() * count
即可获得有效数字。
map.to(num).last // Scala
map.floorKey(num) // Java
会给你随机加权项目。
对于少量的桶也是可能的:创建一个100,000 100,000的数组,并根据字段的权重分配桶的数量。然后创建一个0到100,000-1之间的随机整数,然后立即返回桶号。
答案 3 :(得分:6)
如果您想要运行时选择效率,那么在设置上花费更多时间可能是最好的。这是一种可能的解决方案。它有更多的代码,但保证log(n)选择。
WeightedItemSelector实现从加权对象集合中选择随机对象。 选择保证在log(n)时间内运行。
public class WeightedItemSelector<T> {
private final Random rnd = new Random();
private final TreeMap<Object, Range<T>> ranges = new TreeMap<Object, Range<T>>();
private int rangeSize; // Lowest integer higher than the top of the highest range.
public WeightedItemSelector(List<WeightedItem<T>> weightedItems) {
int bottom = 0; // Increments by size of non zero range added as ranges grows.
for(WeightedItem<T> wi : weightedItems) {
int weight = wi.getWeight();
if(weight > 0) {
int top = bottom + weight - 1;
Range<T> r = new Range<T>(bottom, top, wi);
if(ranges.containsKey(r)) {
Range<T> other = ranges.get(r);
throw new IllegalArgumentException(String.format("Range %s conflicts with range %s", r, other));
}
ranges.put(r, r);
bottom = top + 1;
}
}
rangeSize = bottom;
}
public WeightedItem<T> select() {
Integer key = rnd.nextInt(rangeSize);
Range<T> r = ranges.get(key);
if(r == null)
return null;
return r.weightedItem;
}
}
Range实现范围选择以利用TreeMap选择。
class Range<T> implements Comparable<Object>{
final int bottom;
final int top;
final WeightedItem<T> weightedItem;
public Range(int bottom, int top, WeightedItem<T> wi) {
this.bottom = bottom;
this.top = top;
this.weightedItem = wi;
}
public WeightedItem<T> getWeightedItem() {
return weightedItem;
}
@Override
public int compareTo(Object arg0) {
if(arg0 instanceof Range<?>) {
Range<?> other = (Range<?>) arg0;
if(this.bottom > other.top)
return 1;
if(this.top < other.bottom)
return -1;
return 0; // overlapping ranges are considered equal.
} else if (arg0 instanceof Integer) {
Integer other = (Integer) arg0;
if(this.bottom > other.intValue())
return 1;
if(this.top < other.intValue())
return -1;
return 0;
}
throw new IllegalArgumentException(String.format("Cannot compare Range objects to %s objects.", arg0.getClass().getName()));
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{\"_class\": Range {\"bottom\":\"").append(bottom).append("\", \"top\":\"").append(top)
.append("\", \"weightedItem\":\"").append(weightedItem).append("}");
return builder.toString();
}
}
WeightedItem只是封装了一个要选择的项目。
public class WeightedItem<T>{
private final int weight;
private final T item;
public WeightedItem(int weight, T item) {
this.item = item;
this.weight = weight;
}
public T getItem() {
return item;
}
public int getWeight() {
return weight;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{\"_class\": WeightedItem {\"weight\":\"").append(weight).append("\", \"item\":\"")
.append(item).append("}");
return builder.toString();
}
}
答案 4 :(得分:5)
我想我之前已经这样做了......但是可能有更有效的方法来做到这一点。
答案 5 :(得分:1)
下面是一个随机发生器,它也能保持比例的精确度:
public class WeightedRandomizer
{
private final Random randomizer;
public WeightedRandomizer(Random randomizer)
{
this.randomizer = randomizer;
}
public IWeighable getRandomWeighable(List<IWeighable> weighables)
{
double totalWeight = 0.0;
long totalSelections = 1;
List<IWeighable> openWeighables = new ArrayList<>();
for (IWeighable weighable : weighables)
{
totalWeight += weighable.getAllocation();
totalSelections += weighable.getNumSelections();
}
for(IWeighable weighable : weighables)
{
double allocation = weighable.getAllocation() / totalWeight;
long numSelections = weighable.getNumSelections();
double proportion = (double) numSelections / (double) totalSelections;
if(proportion < allocation)
{
openWeighables.add(weighable);
}
}
IWeighable selection = openWeighables.get(this.randomizer.nextInt(openWeighables.size()));
selection.setNumSelections(selection.getNumSelections() + 1);
return selection;
}
}
答案 6 :(得分:1)
使用包含Item
方法的getWeight()
类(如您的问题所示):
/**
* Gets a random-weighted object.
* @param items list with weighted items
* @return a random item from items with a chance equal to its weight.
* @assume total weight == 1
*/
public static Item getRandomWeighted(List<Item> items) {
double value = Math.random(), weight = 0;
for (Item item : items) {
weight += item.getWeight();
if (value < weight)
return item;
}
return null; // Never will reach this point if assumption is true
}
使用Map
和更通用的方法:
/**
* Gets a random-weighted object.
* @param balancedObjects the map with objects and their weights.
* @return a random key-object from the map with a chance equal to its weight/totalWeight.
* @throws IllegalArgumentException if total weight is not positive.
*/
public static <E> E getRandomWeighted(Map<E, ? extends Number> balancedObjects) throws IllegalArgumentException {
double totalWeight = balancedObjects.values().stream().mapToInt(Number::intValue).sum(); // Java 8
if (totalWeight <= 0)
throw new IllegalArgumentException("Total weight must be positive.");
double value = Math.random()*totalWeight, weight = 0;
for (Entry<E, ? extends Number> e : balancedObjects.entrySet()) {
weight += e.getValue().doubleValue();
if (value < weight)
return e.getKey();
}
return null; // Never will reach this point
}