web-dev-qa-db-fra.com

obtenir un élément aléatoire pondéré

J'ai, par exemple, ce tableau

 + ----------------- + 
 | fruit | poids | 
 + ----------------- + 
 | Apple | 4 | 
 | Orange | 2 | 
 | Citron | 1 | 
 + ----------- ------ + 

Je dois retourner un fruit au hasard. Mais Apple devrait être choisi 4 fois plus souvent que Citron et 2 fois plus fréquent que orange.

Dans un cas plus général, il devrait être f(weight) fois fréquemment.

Quel est un bon algorithme général pour implémenter ce comportement?

Ou peut-être qu'il y a des gemmes prêtes sur Ruby? :)

[~ # ~] ps [~ # ~]
J'ai implémenté l'algorithme actuel dans Ruby https://github.com/fl00r/pickup

50
fl00r

La solution la plus simple sur le plan conceptuel serait de créer une liste où chaque élément apparaît autant de fois que son poids, donc

fruits = [Apple, Apple, Apple, Apple, orange, orange, lemon]

Utilisez ensuite les fonctions dont vous disposez pour sélectionner un élément aléatoire dans cette liste (par exemple, générer un index aléatoire dans la plage appropriée). Ceci n'est bien sûr pas très efficace en mémoire et nécessite des poids entiers.


Une autre approche, légèrement plus compliquée, ressemblerait à ceci:

  1. Calculez les sommes cumulées des poids:

    intervals = [4, 6, 7]
    

    Où un indice inférieur à 4 représente un Apple, 4 à moins de 6 an orange et 6 à moins de 7 a citron.

  2. Générez un nombre aléatoire n dans la plage de 0 À sum(weights).

  3. Recherchez le dernier élément dont la somme cumulée est supérieure à n. Le fruit correspondant est votre résultat.

Cette approche nécessite un code plus compliqué que le premier, mais moins de mémoire et de calcul et prend en charge les poids à virgule flottante.

Pour l'un ou l'autre algorithme, l'étape de configuration peut être effectuée une fois pour un nombre arbitraire de sélections aléatoires.

49
Benjamin Kloster

Voici un algorithme (en C #) qui peut sélectionner un élément pondéré au hasard dans n'importe quelle séquence, en ne l'itérant qu'une seule fois:

public static T Random<T>(this IEnumerable<T> enumerable, Func<T, int> weightFunc)
{
    int totalWeight = 0; // this stores sum of weights of all elements before current
    T selected = default(T); // currently selected element
    foreach (var data in enumerable)
    {
        int weight = weightFunc(data); // weight of current element
        int r = Random.Next(totalWeight + weight); // random value
        if (r >= totalWeight) // probability of this is weight/(totalWeight+weight)
            selected = data; // it is the probability of discarding last selected element and selecting current one instead
        totalWeight += weight; // increase weight sum
    }

    return selected; // when iterations end, selected is some element of sequence. 
}

Ceci est basé sur le raisonnement suivant: sélectionnons le premier élément de notre séquence comme "résultat courant"; puis, à chaque itération, conservez-le ou jetez-le et choisissez le nouvel élément comme courant. Nous pouvons calculer la probabilité qu'un élément donné soit sélectionné à la fin comme un produit de toutes les probabilités qu'il ne serait pas être rejeté dans les étapes suivantes, multiplié par la probabilité qu'il soit sélectionné en premier lieu . Si vous faites le calcul, vous verriez que ce produit se simplifie en (poids de l'élément)/(somme de tous les poids), ce qui est exactement ce dont nous avons besoin!

Étant donné que cette méthode n'itère qu'une seule fois la séquence d'entrée, elle fonctionne même avec des séquences d'une taille obscène, à condition que la somme des poids tienne dans un int (ou vous pouvez choisir un type plus grand pour ce compteur)

30
Nevermind

Les réponses déjà présentes sont bonnes et je vais les développer un peu.

Comme Benjamin l'a suggéré, les sommes cumulées sont généralement utilisées dans ce type de problème:

+------------------------+
| fruit  | weight | csum |
+------------------------+
| Apple  |   4    |   4  |
| orange |   2    |   6  |
| lemon  |   1    |   7  |
+------------------------+

Pour trouver un élément dans cette structure, vous pouvez utiliser quelque chose comme le morceau de code de Nevermind. Ce morceau de code C # que j'utilise habituellement:

double r = Random.Next() * totalSum;
for(int i = 0; i < fruit.Count; i++)
{
    if (csum[i] > r)
        return fruit[i];
}

Passons maintenant à la partie intéressante. Quelle est l'efficacité de cette approche et quelle est la solution la plus efficace? Mon morceau de code nécessite O (n) mémoire et s'exécute dans O (n) temps. Je ne pense pas que cela puisse être fait avec moins de O (n) espace mais la complexité temporelle peut être beaucoup plus faible, O (log n) en fait. L'astuce consiste à utiliser la recherche binaire au lieu de la boucle for régulière.

double r = Random.Next() * totalSum;
int lowGuess = 0;
int highGuess = fruit.Count - 1;

while (highGuess >= lowGuess)
{
    int guess = (lowGuess + highGuess) / 2;
    if ( csum[guess] < r)
        lowGuess = guess + 1;
    else if ( csum[guess] - weight[guess] > r)
        highGuess = guess - 1;
    else
        return fruit[guess];
}

Il y a aussi une histoire sur la mise à jour des poids. Dans le pire des cas, la mise à jour du poids pour un élément entraîne la mise à jour des sommes cumulées pour tous les éléments, augmentant la complexité de la mise à jour à O (n) . Cela aussi peut être réduit à O (log n) en utilisant arbre indexé binaire .

21
Emperor Orionii

Ceci est une simple implémentation Python:

from random import random

def select(container, weights):
    total_weight = float(sum(weights))
    rel_weight = [w / total_weight for w in weights]

    # Probability for each element
    probs = [sum(rel_weight[:i + 1]) for i in range(len(rel_weight))]

    slot = random()
    for (i, element) in enumerate(container):
        if slot <= probs[i]:
            break

    return element

et

population = ['Apple','orange','lemon']
weights = [4, 2, 1]

print select(population, weights)

Dans les algorithmes génétiques, cette procédure de sélection est appelée sélection proportionnelle de remise en forme ou sélection de la roulette puisque:

  • une proportion de la roue est affectée à chacune des sélections possibles en fonction de leur valeur de poids. Ceci peut être réalisé en divisant le poids d'une sélection par le poids total de toutes les sélections, les normalisant ainsi à 1.
  • puis une sélection aléatoire est effectuée similaire à la façon dont la roue de roulette est tournée.

Roulette wheel selection

Les algorithmes typiques ont une complexité O(N) ou O (log N) mais vous pouvez également faire O(1) (par exemple roulette-roulette) sélection via acceptation stochastique ).

8
manlio

This Gist fait exactement ce que vous demandez.

public static Random random = new Random(DateTime.Now.Millisecond);
public int chooseWithChance(params int[] args)
    {
        /*
         * This method takes number of chances and randomly chooses
         * one of them considering their chance to be choosen.    
         * e.g. 
         *   chooseWithChance(0,99) will most probably (%99) return 1
         *   chooseWithChance(99,1) will most probably (%99) return 0
         *   chooseWithChance(0,100) will always return 1.
         *   chooseWithChance(100,0) will always return 0.
         *   chooseWithChance(67,0) will always return 0.
         */
        int argCount = args.Length;
        int sumOfChances = 0;

        for (int i = 0; i < argCount; i++) {
            sumOfChances += args[i];
        }

        double randomDouble = random.NextDouble() * sumOfChances;

        while (sumOfChances > randomDouble)
        {
            sumOfChances -= args[argCount -1];
            argCount--;
        }

        return argCount-1;
    }

vous pouvez l'utiliser comme ça:

string[] fruits = new string[] { "Apple", "orange", "lemon" };
int choosenOne = chooseWithChance(98,1,1);
Console.WriteLine(fruits[choosenOne]);

Le code ci-dessus renverra très probablement (% 98) 0 qui est l'index de 'Apple' pour le tableau donné.

En outre, ce code teste la méthode fournie ci-dessus:

Console.WriteLine("Start...");
int flipCount = 100;
int headCount = 0;
int tailsCount = 0;

for (int i=0; i< flipCount; i++) {
    if (chooseWithChance(50,50) == 0)
        headCount++;
    else
        tailsCount++;
}

Console.WriteLine("Head count:"+ headCount);
Console.WriteLine("Tails count:"+ tailsCount);

Cela donne une sortie quelque chose comme ça:

Start...
Head count:52
Tails count:48
0
Ramazan Polat