it-swarm.com.de

Zufällige gewichtete Auswahl in Java

Ich möchte ein zufälliges Element aus einem Satz auswählen, aber die Möglichkeit, ein Element auszuwählen, sollte proportional zum zugehörigen Gewicht sein

Beispieleingaben:

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

Wenn ich also 4 mögliche Gegenstände habe, wäre die Chance, einen Gegenstand ohne Gewichte zu erhalten, 1: 4.

In diesem Fall sollte ein Benutzer zehn Mal häufiger das Elendschwert bekommen als das dreischneidige Schwert.

Wie mache ich eine gewichtete Zufallsauswahl in Java?

54
yosi

Ich würde eine NavigableMap verwenden

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

Angenommen, ich habe eine Liste von Tieren, Hunden, Katzen, Pferden mit Wahrscheinlichkeiten von 40%, 35% bzw. 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());
} 
92
Peter Lawrey

Sie finden kein Framework für diese Art von Problem, da die angeforderte Funktionalität nichts anderes als eine einfache Funktion ist. Tun Sie so etwas:

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

In Apache Commons gibt es jetzt eine Klasse: EnumeratedDistribution

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

wobei itemWeights ein List<Pair<Item,Double>> ist, wie (unter der Annahme einer Artikelschnittstelle in Arnes Antwort):

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

oder in Java 8:

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

Hinweis: Pair muss hier org.Apache.commons.math3.util.Pair sein, nicht org.Apache.commons.lang3.Tuple.Pair.

15
kdkeck

Verwenden Sie eine Aliasmethode

Wenn Sie (wie in einem Spiel) häufig rollen, sollten Sie eine Aliasmethode verwenden.

Der folgende Code ist eine recht lange Implementierung einer solchen Aliasmethode. Dies ist jedoch auf den Initialisierungsteil zurückzuführen. Das Abrufen von Elementen ist sehr schnell (siehe die Methoden next und applyAsInt, für die sie keine Schleife ausführen).

Verwendungszweck

Set<Item> items = ... ;
ToDoubleFunction<Item> weighter = ... ;

Random random = new Random();

RandomSelector<T> selector = RandomSelector.weighted(items, weighter);
Item drop = selector.next(random);

Implementierung

Diese Implementierung:

  • verwendet Java 8;
  • soll so schnell wie möglichsein (zumindest habe ich versucht, dies mit Micro-Benchmarking zu tun);
  • ist total threadsicher (behält eine Random in jedem Thread für maximale Leistung bei, verwenden Sie ThreadLocalRandom?);
  • holt Elemente in O(1), im Gegensatz zu dem, was Sie hauptsächlich im Internet oder in StackOverflow finden, wo naive Implementierungen in O(n) oder O (log (n) ausgeführt werden. );
  • hält die Elemente unabhängig von ihrem Gewicht, so dass einem Artikel verschiedene Gewichtungen in verschiedenen Kontexten zugewiesen werden können.

Wie auch immer, hier ist der Code. (Beachten Sie, dass ich eine aktuelle Version dieser Klasse halte .)

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

Wenn Sie nach der Auswahl Elemente entfernen müssen, können Sie eine andere Lösung verwenden. Fügen Sie alle Elemente in eine 'LinkedList' ein. Jedes Element muss so oft hinzugefügt werden, wie es gewichtet wird. Verwenden Sie dann Collections.shuffle(), die laut JavaDoc

Die angegebene Liste wird nach dem Zufallsprinzip mit einer Standardquelle für Zufallszahlen zufällig verteilt. Alle Permutationen treten mit ungefähr gleicher Wahrscheinlichkeit auf.

Abrufen und Entfernen von Elementen mit pop() oder 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
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
ronen

139

Es gibt einen einfachen Algorithmus für die zufällige Auswahl eines Artikels, bei dem Artikel individuelle Gewichte haben:

  1. berechnen Sie die Summe aller Gewichte

  2. wählen Sie eine Zufallszahl, die 0 oder größer ist und kleiner als die Summe der Gewichte ist

  3. gehen Sie die Artikel einzeln durch und subtrahieren Sie deren Gewicht von Ihrer Zufallszahl, bis Sie den Artikel erhalten, bei dem die Zufallszahl unter dem Gewicht des Artikels liegt

0
Quinton Gordon