Como mencioné en mi comentario anterior, le recomiendo que haga un perfil de esto antes de complicar demasiado su código. Un for
dado de suma de bucle rápido es mucho más fácil de entender y modificar que las fórmulas matemáticas complicadas y la creación / búsqueda de tablas. Siempre perfile primero para asegurarse de que está resolviendo los problemas importantes. ;)
Dicho esto, hay dos formas principales de muestrear distribuciones de probabilidad sofisticadas de una sola vez:
1. Distribuciones de probabilidad acumulativa
Hay un buen truco para muestrear a partir de distribuciones de probabilidad continuas utilizando solo una entrada aleatoria uniforme . Tiene que ver con la distribución acumulativa , la función que responde "¿Cuál es la probabilidad de obtener un valor no mayor que x?"
Esta función no disminuye, comienza en 0 y sube a 1 sobre su dominio. A continuación se muestra un ejemplo de la suma de dos dados de seis lados:
Si su función de distribución acumulativa tiene un inverso conveniente para calcular (o puede aproximarlo con funciones por partes como las curvas de Bézier), puede usar esto para tomar muestras de la función de probabilidad original.
La función inversa maneja la parcelación del dominio entre 0 y 1 en intervalos asignados a cada salida del proceso aleatorio original, con el área de captación de cada uno coincidiendo con su probabilidad original. (Esto es cierto infinitamente para distribuciones continuas. Para distribuciones discretas como tiradas de dados, debemos aplicar un redondeo cuidadoso)
Aquí hay un ejemplo de cómo usar esto para emular 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);
}
Compare esto 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;
}
¿Ves lo que quiero decir sobre la diferencia en la claridad y flexibilidad del código? La forma ingenua puede ser ingenua con sus bucles, pero es corta y simple, inmediatamente obvia sobre lo que hace y fácil de escalar a diferentes tamaños y números de dados. Realizar cambios en el código de distribución acumulativo requiere algunas matemáticas no triviales, y sería fácil de romper y causar resultados inesperados sin errores obvios. (Que espero no haber hecho arriba)
Por lo tanto, antes de deshacerse de un ciclo claro, asegúrese absolutamente de que realmente sea un problema de rendimiento que valga este tipo de sacrificio.
2. El método alias
El método de distribución acumulativa funciona bien cuando puede expresar el inverso de la función de distribución acumulativa como una simple expresión matemática, pero eso no siempre es fácil o incluso posible. Una alternativa confiable para distribuciones discretas es algo llamado Método Alias .
Esto le permite tomar muestras de cualquier distribución de probabilidad discreta arbitraria utilizando solo dos entradas aleatorias independientes, distribuidas uniformemente.
Funciona tomando una distribución como la de abajo a la izquierda (no se preocupe que las áreas / pesos no sumen 1, para el Método Alias nos importa el peso relativo ) y conviértalo en una tabla como la de el derecho donde:
- Hay una columna para cada resultado.
- Cada columna se divide en a lo sumo dos partes, cada una asociada con uno de los resultados originales.
- Se preserva el área / peso relativo de cada resultado.
(Diagrama basado en imágenes de este excelente artículo sobre métodos de muestreo )
En el código, representamos esto con dos tablas (o una tabla de objetos con dos propiedades) que representan la probabilidad de elegir el resultado alternativo de cada columna y la identidad (o "alias") de ese resultado alternativo. Entonces podemos muestrear de la distribución así:
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];
}
}
Esto implica un poco de configuración:
Calcule las probabilidades relativas de cada resultado posible (por lo tanto, si está obteniendo 1000d6, necesitamos calcular la cantidad de formas de obtener cada suma de 1000 a 6000)
Construya un par de tablas con una entrada para cada resultado. El método completo va más allá del alcance de esta respuesta, por lo que recomiendo consultar esta explicación del algoritmo del Método Alias .
Almacene esas tablas y refiérase a ellas cada vez que necesite una nueva tirada aleatoria de esta distribución.
Esta es una compensación espacio-tiempo . El paso de precomputación es algo exhaustivo, y necesitamos reservar memoria proporcional al número de resultados que tenemos (aunque incluso para 1000d6, estamos hablando de kilobytes de un solo dígito, por lo que no hay nada que perder el sueño), pero a cambio de nuestra muestra es de tiempo constante, no importa cuán compleja sea nuestra distribución.
Espero que uno u otro de esos métodos pueda ser de alguna utilidad (o que te haya convencido de que la simplicidad del método ingenuo vale la pena el tiempo que lleva en bucle);)