Java中的随机加权选择

问题描述 投票:58回答:7

我想从集合中选择一个随机项目,但是选择任何项目的机会应与相关的权重成正比

示例输入:

item                weight
----                ------
sword of misery         10
shield of happy          5
potion of dying          6
triple-edged sword       1

因此,如果我有4种可能的物品,那么没有重量的任何一件物品的机会都是4分之一。

在这种情况下,用户遭受痛苦之剑的可能性应该是三刃剑的十倍。

如何在Java中进行加权随机选择?

java random double
7个回答
102
投票
我会使用NavigableMap

public class RandomCollection<E> { private final NavigableMap<Double, E> map = new TreeMap<Double, E>(); private final Random random; private double total = 0; public RandomCollection() { this(new Random()); } public RandomCollection(Random random) { this.random = random; } public RandomCollection<E> add(double weight, E result) { if (weight <= 0) return this; total += weight; map.put(total, result); return this; } public E next() { double value = random.nextDouble() * total; return map.higherEntry(value).getValue(); } }

说我有一个动物狗,猫,马的概率分别为40%,35%和25%的列表

RandomCollection<String> rc = new RandomCollection<>() .add(40, "dog").add(35, "cat").add(25, "horse"); for (int i = 0; i < 10; i++) { System.out.println(rc.next()); }


23
投票
您将找不到针对此类问题的框架,因为所请求的功能仅是简单功能。做这样的事情:

interface Item { double getWeight(); } class RandomItemChooser { public Item chooseOnWeight(List<Item> items) { double completeWeight = 0.0; for (Item item : items) completeWeight += item.getWeight(); double r = Math.random() * completeWeight; double countWeight = 0.0; for (Item item : items) { countWeight += item.getWeight(); if (countWeight >= r) return item; } throw new RuntimeException("Should never be shown."); } }


20
投票
Apache Commons中现在有一个用于此的类:EnumeratedDistribution

Item selectedItem = new EnumeratedDistribution<>(itemWeights).sample();

其中itemWeightsList<Pair<Item, Double>>,例如(假定Arne的答案中的Item接口:]

final List<Pair<Item, Double>> itemWeights = Collections.newArrayList(); for (Item i: itemSet) { itemWeights.add(new Pair(i, i.getWeight())); }

或在Java 8中:

itemSet.stream().map(i -> new Pair(i, i.getWeight())).collect(toList());

注意: Pair这里需要为org.apache.commons.math3.util.Pair,而不是org.apache.commons.lang3.tuple.Pair


5
投票
使用别名方法

如果要滚动很多次(例如在游戏中),则应使用别名方法。

实际上,以下代码是这种别名方法的相当长的实现。但这是由于初始化部分。元素的检索非常快(请参见它们不循环的nextapplyAsInt方法)。

用法

Set<Item> items = ... ; ToDoubleFunction<Item> weighter = ... ; Random random = new Random(); RandomSelector<T> selector = RandomSelector.weighted(items, weighter); Item drop = selector.next(random);
实施

此实现:

    使用
  • Java 8;
  • 被设计为
  • 尽可能快
  • (至少我尝试使用微基准测试来做到这一点);完全是
  • 线程安全的
  • (为了最大化性能,在每个线程中保留一个Random,使用ThreadLocalRandom?];
  • 在O(1)中获取元素
  • ,这与您在Internet或StackOverflow上通常会在O(n)或O(log(n))中运行的简单实现不同;保留
  • 与重量无关的物品
  • ,因此可以在不同的上下文中为物品分配不同的重量。
无论如何,这是代码。 (请注意I maintain an up to date version of this class。)

import static java.util.Objects.requireNonNull; import java.util.*; import java.util.function.*; public final class RandomSelector<T> { public static <T> RandomSelector<T> weighted(Set<T> elements, ToDoubleFunction<? super T> weighter) throws IllegalArgumentException { requireNonNull(elements, "elements must not be null"); requireNonNull(weighter, "weighter must not be null"); if (elements.isEmpty()) { throw new IllegalArgumentException("elements must not be empty"); } // Array is faster than anything. Use that. int size = elements.size(); T[] elementArray = elements.toArray((T[]) new Object[size]); double totalWeight = 0d; double[] discreteProbabilities = new double[size]; // Retrieve the probabilities for (int i = 0; i < size; i++) { double weight = weighter.applyAsDouble(elementArray[i]); if (weight < 0.0d) { throw new IllegalArgumentException("weighter may not return a negative number"); } discreteProbabilities[i] = weight; totalWeight += weight; } if (totalWeight == 0.0d) { throw new IllegalArgumentException("the total weight of elements must be greater than 0"); } // Normalize the probabilities for (int i = 0; i < size; i++) { discreteProbabilities[i] /= totalWeight; } return new RandomSelector<>(elementArray, new RandomWeightedSelection(discreteProbabilities)); } private final T[] elements; private final ToIntFunction<Random> selection; private RandomSelector(T[] elements, ToIntFunction<Random> selection) { this.elements = elements; this.selection = selection; } public T next(Random random) { return elements[selection.applyAsInt(random)]; } private static class RandomWeightedSelection implements ToIntFunction<Random> { // Alias method implementation O(1) // using Vose's algorithm to initialize O(n) private final double[] probabilities; private final int[] alias; RandomWeightedSelection(double[] probabilities) { int size = probabilities.length; double average = 1.0d / size; int[] small = new int[size]; int smallSize = 0; int[] large = new int[size]; int largeSize = 0; // Describe a column as either small (below average) or large (above average). for (int i = 0; i < size; i++) { if (probabilities[i] < average) { small[smallSize++] = i; } else { large[largeSize++] = i; } } // For each column, saturate a small probability to average with a large probability. while (largeSize != 0 && smallSize != 0) { int less = small[--smallSize]; int more = large[--largeSize]; probabilities[less] = probabilities[less] * size; alias[less] = more; probabilities[more] += probabilities[less] - average; if (probabilities[more] < average) { small[smallSize++] = more; } else { large[largeSize++] = more; } } // Flush unused columns. while (smallSize != 0) { probabilities[small[--smallSize]] = 1.0d; } while (largeSize != 0) { probabilities[large[--largeSize]] = 1.0d; } } @Override public int applyAsInt(Random random) { // Call random once to decide which column will be used. int column = random.nextInt(probabilities.length); // Call random a second time to decide which will be used: the column or the alias. if (random.nextDouble() < probabilities[column]) { return column; } else { return alias[column]; } } } }


2
投票
public class RandomCollection<E> { private final NavigableMap<Double, E> map = new TreeMap<Double, E>(); private double total = 0; public void add(double weight, E result) { if (weight <= 0 || map.containsValue(result)) return; total += weight; map.put(total, result); } public E next() { double value = ThreadLocalRandom.current().nextDouble() * total; return map.ceilingEntry(value).getValue(); } }

1
投票
如果选择后需要删除元素,则可以使用其他解决方案。将所有元素添加到“ LinkedList”中,每个元素必须按其权重添加多次,然后使用Collections.shuffle(),根据JavaDoc

使用默认的随机源随机排列指定的列表。所有排列发生的可能性几乎相等。

最后,使用pop()removeFirst()获取和删除元素>

Map<String, Integer> map = new HashMap<String, Integer>() {{ put("Five", 5); put("Four", 4); put("Three", 3); put("Two", 2); put("One", 1); }}; LinkedList<String> list = new LinkedList<>(); for (Map.Entry<String, Integer> entry : map.entrySet()) { for (int i = 0; i < entry.getValue(); i++) { list.add(entry.getKey()); } } Collections.shuffle(list); int size = list.size(); for (int i = 0; i < size; i++) { System.out.println(list.pop()); }


1
投票
139
© www.soinside.com 2019 - 2024. All rights reserved.