Точно імітуючи безліч рулонів кісток без петель?


14

Гаразд, якщо ваша гра заграє кубиків, ви можете просто зателефонувати в генератор випадкових чисел. Але для будь-якого набору кубиків, які викочуються досить часто, ви отримаєте криву розподілу / гістограму. Отже, моє запитання: чи є приємний простий розрахунок, який я можу запустити, який дасть мені число, яке відповідає цьому розподілу?

Напр. 2D6 - оцінка -% ймовірність

2 - 2,77%

3 - 5,55%

4 - 8,33%

5 - 11,11%

6 - 13,88%

7 - 16,66%

8 - 13,88%

9 - 11,11%

10 - 8,33%

11 - 5,55%

12 - 2,77%

Отже, знаючи вищесказане, ви можете згорнути один d100 і розробити точне значення 2D6. Але як тільки ми почнемо з 10D6, 50D6, 100D6, 1000D6, це може заощадити багато часу на обробку. Тож повинен бути підручник / метод / алгоритм, який може це зробити швидко? Це, мабуть, зручно для фондових ринків, казино, стратегічних ігор, фортеці-карликів тощо. Що робити, якщо ви могли б імітувати результати повного стратегічного бою, який би зайняв кілька годин, щоб грати з кількома дзвінками на цю функцію та деякою базовою математикою?


5
Навіть при 1000 d6 цикл буде досить швидким на сучасному ПК, що ви навряд чи це помітите, тому це може бути передчасна оптимізація. Завжди спробуйте профілювати, перш ніж замінювати прозору петлю непрозорою формулою. Однак, існують алгоритмічні варіанти. Вас цікавить дискретна ймовірність на зразок кісток у ваших прикладах, чи прийнятно моделювати їх як безперервний розподіл ймовірностей (тому можливий дробовий результат на зразок 2,5)?
DMGregory

DMGregory правильна, обчислення 1000d6 не буде такою великою частиною свиней процесора. Однак є річ під назвою Біноміальний розподіл, яка (при розумній роботі) отримає результат, який вас цікавить. Також, якщо ви хочете колись знайти ймовірності для довільного набору правил рулону, спробуйте TRoll, який має скромну мову встановити для того, щоб вказати, як закотити набір кісток, і він обчислить усі ймовірності для кожного можливого результату.
Draco18s більше не довіряє SE

Скористайтеся розподілом Пуассона: с.
Луїс Масуеллі

1
Для будь-якого набору кісток, який буде прокатуватися досить часто, ви, ймовірно, отримаєте криву / гістограму розподілу. Це важлива відмінність. Кістки можуть згорнути мільйон 6 разів поспіль, навряд чи, але це може
Річард Тінгл

@RichardTingle Чи можете ви детальніше? Крива / гістограма розподілу також включатиме випадок "мільйон 6 підряд".
amitp

Відповіді:


16

Як я вже згадував у своєму коментарі вище, рекомендую вам профайлювати це, перш ніж надмірно ускладнювати свій код. Швидке forкільце, що підсумовує кістки, набагато простіше зрозуміти та змінити, ніж складні математичні формули та побудова таблиць / пошук. Завжди спочатку профілюйте, щоб переконатися, що ви вирішуєте важливі проблеми. ;)

Однак, є два основні способи вибірки складних розподілів ймовірностей одним махом:


1. Сукупні розподіли ймовірностей

Існує акуратний трюк для вибірки з безперервного розподілу ймовірностей, використовуючи лише один рівномірний випадковий вхід . Це має відношення до кумулятивного розподілу , функції, яка відповідає "Яка ймовірність отримання значення не більше х?"

Ця функція не зменшується, починаючи з 0 і піднімаючись до 1 над своєю областю. Приклад суми двох шестигранних кісток наведено нижче:

Графіки ймовірності, кумулятивного розподілу та оберненого для 2d6

Якщо у вашій функції кумулятивного розподілу є зручна для обчислення обернена (або ви можете наблизити її за допомогою кускових функцій, таких як криві Безьє), ви можете використовувати це для вибірки з вихідної функції ймовірності.

Зворотна функція обробляє розбиття домену між 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, для методу Псевдоніма ми дбаємо про відносну вагу) та перетворимо його в таблицю, подібну до тієї на право де:

  • Для кожного результату є одна колонка.
  • Кожен стовпець розбивається на щонайменше дві частини, кожна пов'язана з одним із початкових результатів.
  • Відносна площа / вага кожного результату зберігається.

Приклад методу Alias ​​перетворення розподілу в таблицю пошуку

(Діаграма на основі зображень із цієї чудової статті про методи вибірки )

У коді ми представляємо це двома таблицями (або таблицею об'єктів з двома властивостями), що представляють ймовірність вибору альтернативного результату з кожного стовпця та ідентичність (або "псевдонім") цього альтернативного результату. Тоді ми можемо взяти вибірку з розподілу так:

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];
    }
}

Це передбачає трохи налаштування:

  1. Обчисліть відносні ймовірності кожного можливого результату (тому якщо ви прокручуєте 1000d6, нам потрібно обчислити кількість способів отримати кожну суму від 1000 до 6000)

  2. Побудуйте пару таблиць із записом для кожного результату. Повний метод виходить за рамки цієї відповіді, тому я настійно рекомендую звернутися до цього пояснення алгоритму методу псевдоніму .

  3. Зберігайте ці таблиці та посилайтеся на них щоразу, коли вам потрібен новий випадковий штамповий ролик із цього розповсюдження.

Це компроміс у просторі та часі . Крок попереднього обчислення є дещо вичерпним, і нам потрібно відкласти пам'ять пропорційно кількості результатів, які ми маємо (хоча навіть за 1000d6 ми говоримо про одноцифрові кілобайти, тому нічого не втрачати), але в обмін нашої вибірки є постійним часом, незалежно від того, наскільки складним може бути наш розподіл.


Я сподіваюся, що один чи інший із цих методів може бути корисним (або що я переконав вас, що простота наївного методу коштує часу, необхідного для його циклу);)


1
Дивовижна відповідь. Мені подобається наївний підхід. Набагато менше місця для помилок і легко зрозуміти.
bummzack

FYI це питання є копіювальною вставкою з випадкового запитання на reddit.
Vaillancourt

Для неповноти я думаю, що це нитка reddit, про яку говорить @AlexandreVaillancourt. Відповіді в основному пропонують зберегти циклічну версію (з деякими доказами, що її часова вартість може бути розумною) або наблизити велику кількість кісток, використовуючи нормальний / гауссовий розподіл.
DMGregory

+1 для методу псевдоніму, мабуть, так мало хто про це знає, і це дійсно ідеальне рішення для багатьох таких типів ситуацій вибору ймовірності та +1 для згадки про рішення Гаусса, яке, мабуть, є "кращим" відповідь, якщо ми дбаємо лише про продуктивність та економію місця.
WHN

0

На жаль, відповідь полягає в тому, що цей метод не призведе до підвищення продуктивності.

Я вважаю, що у питанні, як генерується випадкове число, можуть виникнути непорозуміння. Візьмемо приклад нижче [Java]:

Random r = new Random();
int n = 20;
int min = 1; //arbitrary
int max = 6; //arbitrary
for(int i = 0; i < n; i++){
    int randomNumber = (r.nextInt(max - min + 1) + min)); //silly maths
    System.out.println("Here's a random number: " + randomNumber);
}

Цей код буде циклічно розміщувати 20 разів, друкуючи випадкові числа від 1 до 6 (включно). Коли ми говоримо про продуктивність цього коду, потрібен певний час, щоб створити об'єкт Random (який передбачає створення масиву псевдовипадкових цілих чисел на основі внутрішнього годинника комп'ютера на час його створення), а потім 20 постійних часу перегляд кожного виклику nextInt (). Оскільки кожен "рулон" - це постійна операція за часом, це робить прокат дуже дешевим у часі. Також зауважте, що діапазон від мінімуму до максимуму не має значення (іншими словами, комп'ютер може накручувати d6 так само просто, як і для прокатки d10000). Якщо говорити про часову складність, продуктивність рішення просто O (n), де n - кількість кісток.

Крім того, ми могли б наблизити будь-яку кількість рулонів d6 до одного d100 (або d10000 для цього питання). Використовуючи цей метод, нам спочатку потрібно обчислити відсотки s [кількість граней на кубики] * n [кількість кісток], перш ніж розгорнути (технічно це s * n - n + 1 відсоток, і ми повинні мати можливість розділити це приблизно навпіл, оскільки це симетрично; зауважте, що у вашому прикладі для імітації рулону 2d6 ви обчислили 11 відсотків і 6 були унікальними). Після прокатки ми можемо скористатися двійковим пошуком, щоб визначити, у який діапазон потрапив наш рулон. За часовою складністю це рішення оцінюється до рішення O (s * n), де s - кількість сторін, а n - кількість кісток. Як ми бачимо, це повільніше, ніж рішення O (n), запропоноване в попередньому пункті.

Екстраполюючи звідти, скажіть, що ви створили обидві ці програми для імітації рулону 1000d20. Перший просто закатав би 1000 разів. Друга програма спочатку повинна визначити 19,001 відсотків (для потенційного діапазону від 1000 до 20000), перш ніж робити щось інше. Тому, якщо ви не користуєтесь дивною системою, де пошук пам’яті в кілька разів дорожчий, ніж операції з плаваючою комою, використання виклику nextInt () для кожного рулону здається дорогою.


2
Вище наведений аналіз не зовсім коректний. Якщо ми відкладемо деякий час вперед, щоб генерувати таблиці ймовірностей та псевдонімів відповідно до методу Псевдоніму , то ми можемо взяти вибірку з довільного дискретного розподілу ймовірностей у постійному часі (2 випадкові числа та пошук таблиці). Тож імітація рулону з 5 кубиків або рулону з 500 кубиків займає стільки ж роботи, як тільки таблиці будуть підготовлені. Це асимптотично швидше, ніж перебирання великої кількості кісток для кожного зразка, хоча це не обов'язково робить його кращим рішенням проблеми. ;)
DMGregory

0

Якщо ви хочете зберігати комбінації кісток, хороша новина полягає в тому, що рішення є, а поганим є те, що наші комп’ютери якось обмежені щодо подібних проблем.

Хороша новина:

Існує детерміністський підхід до цієї проблеми:

1 / Обчисліть усі комбінації вашої групи кісток

2 / Визначте ймовірність для кожної комбінації

3 / Шукайте результат у цьому списку замість того, щоб кидати кубики

Погані новини:

Кількість поєднання з повторенням задається наступними формулами

Γнк=(н+к-1к)=(н+к-1)!к! (н-1)!

( з французької вікіпедії ):

Поєднання з повторами

Це означає, що, наприклад, зі 150 кубиками ви маєте 698'526'906 комбінацій. Припустимо, ви зберігаєте ймовірність як 32-розрядний плавець, вам знадобиться 2,6 ГБ пам'яті, і вам все одно потрібно додати вимогу пам'яті до індексів ...

В обчислювальному плані число комбінацій можна обчислити закрутками, що зручно, але не вирішує обмежень пам'яті.

На закінчення, для великої кількості кубиків я б радив кидати кубики і спостерігати за результатом, а не попередньо розраховувати ймовірності, пов'язані з кожною комбінацією.

Редагувати

Однак, оскільки вас цікавить лише сума кубиків, ви можете зберігати ймовірності з набагато меншими ресурсами.

Ви можете обчислити точні ймовірності для кожної суми кісток за допомогою згортки.

Жi(м)=нЖ1(н)Жi-1(м-н)

Потім, починаючи з 1/6 форми кожного результату з 1 кістки, ви можете побудувати всі правильні ймовірності для будь-якої кількості кісток.

Ось приблизний код Java, який я написав для ілюстрації (не дуже оптимізований):

public class DiceProba {

private float[][] probas;
private int currentCalc;

public int getCurrentCalc() {
    return currentCalc;
}

public float[][] getProbas() {
    return probas;
}

public void calcProb(int faces, int diceNr) {

    if (diceNr < 0) {
        currentCalc = 0;
        return;
    }

    // Initialize
    float baseProba = 1.0f / ((float) faces);
    probas = new float[diceNr][];
    probas[0] = new float[faces + 1];
    probas[0][0] = 0.0f;
    for (int i = 1; i <= faces; ++i)
        probas[0][i] = baseProba;

    for (int i = 1; i < diceNr; ++i) {

        int maxValue = (i + 1) * faces + 1;
        probas[i] = new float[maxValue];

        for (int j = 0; j < maxValue; ++j) {

            probas[i][j] = 0;
            for (int k = 0; k <= j; ++k) {
                probas[i][j] += probability(faces, k, 0) * probability(faces, j - k, i - 1);
            }

        }

    }

    currentCalc = diceNr;

}

private float probability(int faces, int number, int diceNr) {

    if (number < 0 || number > ((diceNr + 1) * faces))
        return 0.0f;

    return probas[diceNr][number];

}

}

Зателефонуйте calcProb () з потрібними параметрами та перейдіть до таблиці проб для отримання результатів (перший індекс: 0 для 1 кубика, 1 для двох кубиків ...).

Я перевірив це на 1'000D6 на своєму ноутбуці, для обчислення всіх ймовірностей від 1 до 1'000 кубиків та всіх можливих сум кубиків знадобилося 10 секунд.

Завдяки попередній обчислювальній роботі та ефективному зберіганню ви можете отримати швидкі відповіді на велику кількість кубиків.

Сподіваюся, це допомагає.


3
Оскільки ОП шукає лише значення суми кісток, ця комбінаторна математика не застосовується, а кількість записів таблиці ймовірностей зростає лінійно з розміром кісток та кількістю кісток.
DMGregory

Ти правий ! Я відредагував свою відповідь. Ми завжди розумні, коли багато;)
elenfoiro78

Я думаю, ви можете трохи підвищити ефективність, використовуючи підхід «ділити і перемогти». Ми можемо обчислити таблицю ймовірностей для 20d6, склавши таблицю для 10d6 із собою. 10d6 ми можемо знайти, склавши таблицю 5d6 із собою. 5d6 ми можемо знайти, склавши таблиці 2d6 & 3d6. Далі вдвічі половина дозволяє нам пропустити генерування більшості розмірів таблиці від 1-20, і зосередити зусилля на цікавих.
DMGregory

1
І використовуйте симетрію!
elenfoiro78
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.