【问题标题】:Weighted randomness in Java [duplicate]Java中的加权随机性[重复]
【发布时间】:2011-10-07 21:55:51
【问题描述】:

在 Java 中,给定 n 个项目,每个项目的权重 w,如何从集合中随机选择一个具有等于 w 机会的项目em>?

假设每个权重是从 0.0 到 1.0 的双精度数,并且集合中的权重总和为 1。Item.getWeight() 返回项目的权重。

【问题讨论】:

标签: java random


【解决方案1】:

2020 年更新(有趣的是,这在下面的 2011 版本中获得了 37 次投票,但有一个明显的错误):

  • 修复了当Math.random() 产生一个非常接近1.0 的数字时无法选择最后一项的问题,并且我们在浮点精度方面不走运:随机索引-1 将是结果,这显然是错误的。李>
  • 一些代码压缩
  • 使用较少的变量名
Item[] items = ...;

// Compute the total weight of all items together.
// This can be skipped of course if sum is already 1.
double totalWeight = 0.0;
for (Item i : items) {
    totalWeight += i.getWeight();
}

// Now choose a random item.
int idx = 0;
for (double r = Math.random() * totalWeight; idx < items.length - 1; ++idx) {
    r -= items[idx].getWeight();
    if (r <= 0.0) break;
}
Item myRandomItem = items[idx];

2011版(留作比较):

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

【讨论】:

  • 这个。仅供参考,如果需要,您可以进行二进制搜索。
  • 对于二进制搜索,部分和也必须存储。
  • 我认为由于使用了&lt;=,这有非常轻微的偏差:考虑两个重量为1 的项目,应该平等对待。如果Math.random() == 0.5,则random == 1,这将选择列表中的第一项。换句话说,来自[0.5,1] 的任何内容都将导致第一个项目被选中,而来自[0,0.5) 的任何内容将导致第二个项目,因此后者的范围非常小。解决方法是使用&lt; 0.0d 而不是&lt;=。 (请注意,我不是统计专家,所以这可能不是 100% 正确的!)
  • @kirbyfan64sos:我相信你错了:间隔完全相同。 [0,0.5)[0,0.5] 同等大,被选中的机会相同。在处理连续的数字时,一个数字确实不会产生影响。这是因为准确选择 0.5 的机会是 0,但并非不可能。
【解决方案2】:

一种优雅的方法是对指数分布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() 转换例如从地图中获取输入权重流。

【讨论】:

    【解决方案3】:

    使用包含 getWeight() 方法的 Item 类(如您的问题所示):

    /**
     * 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
    }
    

    【讨论】:

      【解决方案4】:

      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 个 Int 的数组,并根据各个字段的权重分配存储桶的数量。然后你创建一个介于 0 和 100,000-1 之间的随机整数,你会立即得到桶号。

      【讨论】:

      • 嗯,在节省几个 CPU 周期或节省几个程序员小时之间总是存在妥协。 :)
      • 不错的解决方案!我认为至少在 Java 和 int 权重的情况下需要做一个小的改变。 num 永远不会等于 count(即权重之和),num = 0 将抛出 NullPointerException。简单的解决方法是使用map.lowerKey(num + 1)
      • 由于 rand.nextDouble() 产生介于 0(包括)和 1(不包括)之间的数字,一个更好的主意可能是使用 num = (1-rand.nextDouble()) * count
      • @mmlac 我试图说明的重点是这一点。第一个对象以 0 的计数插入。当 Java 生成 0.0 作为随机数(可能发生)时,TreeMap 的 lowerKey 将返回 null,因为根据其定义,它返回严格小于给定键的最大键,或者如果没有这样的键,则为 null。因此,您要么确保随机数严格大于 0,要么使用不同的键插入第一个对象。另一个想法是,每当您返回的随机对象为空时,您就在树中查询另一个对象。
      • 应该是map.floorEntry(num);您需要实际项目而不是加权键。
      【解决方案5】:

      下面是一个保持比例精度的随机化器:

      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】:

        如果您希望提高运行时选择效率,那么在设置上多花一点时间可能是最好的选择。这是一种可能的解决方案。它有更多代码,但保证 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();
            }
        }
        

        【讨论】:

        • 权重是int,应该是double。
        【解决方案7】:
        1. 对项目...(i1, i2, ..., in)... 进行任意排序,权重为 w1, w2, ..., wn。
        2. 在 0 和 1 之间选择一个随机数(具有足够的粒度,通过使用任何随机化函数和适当的缩放比例)。将此称为 r0。
        3. 令 j = 1
        4. 从你的 r(j-1) 中减去 wj 得到 rj。如果 rj

        我想我以前也这样做过……但可能有更有效的方法来做到这一点。

        【讨论】:

        • wrji2wwjri,我可怜的眼睛!
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-03-31
        • 1970-01-01
        • 2011-06-05
        • 1970-01-01
        • 2015-04-23
        • 2017-08-10
        相关资源
        最近更新 更多