【问题标题】:Random weighted selection in JavaJava中的随机加权选择
【发布时间】:2023-03-31 10:44:01
【问题描述】:

我想从一组中随机选择一个项目,但选择任何项目的机会应该与相关的重量成正比

示例输入:

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

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

在这种情况下,用户获得苦难之剑的可能性应该是三刃剑的 10 倍。

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

【问题讨论】:

标签: java random double


【解决方案1】:

我会使用 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());
} 

【讨论】:

  • @cpu_meltdown log(n) 的开销 ;)
  • 感谢彼得的回答!它运作良好。如果有人——像我一样——想知道if (weight &lt;= 0 return this;,它有一个重要的用途。如果没有它,如果您重用该示例并在调用.add(25, "horse") 之后调用.add(0, "lizard") 并调用.add(25, "horse"),由于对put(total, result) 的新调用,这将覆盖地图中“马”的条目与上一个条目相同的总重量,因此将“马”替换为“蜥蜴”,即使它应该有 0% 的机会被选中。
  • @PeterLawrey 谢谢,如果我们想在随机选择之外支持随机删除,你会如何更新?
  • 是否需要加到100%?
【解决方案2】:

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

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

其中itemWeightsList&lt;Pair&lt;Item, Double&gt;&gt;,例如(假设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

【讨论】:

  • 这确实应该在答案列表中更高...为什么要重新发明轮子?此外,EnumeratedDistribution 允许一次选择多个样本,非常简洁。
  • Commons Math3 现在不受支持。 EnumeratedDistribution 的功能已移至 Commons RNG 库中的 DiscreteProbabilityCollectionSampler
【解决方案3】:

您将找不到解决此类问题的框架,因为所请求的功能只不过是一个简单的功能。做这样的事情:

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.");
    }
}

【讨论】:

  • 项目列表应该使用哪个顺序?从高到小?谢谢。
  • 您不需要对列表进行排序。顺序无关紧要。
  • 列表中项目的顺序无关紧要,因为r的值是一个均匀分布的随机数,也就是说r是某个值的概率等于所有其他值r 可能是。因此,列表中的项目不是“受欢迎的”,它们在列表中的位置无关紧要。
  • 如果使用countWeight &gt;= r,如果恰好是第一个项目并且r = 0,则可以选择权重为零的项目。
【解决方案4】:

使用别名方法

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

下面的代码确实是这样一个别名方法的相当长的实现。但这是因为初始化部分。元素的检索非常快(参见next 和它们不循环的applyAsInt 方法)。

用法

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];
      }
    }
  }
}

【讨论】:

    【解决方案5】:

    139

    有一种简单的随机挑选物品的算法,其中物品具有单独的权重:

    1. 计算所有权重的总和

    2. 选择一个大于等于 0 且小于权重总和的随机数

    3. 一次检查一件物品,从你的随机数中减去它们的重量,直到你得到随机数小于该物品重量的物品

    【讨论】:

      【解决方案6】:
      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();
        }
      }
      

      【讨论】:

        【解决方案7】:

        如果您在选择后需要删除元素,您可以使用其他解决方案。将所有元素添加到'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());
        }
        

        【讨论】:

        • 问题是关于加权随机的。这不考虑这一点。
        • @GuillaumePerrot 实际上,这段代码做了加权选择
        • 我现在是如何得到它的,但是如果您的权重具有较大的值,例如 100000 您需要将 100000 个元素添加到列表中,这将非常低效。
        • @GuillaumePerrot 是的,你是对的。尽管如此,这种算法还是可以在某些情况下使用。
        猜你喜欢
        • 1970-01-01
        • 2015-07-05
        • 2010-09-08
        • 2020-01-22
        • 2017-12-26
        • 1970-01-01
        • 2010-09-08
        相关资源
        最近更新 更多