Come faccio a creare una raccolta ponderata e quindi scegliere un elemento casuale da essa?


34

Ho un bottino che voglio riempire con un oggetto casuale. Ma voglio che ogni oggetto abbia una diversa possibilità di essere scelto. Per esempio:

  • Probabilità del 5% di 10 monete d'oro
  • Probabilità di spada del 20%
  • 45% di possibilità di scudo
  • Probabilità del 20% di armatura
  • 10% di probabilità di pozione

Come posso farlo in modo da selezionare esattamente uno degli elementi sopra, in cui tali percentuali sono le rispettive possibilità di ottenere il bottino?


1
Cordiali saluti, in teoria, O (1) tempo per campione è possibile per qualsiasi distribuzione finita, anche una distribuzione le cui voci cambiano dinamicamente. Vedi ad esempio cstheory.stackexchange.com/questions/37648/… .
Neal Young,

Risposte:


37

La soluzione di probabilità con codifica soft

La soluzione di probabilità codificata presenta lo svantaggio di cui hai bisogno per impostare le probabilità nel tuo codice. Non è possibile determinarli in fase di esecuzione. È anche difficile da mantenere.

Ecco una versione dinamica dello stesso algoritmo.

  1. Crea una matrice di coppie di articoli reali e peso di ciascun articolo
  2. Quando si aggiunge un articolo, il peso dell'articolo deve essere il proprio peso più la somma dei pesi di tutti gli articoli già presenti nell'array. Quindi dovresti tenere traccia della somma separatamente. Soprattutto perché ne avrai bisogno per il prossimo passo.
  3. Per recuperare un oggetto, genera un numero casuale compreso tra 0 e la somma dei pesi di tutti gli articoli
  4. iterare l'array dall'inizio alla fine fino a quando non si trova una voce con un peso maggiore o uguale al numero casuale

Ecco un'implementazione di esempio in Java sotto forma di una classe modello che puoi creare un'istanza per qualsiasi oggetto utilizzato dal tuo gioco. È quindi possibile aggiungere oggetti con il metodo .addEntry(object, relativeWeight)e selezionare una delle voci aggiunte in precedenza.get()

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class WeightedRandomBag<T extends Object> {

    private class Entry {
        double accumulatedWeight;
        T object;
    }

    private List<Entry> entries = new ArrayList<>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void addEntry(T object, double weight) {
        accumulatedWeight += weight;
        Entry e = new Entry();
        e.object = object;
        e.accumulatedWeight = accumulatedWeight;
        entries.add(e);
    }

    public T getRandom() {
        double r = rand.nextDouble() * accumulatedWeight;

        for (Entry entry: entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.object;
            }
        }
        return null; //should only happen when there are no entries
    }
}

Uso:

WeightedRandomBag<String> itemDrops = new WeightedRandomBag<>();

// Setup - a real game would read this information from a configuration file or database
itemDrops.addEntry("10 Gold",  5.0);
itemDrops.addEntry("Sword",   20.0);
itemDrops.addEntry("Shield",  45.0);
itemDrops.addEntry("Armor",   20.0);
itemDrops.addEntry("Potion",  10.0);

// drawing random entries from it
for (int i = 0; i < 20; i++) {
    System.out.println(itemDrops.getRandom());
}

Ecco la stessa classe implementata in C # per il tuo progetto Unity, XNA o MonoGame:

using System;
using System.Collections.Generic;

class WeightedRandomBag<T>  {

    private struct Entry {
        public double accumulatedWeight;
        public T item;
    }

    private List<Entry> entries = new List<Entry>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void AddEntry(T item, double weight) {
        accumulatedWeight += weight;
        entries.Add(new Entry { item = item, accumulatedWeight = accumulatedWeight });
    }

    public T GetRandom() {
        double r = rand.NextDouble() * accumulatedWeight;

        foreach (Entry entry in entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.item;
            }
        }
        return default(T); //should only happen when there are no entries
    }
}

Ed eccone uno in JavaScript :

var WeightedRandomBag = function() {

    var entries = [];
    var accumulatedWeight = 0.0;

    this.addEntry = function(object, weight) {
        accumulatedWeight += weight;
        entries.push( { object: object, accumulatedWeight: accumulatedWeight });
    }

    this.getRandom = function() {
        var r = Math.random() * accumulatedWeight;
        return entries.find(function(entry) {
            return entry.accumulatedWeight >= r;
        }).object;
    }   
}

Pro:

  • Può gestire qualsiasi rapporto di peso. Se lo desideri, puoi avere oggetti con probabilità astronomicamente piccole nel set. Inoltre, non è necessario aggiungere fino a 100 pesi.
  • Puoi leggere gli articoli e i pesi in fase di esecuzione
  • Utilizzo della memoria proporzionale al numero di elementi nell'array

Contra:

  • Richiede un po 'più di programmazione per avere ragione
  • Nel peggiore dei casi, potrebbe essere necessario iterare l'intero array ( O(n)complessità di runtime). Quindi, quando hai un set molto grande di oggetti e disegni molto spesso, potrebbe rallentare. Una semplice ottimizzazione consiste nel mettere al primo posto gli elementi più probabili, in modo che l'algoritmo si concluda presto nella maggior parte dei casi. Un'ottimizzazione più complessa che puoi fare è sfruttare il fatto che l'array sia ordinato e fare una ricerca bisection. Questo richiede solo O(log n)tempo.
  • È necessario creare l'elenco in memoria prima di poterlo utilizzare (sebbene sia possibile aggiungere facilmente elementi in fase di esecuzione. È possibile aggiungere anche la rimozione di elementi, ma ciò richiederebbe l'aggiornamento dei pesi accumulati di tutti gli elementi che seguono la voce rimossa, che ha di nuovo il O(n)peggior tempo di esecuzione)

2
Il codice C # può essere scritto usando LINQ: return entry.FirstOrDefault (e => e.accumulatedWeight> = r). Ancora più importante, c'è una leggera possibilità che a causa della perdita di precisione in virgola mobile questo algoritmo restituirà null se il valore casuale diventa solo un po 'più grande del valore accumulato. Come precauzione, potresti aggiungere un piccolo valore (diciamo 1.0) all'ultimo elemento, ma poi dovresti dichiarare esplicitamente nel tuo codice che l'elenco è definitivo.
Imil

1
Una piccola variante su questo ho usato personalmente, se vuoi che i valori di peso in runtime non vengano cambiati nel valore peso più tutti i precedenti, puoi sottrarre il peso di ogni voce passata dal tuo valore casuale, fermandoti quando il valore casuale è inferiore al peso degli articoli correnti (o quando si sottrae il peso si ottiene il valore <0)
Lunin

2
@ BlueRaja-DannyPflughoeft ottimizzazione prematura ... la domanda riguardava la selezione di un oggetto da un bottino aperto. Chi aprirà 1000 scatole al secondo?
Imil

4
@IMil: No, la domanda è generale per la selezione di articoli ponderati casuali . Per i lootbox in particolare, questa risposta probabilmente va bene perché ci sono un piccolo numero di elementi e le probabilità non cambiano (sebbene, dato che di solito sono fatte su un server, 1000 / sec non è irrealistico per un gioco popolare) .
BlueRaja - Danny Pflughoeft,

4
@opa quindi contrassegna per chiudere come duplicato. È davvero sbagliato dare una buona risposta solo perché la domanda è stata posta prima?
Baldrickk,

27

Nota: ho creato una libreria C # per questo problema esatto

Le altre soluzioni vanno bene se hai solo un numero limitato di articoli e le tue probabilità non cambiano mai. Tuttavia, con molti oggetti o cambiando le probabilità (es. Rimuovendo oggetti dopo averli selezionati) , vorrai qualcosa di più potente.

Ecco le due soluzioni più comuni (entrambe incluse nella libreria sopra)

Walker Alias ​​Method

Una soluzione intelligente che è estremamente veloce ( O(1)!) Se le tue probabilità sono costanti. In sostanza, l'algoritmo crea un bersaglio 2D ("alias table") tra le tue probabilità e gli lancia un dardo.

Bersaglio per freccette

Ci sono molti articoli online su come funziona se vuoi saperne di più.

L'unico problema è che se le probabilità cambiano, è necessario rigenerare la tabella alias, che è lenta. Pertanto, se è necessario rimuovere gli articoli dopo che sono stati raccolti, questa non è la soluzione per te.

Soluzione ad albero

L'altra soluzione comune è quella di creare un array in cui ogni articolo memorizza la somma della sua probabilità e tutti gli elementi prima di esso. Quindi genera semplicemente un numero casuale da [0,1) ed esegui una ricerca binaria per trovare quel numero nella lista.

Questa soluzione è molto facile da codificare / comprendere, ma effettuare una selezione è più lento del metodo alias di Walker e la modifica delle probabilità è ancora O(n). Possiamo migliorarlo trasformando l'array in un albero di ricerca binaria, in cui ciascun nodo tiene traccia della somma delle probabilità in tutti gli elementi nella sua sottostruttura. Quindi quando generiamo il numero da [0,1), possiamo semplicemente camminare lungo l'albero per trovare l'oggetto che rappresenta.

Questo ci dà la possibilità O(log n)di scegliere un oggetto e di cambiarne le probabilità! Questo rende NextWithRemoval()estremamente veloce!

I risultati

Ecco alcuni rapidi benchmark della libreria sopra, confrontando questi due approcci

         Benchmark di WeightedRandomizer | Albero | tavolo
-------------------------------------------------- ---------------------------------
Aggiungi () x10000 + NextWithReplacement () x10: | 4 ms | 2 ms
Aggiungi () x10000 + NextWithReplacement () x10000: | 7 ms | 4 ms
Aggiungi () x10000 + NextWithReplacement () x100000: | 35 ms | 28 ms
(Add () + NextWithReplacement ()) x10000 (interfogliato) | 8 ms | 5403 ms
Aggiungi () x10000 + NextWithRemoval () x10000: | 10 ms | 5948 ms

Come puoi vedere, per il caso speciale di probabilità statiche (non modificabili), il metodo Alias ​​di Walker è circa il 50-100% più veloce. Ma nei casi più dinamici, l'albero è più veloce di molti ordini di grandezza !


La soluzione ad albero ci fornisce anche un tempo di esecuzione decente ( nlog(n)) quando si ordinano gli articoli in base al peso.
Nathan Merrill,

2
Sono scettico sui tuoi risultati, ma questa è la risposta corretta. Non sono sicuro del perché questa non sia la risposta migliore, considerando che questo è in realtà il modo canonico per gestire questo problema.
quando il

Quale file contiene la soluzione ad albero? Secondo, la tua tabella di riferimento: l'alias di Walker è la colonna "tabella"?
Yakk,

1
@Yakk: il codice per la soluzione basata su alberi è qui . È basato su un'implementazione open source di un albero AA . E 'sì' alla tua seconda domanda.
BlueRaja - Danny Pflughoeft,

1
La parte Walker è praticamente solo link.
Accumulo

17

La soluzione della ruota della fortuna

Puoi usare questo metodo quando le probabilità nel tuo pool di articoli hanno un denominatore comune piuttosto grande e devi trarne molto spesso.

Crea una serie di opzioni. Ma inseriscici ogni elemento più volte, con il numero di duplicati di ciascun elemento proporzionale alla sua possibilità di apparire. Nell'esempio sopra, tutti gli elementi hanno probabilità che sono moltiplicatori del 5%, quindi puoi creare un array di 20 elementi come questo:

10 gold
sword
sword
sword
sword
shield
shield
shield
shield
shield
shield
shield
armor
armor
armor
armor
potion
potion

Quindi scegli semplicemente un elemento casuale di quell'elenco generando un numero intero casuale compreso tra 0 e la lunghezza dell'array - 1.

svantaggi:

  • È necessario creare l'array la prima volta che si desidera generare un elemento.
  • Quando si suppone che uno dei tuoi elementi abbia una probabilità molto bassa, si finisce con un array davvero grande, che può richiedere molta memoria.

vantaggi:

  • Quando hai già l'array e vuoi disegnarlo più volte, allora è molto veloce. Solo un numero intero casuale e un accesso all'array.

3
Come soluzione ibrida per evitare il secondo svantaggio, è possibile designare l'ultimo slot come "altro" e gestirlo con altri mezzi, come l'approccio array di Philipp. Quindi potresti riempire l'ultimo slot con un array che offre una probabilità del 99,9% di una pozione e solo uno 0,1% di una Epic Scepter of the Apocalypse. Tale approccio a due livelli sfrutta i vantaggi di entrambi gli approcci.
Cort Ammon - Ripristina Monica il

1
Uso in qualche modo una variante di questo nel mio progetto. Quello che faccio è calcolare ogni articolo e peso, e memorizzarli in un array, [('gold', 1),('sword',4),...]sommare tutti i pesi, quindi rotolare un numero casuale da 0 alla somma, quindi iterare l'array e calcolare dove atterra il numero casuale (es. a reduce). Funziona bene per le matrici che vengono aggiornate spesso e senza memoria principale.

1
@Thebluefish Quella soluzione è descritta nella mia altra risposta "The Soft-coded Probabilities Solution"
Philipp

7

La soluzione di probabilità codificata

Il modo più semplice per trovare un oggetto casuale da una raccolta ponderata è attraversare una catena di istruzioni if-else, in cui ogni if-else aumenta probabilmente, poiché la precedente non colpisce.

int rand = random(100); //Random number between 1 and 100 (inclusive)
if(rand <= 5) //5% chance
{
    print("You found 10 gold!");
}
else if(rand <= 25) //20% chance
{
    print("You found a sword!");
}
else if(rand <= 70) //45% chance
{
    print("You found a shield!");
}
else if(rand <= 90) //20% chance
{
    print("You found armor!");
}
else //10% chance
{
    print("You found a potion!");
}

Il motivo per cui i condizionali sono uguali alla sua possibilità più tutte le possibilità dei condizionali precedenti è perché i condizionali precedenti hanno già eliminato la possibilità che siano quegli oggetti. Quindi, per il condizionale dello scudo else if(rand <= 70), 70 è uguale alla probabilità del 45% dello scudo, più il 5% di probabilità dell'oro e il 20% di probabilità della spada.

vantaggi:

  • Facile da programmare, perché non richiede strutture di dati.

svantaggi:

  • Difficile da mantenere, perché è necessario mantenere i tassi di abbandono nel codice. Non è possibile determinarli in fase di esecuzione. Quindi, se vuoi qualcosa di più a prova di futuro, dovresti controllare le altre risposte.

3
Questo sarebbe davvero fastidioso da mantenere. Ad esempio, se desideri rimuovere l'oro e fare in modo che la pozione prenda il suo posto, devi regolare le probabilità di tutti gli oggetti tra di loro.
Alexander - Ripristina Monica il

1
Per evitare il problema menzionato da @Alexander, puoi invece sottrarre il tasso corrente ad ogni passaggio, invece di aggiungerlo a ogni condizione.
AlexanderJ93,

2

In C # è possibile utilizzare una scansione Linq per eseguire l'accumulatore per verificare un numero casuale nell'intervallo compreso tra 0 e 100.0f e .Prima () per ottenere. Quindi, come una riga di codice.

Quindi qualcosa del tipo:

var item = a.Select(x =>
{
    sum += x.prob;
    if (rand < sum)
        return x.item;
    else
        return null;
 }).FirstOrDefault());

sumè un numero intero inizializzato zero ed aè un elenco di prob / elementi / tuple / istanze prob / item. randè un numero casuale precedentemente generato nell'intervallo.

Questo semplicemente accumula la somma sull'elenco degli intervalli fino a quando non supera il numero casuale precedentemente selezionato e restituisce l'articolo o il valore nullo, dove sarebbe restituito null se l'intervallo di numeri casuali (ad esempio 100) è inferiore all'intervallo di ponderazione totale per errore e il numero casuale selezionato è al di fuori dell'intervallo di ponderazione totale.

Tuttavia, noterai che i pesi in OP corrispondono strettamente a una distribuzione normale (curva a campana). Penso che in generale non vorrai intervalli specifici, tenderai a desiderare una distribuzione che si assottigli o attorno a una curva a campana o semplicemente su una curva esponenziale decrescente (ad esempio). In questo caso, puoi semplicemente utilizzare una formula matematica per generare un indice in una matrice di elementi, ordinati in ordine di probabilità preferita. Un buon esempio è il CDF nella distribuzione normale

Anche un esempio qui .

Un altro esempio è che potresti prendere un valore casuale da 90 gradi a 180 gradi per ottenere il quadrante in basso a destra di un cerchio, prendere il componente x usando cos (r) e usarlo per indicizzare in un elenco prioritario.

Con diverse formule potresti avere un approccio generale in cui devi solo inserire un elenco prioritario di qualsiasi lunghezza (es. N) e mappare il risultato della formula (es. Cos (x) è 0 a 1) per moltiplicazione (es. Ncos (x ) = Da 0 a N) per ottenere l'indice.


3
Potresti darci questa riga di codice se è solo una riga? Non ho familiarità con C #, quindi non so cosa intendi.
HEGX64,

@ HEGX64 aggiunto ma utilizzo mobile e editor non funzionanti. Puoi modificare?
Sentinella

4
Puoi cambiare questa risposta per spiegare il concetto alla base, piuttosto che una specifica implementazione in una lingua specifica?
Raimund Krämer,

@ RaimundKrämer Erm, fatto?
Sentinella

Downvote senza spiegazione = inutile e antisociale.
GrGreau,

1

Le probabilità non devono essere codificate. Gli elementi e le soglie possono essere uniti in un array.

for X in itemsrange loop
  If items (X).threshold < random() then
     Announce (items(X).name)
     Exit loop
  End if
End loop

Devi ancora accumulare le soglie, ma puoi farlo durante la creazione di un file di parametri invece di codificarlo.


3
Potresti approfondire come calcolare la soglia corretta? Ad esempio, se hai tre oggetti con una probabilità del 33% ciascuno, come costruiresti questa tabella? Poiché ogni volta viene generato un nuovo random (), il primo avrebbe bisogno di 0,3333, il secondo avrebbe bisogno di 0,5 e l'ultimo avrebbe bisogno di 1,0. O ho letto l'algoritmo sbagliato?
pipe

Lo calcoli come hanno fatto gli altri nelle loro risposte. Per uguali probabilità di oggetti X, la prima soglia è 1 / X, la seconda, 2 / X, ecc.
Francia,

Farlo per 3 elementi in questo algoritmo renderebbe le soglie 1/3, 2/3 e 3/3 ma le probabilità di risultato 1/3, 4/9 e 2/9 per il primo, il secondo e il terzo elemento. Intendi davvero avere la chiamata random()in loop?
pipe,

No, è sicuramente un bug. Ogni controllo richiede lo stesso numero casuale.
WGroleau,

0

Ho svolto questa funzione: https://github.com/thewheelmaker/GDscript_Weighted_Random Now! nel tuo caso puoi usarlo in questo modo:

on_normal_case([5,20,45,20,10],0)

Fornisce solo un numero compreso tra 0 e 4 ma è possibile inserirlo in un array in cui sono stati trovati gli elementi.

item_array[on_normal_case([5,20,45,20,10],0)]

O in funzione:

item_function(on_normal_case([5,20,45,20,10],0))

Ecco il codice L'ho fatto su GDscript, puoi, ma può alterare altre lingue, anche verificare la presenza di errori logici:

func on_normal_case(arrayy,transformm):
    var random_num=0
    var sum=0
    var summatut=0
    #func sumarrays_inarray(array):
    for i in range(arrayy.size()):
        sum=sum+arrayy[i]
#func no_fixu_random_num(here_range,start_from):
    random_num=randi()%sum+1
#Randomies be pressed down
#first start from zero
    if 0<=random_num and random_num<=arrayy[0]:
        #print(random_num)
        #print(array[0])
        return 0+ transformm
    summatut=summatut+arrayy[0]
    for i in range(arrayy.size()-1):
        #they must pluss together
        #if array[i]<=random_num and random_num<array[i+1]:
        if summatut<random_num and random_num<=summatut+arrayy[i+1]:
            #return i+1+transform
            #print(random_num)
            #print(summatut)
            return i+1+ transformm

        summatut=summatut+arrayy[i+1]
    pass

Funziona così: on_normal_case ([50,50], 0) Questo dà 0 o 1, ha entrambe le stesse probabilità.

on_normal_case ([50,50], 1) Questo dà 1 o 2, ha entrambe la stessa probabilità.

on_normal_case ([20,80], 1) Questo dà 1 o 2, ha un cambiamento maggiore per ottenerne due.

on_normal_case ([20,80,20,20,30], 1) Questo dà numeri casuali compresi nell'intervallo 1-5 e numeri più grandi hanno maggiori probabilità di numeri più piccoli.

on_normal_case ([20,80,0,0,20,20,30,0,0,0,0,33], 45) Questo lancio taglia tra i numeri 45,46,49,50,51,56 che vedi quando ci è zero non si verifica mai.

Quindi la funzione restituisce solo un numero casuale che dipende dalla lunghezza di quell'array array e dal numero del trasformm, e gli ints nell'array sono pesi di probabilità che un numero possa verificarsi, dove quel numero è la posizione dell'array, pluss trasformm numero.

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.