web-dev-qa-db-ja.com

Javaのランダムな重み付き選択

セットからランダムなアイテムを選択したいのですが、アイテムを選択する可能性は関連する重量に比例する必要があります

入力例:

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

したがって、4つの可能なアイテムがある場合、重みなしで1つのアイテムを取得する可能性は4分の1です。

この場合、ユーザーは、トリプルエッジソードよりも悲惨な剣を取得する可能性が10倍高いはずです。

Javaで重み付けランダム選択を行うにはどうすればよいですか?

58
yosi

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());
} 
100
Peter Lawrey

要求された機能は単なる機能に過ぎないため、この種の問題のフレームワークは見つかりません。このようなことをしてください:

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

Apache Commonsには、このためのクラスがあります。 EnumeratedDistribution

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

ここで、itemWeightsList<Pair<Item,Double>>で、次のようになります(Arneの回答でItemインターフェースを想定):

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である必要があります。

17
kdkeck

エイリアスメソッドを使用する

ゲームのように何度もロールする場合は、エイリアスメソッドを使用する必要があります。

以下のコードは、このようなエイリアスメソッドのかなり長い実装です。しかし、これは初期化部分のためです。要素の取得は非常に高速です(ループしない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;
  • できるだけ速くなるように設計されています(まあ、少なくとも、マイクロベンチマークを使用してそうしようとしました);
  • 完全にthread-safe(最大のパフォーマンスのために各スレッドに1つのRandomを保持し、ThreadLocalRandom?を使用);
  • O(1)の要素を取得します。これは、インターネットやStackOverflowでほとんど見られるものとは異なり、素朴な実装はO(n)またはO(log( n));
  • アイテムをその重量とは無関係に保持します。したがって、アイテムは異なるコンテキストでさまざまな重みを割り当てることができます。

とにかく、ここにコードがあります。 (注意してください このクラスの最新バージョンを維持します 。)

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
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();
  }
}
2
ronen

選択後に要素を削除する必要がある場合は、別のソリューションを使用できます。すべての要素を「LinkedList」に追加します。各要素は重みの数だけ追加する必要があります。その後、 JavaDoc に従ってCollections.shuffle()を使用します

ランダム性のデフォルトのソースを使用して、指定されたリストをランダムに並べ替えます。すべての順列は、ほぼ等しい尤度で発生します。

最後に、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
Yuri Heiko

139

アイテムをランダムに選択するための簡単なアルゴリズムがあります。アイテムには個別の重みがあります。

  1. すべての重みの合計を計算する

  2. 0以上で、重みの合計より小さい乱数を選択します

  3. アイテムを一度に1つずつ調べて、乱数がそのアイテムの重量よりも小さいアイテムが得られるまで、乱数からその重量を引きます

1
Quinton Gordon