Как я уже упоминал в своем комментарии выше, я рекомендую вам профилировать его перед тем, как усложнять свой код. Кости с быстрым for
циклом суммирования намного легче понять и изменить, чем сложные математические формулы и построение / поиск таблиц. Всегда делайте профиль первым, чтобы убедиться, что вы решаете важные проблемы. ;)
Тем не менее, есть два основных способа выборки сложных распределений вероятностей одним махом:
1. Совокупное распределение вероятностей
Есть хитрый трюк для выборки из непрерывных распределений вероятности, используя только один равномерный случайный вход . Это связано с кумулятивным распределением , функцией, которая отвечает: «Какова вероятность получения значения, не превышающего x?»
Эта функция является неубывающей, начиная с 0 и повышаясь до 1 в своей области. Пример суммы двух шестигранных кубиков показан ниже:
Если ваша кумулятивная функция распределения имеет удобный для вычисления обратный результат (или вы можете аппроксимировать ее кусочными функциями, такими как кривые Безье), вы можете использовать ее для выборки из исходной функции вероятности.
Обратная функция обрабатывает разделение области между 0 и 1 на интервалы, сопоставленные с каждым выходом исходного случайного процесса, причем область охвата каждого совпадает с его первоначальной вероятностью. (Это верно бесконечно мало для непрерывных распределений. Для дискретных распределений, таких как броски костей, мы должны применять осторожное округление)
Вот пример использования этого для эмуляции 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);
}
Сравните это с:
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;
}
Видите, что я имею в виду в отношении разницы в ясности кода и гибкости? Наивный способ может быть наивным с его циклами, но он короткий и простой, сразу же понятно, что он делает, и его легко масштабировать до различных размеров и чисел. Внесение изменений в накопительный код распределения требует некоторой нетривиальной математики, и было бы легко сломать и вызвать неожиданные результаты без каких-либо очевидных ошибок. (Что я надеюсь, я не сделал выше)
Итак, прежде чем покончить с четким циклом, убедитесь, что это действительно проблема производительности, которая стоит такого рода жертв.
2. Метод псевдонимов
Кумулятивный метод распределения работает хорошо, когда вы можете выразить инверсию кумулятивной функции распределения как простое математическое выражение, но это не всегда легко или даже невозможно. Надежной альтернативой для дискретных распределений является метод псевдонимов .
Это позволяет вам выбирать из любого произвольного дискретного распределения вероятности, используя только два независимых, равномерно распределенных случайных входа.
Он работает, беря распределение, подобное приведенному ниже слева (не беспокойтесь, что площади / веса не равны 1, для метода псевдонимов мы заботимся об относительном весе) и конвертируем его в таблицу, подобную той, что на право где:
- Существует один столбец для каждого результата.
- Каждый столбец разбит не более чем на две части, каждая из которых связана с одним из исходных результатов.
- Относительная площадь / вес каждого результата сохраняется.
(Схема основана на изображениях из этой превосходной статьи о методах отбора проб )
В коде мы представляем это с помощью двух таблиц (или таблицы объектов с двумя свойствами), представляющих вероятность выбора альтернативного результата из каждого столбца и идентичность (или «псевдоним») этого альтернативного результата. Затем мы можем сделать выборку из дистрибутива следующим образом:
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];
}
}
Это включает в себя немного настройки:
Вычислить относительные вероятности каждого возможного результата (поэтому, если вы бросаете 1000d6, нам нужно вычислить количество способов получить каждую сумму от 1000 до 6000)
Создайте пару таблиц с записью для каждого результата. Полный метод выходит за рамки этого ответа, поэтому я настоятельно рекомендую обратиться к этому объяснению алгоритма метода псевдонима .
Сохраняйте эти таблицы и обращайтесь к ним каждый раз, когда вам понадобится новый случайный бросок кубика из этого дистрибутива.
Это пространственно-временной компромисс . Этап предварительного вычисления является несколько исчерпывающим, и нам нужно выделить память пропорционально количеству результатов, которые у нас есть (хотя даже для 1000d6 мы говорим однозначные килобайты, поэтому не нужно терять сон), но взамен нашей выборки постоянное время, независимо от того, насколько сложным может быть наше распределение.
Я надеюсь, что один или другой из этих методов может быть полезным (или что я убедил вас, что простота наивного метода стоит времени, которое требуется для цикла);)