ottenere un oggetto casuale ponderato


51

Ho, ad esempio, questa tabella

+ ----------------- +
| frutta | peso |
+ ----------------- +
| mela | 4 |
| arancio | 2 |
| limone | 1 |
+ ----------------- +

Devo restituire un frutto casuale. Ma la mela dovrebbe essere raccolta 4 volte più frequentemente del limone e 2 volte più frequente dell'arancia .

In casi più generali dovrebbe essere f(weight)volte di frequente.

Qual è un buon algoritmo generale per implementare questo comportamento?

O forse ci sono alcune gemme pronte su Ruby? :)

PS
Ho implementato l'attuale algoritmo in Ruby https://github.com/fl00r/pickup


11
quella dovrebbe essere la stessa formula per ottenere un bottino casuale in Diablo :-)
Jalayn,

1
@Jalayn: In realtà, l'idea per la soluzione degli intervalli nella mia risposta di seguito proviene da ciò che ricordo dei tavoli da combattimento in World of Warcraft. MrGreen
Benjamin Kloster



Ho implementato diversi algoritmi casuali ponderati semplici . Fammi sapere se hai domande.
Leonid Ganeline,

Risposte:


50

La soluzione concettualmente più semplice sarebbe quella di creare un elenco in cui ogni elemento si presenta tante volte quanto il suo peso, quindi

fruits = [apple, apple, apple, apple, orange, orange, lemon]

Quindi utilizza tutte le funzioni che hai a tua disposizione per scegliere un elemento casuale da quell'elenco (ad esempio, genera un indice casuale all'interno dell'intervallo corretto). Questo ovviamente non è molto efficiente in termini di memoria e richiede pesi interi.


Un altro approccio leggermente più complicato sarebbe simile al seguente:

  1. Calcola le somme cumulative dei pesi:

    intervals = [4, 6, 7]

    Dove un indice inferiore a 4 rappresenta una mela , da 4 a sotto 6 un'arancia e da 6 a sotto 7 un limone .

  2. Genera un numero casuale nnell'intervallo di 0a sum(weights).

  3. Trova l'ultimo elemento la cui somma cumulativa è sopra n. Il frutto corrispondente è il tuo risultato.

Questo approccio richiede un codice più complicato del primo, ma meno memoria e calcolo e supporta pesi in virgola mobile.

Per entrambi gli algoritmi, la fase di installazione può essere eseguita una volta per un numero arbitrario di selezioni casuali.


2
la soluzione dell'intervallo sembra buona
Jalayn,

1
Questo è stato il mio primo pensiero :). E se avessi un tavolo con 100 frutti e il peso potesse essere di circa 10k? Sarà un array molto grande e questo non sarà efficiente come voglio. Si tratta della prima soluzione. La seconda soluzione sembra buona
fl00r

1
Ho implementato questo algoritmo in Ruby github.com/fl00r/pickup
fl00r

1
Il metodo alias è il modo defacto di gestirlo Sono sinceramente stupito dal numero di post che ripetono più volte lo stesso codice, il tutto ignorando il metodo alias . per l'amor del cielo ottieni prestazioni costanti nel tempo!
opa,

30

Ecco un algoritmo (in C #) che può selezionare elementi ponderati casuali da qualsiasi sequenza, iterandoli solo una volta:

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. 
}

Questo si basa sul seguente ragionamento: selezioniamo il primo elemento della nostra sequenza come "risultato corrente"; quindi, ad ogni iterazione, mantienila o scarta e scegli il nuovo elemento come corrente. Possiamo calcolare la probabilità che un dato elemento venga selezionato alla fine come prodotto di tutte le probabilità che non venga scartato nei passaggi successivi, più volte la probabilità che venga selezionato in primo luogo. Se fai la matematica, vedresti che questo prodotto semplifica in (peso dell'elemento) / (somma di tutti i pesi), che è esattamente ciò di cui abbiamo bisogno!

Poiché questo metodo scorre una sola volta sulla sequenza di input, funziona anche con sequenze oscenamente grandi, a condizione che la somma dei pesi si adatti a un int(o puoi scegliere un tipo più grande per questo contatore)


2
Vorrei fare un benchmark di questo prima di dare per scontato che sia meglio solo perché scorre una volta. Generare altrettanti valori casuali non è neanche esattamente veloce.
Jean-Bernard Pellerin,

1
@ Jean-Bernard Pellerin l'ho fatto ed è effettivamente più veloce su grandi elenchi. A meno che tu non usi un generatore casuale crittograficamente forte (-8
Nevermind

Dovrebbe essere la risposta accettata imo. Mi piace meglio dell'approccio "intervallo" e "ingresso ripetuto".
Vivin Paliath,

2
Volevo solo dire che sono tornato a questo thread 3 o 4 volte negli ultimi due anni per usare questo metodo. Questo metodo è riuscito ripetutamente a fornire le risposte di cui ho bisogno abbastanza rapidamente per i miei scopi. Vorrei poter votare questa risposta ogni volta che torno per usarla.
Jim Yarbro,

1
Bella soluzione se davvero devi scegliere solo una volta. Altrimenti, fare i preparativi per la soluzione nella prima risposta una volta è molto più efficiente.
Deduplicatore,

22

Le risposte già presenti sono buone e mi espanderò un po 'su di esse.

Come Benjamin ha suggerito, in questo tipo di problema vengono generalmente utilizzate somme cumulative:

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

Per trovare un oggetto in questa struttura, puoi usare qualcosa come il pezzo di codice di Nevermind. Questo pezzo di codice C # che di solito uso:

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

Ora alla parte interessante. Quanto è efficace questo approccio e qual è la soluzione più efficiente? Il mio pezzo di codice richiede memoria O (n) ed eseguito in tempo O (n) . Non penso che si possa fare con meno di O (n) spazio, ma la complessità del tempo può essere molto più bassa, O (log n) in effetti. Il trucco è usare la ricerca binaria invece del normale ciclo.

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

C'è anche una storia sull'aggiornamento dei pesi. Nel peggiore dei casi l'aggiornamento del peso per un elemento provoca l'aggiornamento delle somme cumulative per tutti gli elementi aumentando la complessità dell'aggiornamento a O (n) . Anche quello può essere ridotto a O (log n) usando l' albero indicizzato binario .


Buon punto sulla ricerca binaria
fl00r

La risposta di Nevermind non ha bisogno di spazio extra, quindi è O (1), ma aggiunge complessità di runtime generando ripetutamente numeri casuali e valutando la funzione di peso (che, a seconda del problema sottostante, potrebbe essere costoso).
Benjamin Kloster,

1
In realtà, ciò che affermi di essere "versione più leggibile" del mio codice non lo è. Il codice deve conoscere in anticipo la somma totale dei pesi e le somme cumulative; il mio no.
Nevermind

@Benjamin Kloster Il mio codice chiama la funzione peso solo una volta per elemento: non puoi fare di meglio. Hai ragione sui numeri casuali, però.
Nevermind

@Nevermind: la chiami una sola volta per chiamata alla funzione pick, quindi se l'utente la chiama due volte, la funzione peso viene richiamata di nuovo per ogni elemento. Ovviamente potresti memorizzarlo nella cache, ma non sarai più O (1) per la complessità dello spazio.
Benjamin Kloster,

8

Questa è una semplice implementazione di 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

e

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

print select(population, weights)

Negli algoritmi genetici questa procedura di selezione si chiama Selezione proporzionale del fitness o Selezione della ruota della roulette poiché:

  • una parte della ruota è assegnata a ciascuna delle possibili selezioni in base al loro valore di peso. Ciò può essere ottenuto dividendo il peso di una selezione per il peso totale di tutte le selezioni, quindi normalizzandole a 1.
  • quindi viene effettuata una selezione casuale simile a come viene ruotata la ruota della roulette.

Selezione della ruota della roulette

Gli algoritmi tipici hanno una complessità O (N) o O (log N) ma puoi anche fare O (1) (ad es. Selezione della ruota della roulette tramite accettazione stocastica ).


Sai qual è la fonte originale di questa immagine? Voglio usarlo per un documento ma devo assicurarmi dell'attribuzione.
Malcolm MacLeod,

@MalcolmMacLeod Siamo spiacenti, è utilizzato in molti documenti / siti GA ma non so chi sia l'autore.
manlio,

0

Questa sostanza sta facendo esattamente quello che stai chiedendo.

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

puoi usarlo così:

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

Molto probabilmente il codice sopra riportato (% 98) restituirà 0 che è indice per 'apple' per l'array specificato.

Inoltre, questo codice verifica il metodo fornito sopra:

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

Dà un output qualcosa del genere:

Start...
Head count:52
Tails count:48

2
I programmatori riguardano domande e risposte concettuali che dovrebbero spiegare le cose. Lanciare dump di codice anziché una spiegazione è come copiare il codice dall'IDE alla lavagna: può sembrare familiare e persino a volte comprensibile, ma sembra strano ... solo strano. La lavagna non ha compilatore
moscerino

Hai ragione, ero concentrato sul codice, quindi mi sono dimenticato di dire come funziona. Aggiungerò una spiegazione su come funziona.
Ramazan Polat,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.