Як я вже згадував у своєму коментарі вище, рекомендую вам профайлювати це, перш ніж надмірно ускладнювати свій код. Швидке for
кільце, що підсумовує кістки, набагато простіше зрозуміти та змінити, ніж складні математичні формули та побудова таблиць / пошук. Завжди спочатку профілюйте, щоб переконатися, що ви вирішуєте важливі проблеми. ;)
Однак, є два основні способи вибірки складних розподілів ймовірностей одним махом:
1. Сукупні розподіли ймовірностей
Існує акуратний трюк для вибірки з безперервного розподілу ймовірностей, використовуючи лише один рівномірний випадковий вхід . Це має відношення до кумулятивного розподілу , функції, яка відповідає "Яка ймовірність отримання значення не більше х?"
Ця функція не зменшується, починаючи з 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. Метод псевдоніма
Метод кумулятивного розподілу працює добре, коли ви можете виразити зворотну функцію кумулятивного розподілу як простий математичний вираз, але це не завжди просто і навіть можливо. Надійною альтернативою дискретних розподілів є те, що називається методом Alias .
Це дозволяє вибирати будь-який довільний дискретний розподіл ймовірностей, використовуючи лише два незалежні, рівномірно розподілені випадкові входи.
Це працює, взявши розподіл, подібний до наведеного нижче, ліворуч (не хвилюйтеся, що площі / ваги не дорівнюють 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 ми говоримо про одноцифрові кілобайти, тому нічого не втрачати), але в обмін нашої вибірки є постійним часом, незалежно від того, наскільки складним може бути наш розподіл.
Я сподіваюся, що один чи інший із цих методів може бути корисним (або що я переконав вас, що простота наївного методу коштує часу, необхідного для його циклу);)