Come ho già detto nel mio commento sopra, ti consiglio di profilare questo prima di complicare eccessivamente il tuo codice. Un for
dado sommatore ad anello rapido è molto più facile da capire e modificare rispetto alle complicate formule matematiche e alla costruzione / ricerca di tabelle. Profila sempre per primo per assicurarti di risolvere i problemi importanti. ;)
Detto questo, ci sono due modi principali per campionare sofisticate distribuzioni di probabilità in un colpo solo:
1. Distribuzioni di probabilità cumulative
C'è un trucco per campionare da distribuzioni di probabilità continue usando solo un singolo input casuale uniforme . Ha a che fare con la distribuzione cumulativa , la funzione che risponde "Qual è la probabilità di ottenere un valore non maggiore di x?"
Questa funzione non è decrescente, a partire da 0 e passando a 1 sul suo dominio. Un esempio per la somma di due dadi a sei facce è mostrato sotto:
Se la tua funzione di distribuzione cumulativa ha un inverso comodo da calcolare (o puoi approssimarlo con funzioni a tratti come le curve di Bézier), puoi usarlo per campionare dalla funzione di probabilità originale.
La funzione inversa gestisce la suddivisione del dominio tra 0 e 1 in intervalli mappati su ciascun output del processo casuale originale, con l'area di captazione di ciascuno corrispondente alla sua probabilità originale. (Questo è vero all'infinito per le distribuzioni continue. Per distribuzioni discrete come i tiri di dado dobbiamo applicare un arrotondamento accurato)
Ecco un esempio dell'uso di questo per emulare 2d6:
int SimRoll2d6()
{
// Get a random input in the half-open interval [0, 1).
float t = Random.Range(0f, 1f);
float v;
// Piecewise inverse calculated by hand. ;)
if(t <= 0.5f)
{
v = (1f + sqrt(1f + 288f * t)) * 0.5f;
}
else
{
v = (25f - sqrt(289f - 288f * t)) * 0.5f;
}
return floor(v + 1);
}
Confronta questo con:
int NaiveRollNd6(int n)
{
int sum = 0;
for(int i = 0; i < n; i++)
sum += Random.Range(1, 7); // I'm used to Range never returning its max
return sum;
}
Vedi cosa intendo per la differenza di chiarezza e flessibilità del codice? Il modo ingenuo potrebbe essere ingenuo con i suoi anelli, ma è breve e semplice, immediatamente ovvio su ciò che fa e facile da ridimensionare in base alle diverse dimensioni e numeri di matrici. Apportare modifiche al codice di distribuzione cumulativo richiede alcuni calcoli non banali e sarebbe facile rompere e causare risultati imprevisti senza errori evidenti. (Che spero di non aver fatto sopra)
Quindi, prima di eliminare un ciclo chiaro, assicurati assolutamente che sia davvero un problema di prestazioni degno di questo tipo di sacrificio.
2. Il metodo Alias
Il metodo di distribuzione cumulativa funziona bene quando puoi esprimere l'inverso della funzione di distribuzione cumulativa come una semplice espressione matematica, ma non è sempre facile o addirittura possibile. Un'alternativa affidabile per le distribuzioni discrete è qualcosa chiamato il metodo Alias .
Ciò consente di campionare da qualsiasi distribuzione di probabilità discreta arbitraria utilizzando solo due input casuali indipendenti, distribuiti uniformemente.
Funziona prendendo una distribuzione come quella in basso a sinistra (non preoccuparti che le aree / i pesi non si sommino a 1, per il metodo Alias ci preoccupiamo del peso relativo ) e convertendolo in una tabella come quella in il giusto dove:
- C'è una colonna per ogni risultato.
- Ogni colonna è suddivisa in al massimo due parti, ognuna associata a uno dei risultati originali.
- L'area / peso relativo di ciascun risultato viene preservato.
(Diagramma basato sulle immagini di questo eccellente articolo sui metodi di campionamento )
Nel codice, lo rappresentiamo con due tabelle (o una tabella di oggetti con due proprietà) che rappresentano la probabilità di scegliere il risultato alternativo da ciascuna colonna e l'identità (o "alias") di quel risultato alternativo. Quindi possiamo campionare dalla distribuzione in questo modo:
int SampleFromTables(float[] probabiltyTable, int[] aliasTable)
{
int column = Random.Range(0, probabilityTable.Length);
float p = Random.Range(0f, 1f);
if(p < probabilityTable[column])
{
return column;
}
else
{
return aliasTable[column];
}
}
Ciò comporta un po 'di configurazione:
Calcola le probabilità relative di ogni possibile risultato (quindi se stai tirando 1000d6, dobbiamo calcolare il numero di modi per ottenere ogni somma da 1000 a 6000)
Costruisci una coppia di tabelle con una voce per ogni risultato. Il metodo completo va oltre lo scopo di questa risposta, quindi consiglio vivamente di fare riferimento a questa spiegazione dell'algoritmo del metodo Alias .
Conserva quelle tabelle e fai riferimento ad esse ogni volta che hai bisogno di un nuovo tiro di dado casuale da questa distribuzione.
Questo è un compromesso spazio-temporale . Il passaggio di pre-calcolo è alquanto esaustivo e dobbiamo mettere da parte la memoria proporzionata al numero di risultati che abbiamo (anche se anche per 1000d6, stiamo parlando di kilobyte a una cifra, quindi nulla su cui perdere il sonno), ma in cambio del nostro campionamento è un tempo costante, non importa quanto complessa potrebbe essere la nostra distribuzione.
Spero che l'uno o l'altro di questi metodi possa essere di qualche utilità (o che ti abbia convinto che la semplicità del metodo ingenuo vale il tempo necessario per eseguire il loop);)