Sélection pondérée aléatoire en Java

je veux choisir un article aléatoire à partir d'un ensemble, mais la chance de choisir un article devrait être proportionnelle au poids associé

exemples d'entrées:

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

donc, si j'ai 4 items possibles, la chance d'obtenir un item sans poids serait de 1 sur 4.

dans ce cas, un utilisateur devrait être 10 fois plus susceptible d'obtenir l'épée de la misère que l'épée à triple tranchant.

Comment puis-je faire une sélection aléatoire pondérée en Java?

53
demandé sur Peter Lawrey 2011-06-20 14:06:53

6 réponses

j'utiliserais une carte de navigation

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

dire que j'ai une liste d'Animaux Chien, Chat, Cheval avec des probabilités de 40%, 35%, 25% respectivement

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());
} 
85
répondu Peter Lawrey 2017-06-18 19:12:13

vous ne trouverez pas de cadre pour ce genre de problème, car la fonctionnalité demandée n'est rien de plus qu'une simple fonction. Faites quelque chose comme ceci:

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
répondu Arne Deutsch 2014-09-25 21:19:26

il y a maintenant une classe pour cela dans Apache Commons: Énumérateddistribution

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

itemWeights est un List<Pair<Item,Double>> , comme (en supposant l'interface D'article dans la réponse D'Arne):

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

ou en Java 8:

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

Note: Pair ici doit être org.apache.commons.math3.util.Pair , pas org.apache.commons.lang3.tuple.Pair .

13
répondu kdkeck 2017-05-17 20:50:38

utiliser une méthode alias

si vous allez rouler beaucoup de fois (comme dans un jeu), vous devriez utiliser une méthode d'alias.

le code ci-dessous est une implémentation assez longue d'une telle méthode d'alias, en effet. Mais c'est à cause de la partie initialisation. La récupération des éléments est très rapide (Voir les méthodes next et applyAsInt qu'ils ne bouclent pas).

Utilisation

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

Random random = new Random();

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

La mise en œuvre

Cette mise en œuvre:

  • uses Java 8 ;
  • est conçu pour être aussi vite que possible (bien, au moins, j'ai essayé de le faire en utilisant micro-benchmarking);
  • est totalement thread-safe (garder un Random dans chaque thread pour une performance maximale, utiliser ThreadLocalRandom ?);
  • récupère des éléments dans O(1) , contrairement à ce que l'on trouve principalement sur internet ou sur StackOverflow, où des implémentations naïves tournent dans O(n) ou O(log (n));
  • maintient les articles indépendants de leur poids , de sorte qu'un élément peut être attribué des poids divers dans différents contextes.

bref, voilà le code. (À noter que je tiens à jour une version de cette classe .)

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
répondu Olivier Grégoire 2015-08-01 14:43:32
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
répondu ronen 2016-11-24 22:58:35

si vous devez supprimer des éléments après avoir choisi, vous pouvez utiliser une autre solution. Ajouter tous les éléments dans une 'LinkedList', chaque élément doit être ajouté autant de fois qu'il est de poids, puis utiliser Collections.shuffle() qui, selon JavaDoc

Permute au hasard la liste spécifiée en utilisant une source par défaut de randomness. Toutes les permutations se produisent avec à peu près la même probabilité.

enfin, obtenir et supprimer des éléments en utilisant pop() ou 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
répondu Yuri Heiko 2017-09-13 04:12:50