Como mencionei no meu comentário acima, recomendo que você crie um perfil antes de complicar demais o seu código. Um for
dado de soma rápida de loop é muito mais fácil de entender e modificar do que fórmulas matemáticas complicadas e criação / pesquisa de tabelas. Sempre perfil primeiro para garantir que você está resolvendo os problemas importantes. ;)
Dito isto, existem duas maneiras principais de amostrar distribuições sofisticadas de probabilidade de uma só vez:
1. Distribuições de Probabilidades Cumulativas
Há um truque interessante para amostrar distribuições de probabilidade contínuas usando apenas uma única entrada aleatória uniforme . Tem a ver com a distribuição cumulativa , a função que responde "Qual é a probabilidade de obter um valor não superior a x?"
Essa função não diminui, iniciando em 0 e subindo para 1 sobre seu domínio. Um exemplo para a soma de dois dados de seis lados é mostrado abaixo:
Se sua função de distribuição cumulativa tiver uma inversa conveniente de calcular (ou você pode aproximar-se dela com funções fragmentadas, como curvas de Bézier), você pode usá-la para obter amostras da função de probabilidade original.
A função inversa manipula o parcelamento do domínio entre 0 e 1 em intervalos mapeados para cada saída do processo aleatório original, com a área de captação de cada uma correspondendo à sua probabilidade original. (Isso é verdade infinitamente em distribuições contínuas. Para distribuições discretas, como lançamentos de dados, precisamos aplicar um arredondamento cuidadoso)
Aqui está um exemplo de como usar isso 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 isso com:
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;
}
Entendeu o que quero dizer sobre a diferença de clareza e flexibilidade de código? A maneira ingênua pode ser ingênua com seus loops, mas é curta e simples, imediatamente óbvia sobre o que faz e fácil de ser dimensionada para diferentes tamanhos e números de matrizes. Fazer alterações no código de distribuição cumulativo requer alguma matemática não trivial e seria fácil interromper e causar resultados inesperados sem erros óbvios. (Que espero não ter feito acima)
Portanto, antes de acabar com um loop claro, tenha certeza absoluta de que é realmente um problema de desempenho que vale esse tipo de sacrifício.
2. O método de alias
O método de distribuição cumulativa funciona bem quando você pode expressar o inverso da função de distribuição cumulativa como uma expressão matemática simples, mas isso nem sempre é fácil ou até possível. Uma alternativa confiável para distribuições discretas é algo chamado Método Alias .
Isso permite que você faça uma amostra de qualquer distribuição de probabilidade discreta arbitrária usando apenas duas entradas aleatórias independentes e uniformemente distribuídas.
Ele funciona pegando uma distribuição como a abaixo, à esquerda (não se preocupe, pois as áreas / pesos não somam 1, para o método Alias, nos preocupamos com o peso relativo ) e convertendo-a em uma tabela como a da o certo onde:
- Há uma coluna para cada resultado.
- Cada coluna é dividida em no máximo duas partes, cada uma associada a um dos resultados originais.
- A área / peso relativo de cada resultado é preservada.
(Diagrama baseado nas imagens deste excelente artigo sobre métodos de amostragem )
No código, representamos isso com duas tabelas (ou uma tabela de objetos com duas propriedades) representando a probabilidade de escolher o resultado alternativo de cada coluna e a identidade (ou "alias") desse resultado alternativo. Em seguida, podemos amostrar da distribuição da seguinte forma:
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];
}
}
Isso envolve um pouco de configuração:
Calcule as probabilidades relativas de todos os resultados possíveis (por isso, se você estiver lançando 1000d6, precisamos calcular o número de maneiras de obter cada soma de 1000 a 6000)
Crie um par de tabelas com uma entrada para cada resultado. O método completo vai além do escopo desta resposta, por isso recomendo que se refira a esta explicação do algoritmo do método Alias .
Armazene essas tabelas e consulte-as sempre que precisar de um novo rolo aleatório desta distribuição.
Esta é uma troca de espaço-tempo . A etapa de pré-computação é um pouco exaustiva e precisamos reservar memória proporcional ao número de resultados que temos (embora, mesmo para 1000d6, falemos kilobytes de um dígito, para que nada perca o sono), mas em troca de nossa amostragem é de tempo constante, por mais complexa que seja a nossa distribuição.
Espero que um ou outro desses métodos possa ter alguma utilidade (ou que eu o tenha convencido de que a simplicidade do método ingênuo vale o tempo que leva para fazer um loop);)