Як створити зважену колекцію, а потім вибрати з неї випадковий елемент?


34

У мене є вікно грабування, яке я хочу заповнити випадковим елементом. Але я хочу, щоб кожен предмет мав різні шанси на вибір. Наприклад:

  • 5% шанс на 10 золотих
  • 20% шансів меча
  • 45% шансів на щит
  • 20% шанс броні
  • 10% шанс зілля

Як я можу зробити так, щоб я обрав саме один із пунктів вище, де ці відсотки - це відповідні шанси на отримання видобутку?


1
FYI, теоретично, O (1) час на зразок можливий для будь-якого кінцевого розподілу, навіть розподілу, записи якого динамічно змінюються. Див , наприклад , cstheory.stackexchange.com/questions/37648 / ... .
Ніл Янг

Відповіді:


37

Рішення ймовірностей з м'яким кодом

Рішення ймовірності з твердим кодом має той недолік, що вам потрібно встановити ймовірності у своєму коді. Ви не можете визначити їх під час виконання. Це також важко підтримувати.

Ось динамічна версія того ж алгоритму.

  1. Створіть масив пар фактичних елементів та ваги кожного елемента
  2. Коли ви додаєте елемент, вага предмета повинна бути його власною вагою плюс сума ваг усіх елементів, які вже є в масиві. Тож слід відстежувати суму окремо. Тим більше, що вам це знадобиться для наступного кроку.
  3. Щоб отримати об'єкт, генеруйте випадкове число від 0 до суми ваг усіх елементів
  4. ітератуйте масив від початку до кінця, поки ви не знайдете запис із вагою, більшою або рівною випадковому числу

Ось приклад реалізації на Java у вигляді шаблонного класу, який ви можете створити для будь-якого об'єкта, який використовує ваша гра. Потім ви можете додати об'єкти методом .addEntry(object, relativeWeight)і вибрати одну із записів, яку ви додали раніше.get()

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class WeightedRandomBag<T extends Object> {

    private class Entry {
        double accumulatedWeight;
        T object;
    }

    private List<Entry> entries = new ArrayList<>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void addEntry(T object, double weight) {
        accumulatedWeight += weight;
        Entry e = new Entry();
        e.object = object;
        e.accumulatedWeight = accumulatedWeight;
        entries.add(e);
    }

    public T getRandom() {
        double r = rand.nextDouble() * accumulatedWeight;

        for (Entry entry: entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.object;
            }
        }
        return null; //should only happen when there are no entries
    }
}

Використання:

WeightedRandomBag<String> itemDrops = new WeightedRandomBag<>();

// Setup - a real game would read this information from a configuration file or database
itemDrops.addEntry("10 Gold",  5.0);
itemDrops.addEntry("Sword",   20.0);
itemDrops.addEntry("Shield",  45.0);
itemDrops.addEntry("Armor",   20.0);
itemDrops.addEntry("Potion",  10.0);

// drawing random entries from it
for (int i = 0; i < 20; i++) {
    System.out.println(itemDrops.getRandom());
}

Ось той самий клас, реалізований у C # для вашого проекту Unity, XNA або MonoGame:

using System;
using System.Collections.Generic;

class WeightedRandomBag<T>  {

    private struct Entry {
        public double accumulatedWeight;
        public T item;
    }

    private List<Entry> entries = new List<Entry>();
    private double accumulatedWeight;
    private Random rand = new Random();

    public void AddEntry(T item, double weight) {
        accumulatedWeight += weight;
        entries.Add(new Entry { item = item, accumulatedWeight = accumulatedWeight });
    }

    public T GetRandom() {
        double r = rand.NextDouble() * accumulatedWeight;

        foreach (Entry entry in entries) {
            if (entry.accumulatedWeight >= r) {
                return entry.item;
            }
        }
        return default(T); //should only happen when there are no entries
    }
}

І ось один у JavaScript :

var WeightedRandomBag = function() {

    var entries = [];
    var accumulatedWeight = 0.0;

    this.addEntry = function(object, weight) {
        accumulatedWeight += weight;
        entries.push( { object: object, accumulatedWeight: accumulatedWeight });
    }

    this.getRandom = function() {
        var r = Math.random() * accumulatedWeight;
        return entries.find(function(entry) {
            return entry.accumulatedWeight >= r;
        }).object;
    }   
}

Про:

  • Може працювати з будь-якими співвідношеннями ваги. Ви можете мати в наборі предмети з астрономічно малою ймовірністю. Ваги також не потрібно складати до 100.
  • Ви можете читати предмети та ваги під час виконання
  • Використання пам'яті пропорційно кількості елементів у масиві

Контраст:

  • Щоб отримати право, потрібно трохи більше програмування
  • У гіршому випадку, можливо, вам доведеться повторити весь масив ( O(n)складність виконання). Тому коли у вас дуже великий набір предметів і малюєте дуже часто, це може стати повільним. Проста оптимізація полягає в тому, щоб спочатку ставити найімовірніші елементи, щоб алгоритм припинявся на ранніх етапах у більшості випадків. Складнішою оптимізацією, яку ви можете зробити, є використання факту сортування масиву та пошуку бісекції. На це потрібен лише O(log n)час.
  • Потрібно створити список у пам'яті перед тим, як використовувати його (хоча ви можете легко додавати елементи під час виконання. Також можна буде додавати елементи для видалення, але це потребує оновлення накопиченої ваги всіх елементів, що надходять після видаленого запису, який знову є O(n)найгірший час виконання)

2
Код C # можна записати за допомогою LINQ: return ent.FirstOrDefault (e => e.accumulatedWeight> = r). Що ще важливіше, є невелика ймовірність того, що через втрату точності з плаваючою точкою цей алгоритм повернеться в нулеве значення, якщо випадкове значення отримає лише трохи, ніж накопичене значення. В якості запобіжних заходів ви можете додати невелике значення (скажімо, 1,0) до останнього елемента, але тоді вам доведеться чітко вказати у своєму коді, що список остаточний.
IMil

1
Один невеликий варіант щодо цього, який я особисто використовував, якщо ви хочете, щоб значення ваги під час виконання не змінювались на значення ваги плюс-всі-попередні, ви можете відняти вагу кожного переданого запису від випадкового значення, зупиняючись, коли випадкове значення менше ваги поточних предметів (або при відніманні ваги складає значення <0)
Лунін

2
@ BlueRaja-DannyPflughoeft передчасна оптимізація ... питання полягало у виборі об'єкта з відкритого коробки лут. Хто збирається відкрити 1000 коробок в секунду?
ІМіль

4
@IMil: Ні, питання є загальним для вибору випадкових зважених предметів . Що стосується певних службових скриньок, то ця відповідь, ймовірно, добре, оскільки є невелика кількість елементів, і ймовірності не змінюються (хоча, як правило, це робиться на сервері, 1000 / сек нереально для популярної гри) .
BlueRaja - Danny Pflughoeft

4
@opa, тоді прапор закриється як дуп. Чи насправді неправильно підтримувати хорошу відповідь лише тому, що питання було задано раніше?
Балдрікк

27

Примітка. Я створив бібліотеку C # для цієї точної проблеми

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

Ось два найпоширеніших рішення (обидва вони включені до вищезгаданої бібліотеки)

Метод псевдоніма Уокера

Розумне рішення, яке надзвичайно швидко ( O(1)!), Якщо ваші ймовірності постійні. По суті, алгоритм створює 2D дартсборд ("таблицю псевдонімів") з ваших імовірностей і кидає на нього дротик.

Дартс

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

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

Рішення на основі дерева

Іншим поширеним рішенням є створення масиву, де кожен елемент зберігає суму його ймовірності та всіх елементів перед ним. Тоді просто генеруйте випадкове число з [0,1) та виконайте двійковий пошук місця, де ця кількість знаходиться у списку.

Це рішення дуже легко кодувати / зрозуміти, але вибір виходить повільніше, ніж метод псевдоніму Уокера, і зміна ймовірностей все ж є O(n). Ми можемо вдосконалити його, перетворивши масив у дерево бінарного пошуку, де кожен вузол відстежує суму ймовірностей у всіх елементах свого піддерева. Тоді, коли ми формуємо число з [0,1), ми можемо просто спуститися по дереву, щоб знайти предмет, який він представляє.

Це дає нам O(log n)можливість вибрати предмет і змінити ймовірності! Це робить NextWithRemoval()надзвичайно швидко!

Результати

Ось декілька швидких орієнтирів з вищевказаної бібліотеки, порівнюючи ці два підходи

         Показники зваженого рандомізера | Дерево | Таблиця
-------------------------------------------------- ---------------------------------
Додати () x10000 + NextWithReplacement () x10: | 4 мс | 2 мс
Додати () x10000 + NextWithReplacement () x10000: | 7 мс | 4 мс
Додати () x10000 + NextWithReplacement () x100000: | 35 мс | 28 мс
(Додати () + NextWithReplacement ()) x10000 (перемежовано) | 8 мс | 5403 мс
Додати () x10000 + NextWithRemoval () x10000: | 10 мс | 5948 мс

Отже, як ви бачите, для особливого випадку статичних (незмінних) ймовірностей метод Alias ​​Уокера на 50-100% швидший. Але в більш динамічних випадках дерево на кілька порядків швидше !


Рішення на основі дерева також дає нам гідний час ( nlog(n)) при сортуванні елементів за вагою.
Натан Меррілл

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

Який файл містить рішення на основі дерева? По-друге, ваша орієнтирова таблиця: чи псевдонім Уокера - це стовпець "таблиця"?
Якк

1
@Yakk: Код для рішення на основі дерева знаходиться тут . Вона побудована на реалізацію з відкритим вихідним кодом в якості AA-дерева . І "так" на ваше друге питання.
BlueRaja - Danny Pflughoeft

1
Частина Walker - це лише лише посилання.
Нагромадження

17

Рішення «Колесо фортуни»

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

Створіть масив параметрів. Але введіть у нього кожен елемент кілька разів, при цьому кількість дублікатів кожного елемента пропорційна його шансу з’явитися. У наведеному вище прикладі всі елементи мають ймовірності, що є множниками 5%, тому ви можете створити масив з 20 елементів на зразок цього:

10 gold
sword
sword
sword
sword
shield
shield
shield
shield
shield
shield
shield
armor
armor
armor
armor
potion
potion

Потім просто виберіть випадковий елемент цього списку, генеруючи одне випадкове ціле число між 0 і довжиною масиву - 1.

Недоліки:

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

Переваги:

  • Коли ви вже маєте масив і хочете отримати з нього кілька разів, то це дуже швидко. Лише одне випадкове ціле число та один доступ до масиву.

3
Як гібридне рішення, щоб уникнути другого недоліку, ви можете позначити останній слот як "інший" та обробити його за допомогою інших засобів, таких як підхід до масиву Філіпа. Таким чином, ви можете заповнити цей останній слот масивом, що пропонує 99,9% шансу зілля і лише 0,1% шансу Epic Scepter of the Apocalypse. Такий дворівневий підхід використовує переваги обох підходів.
Корт Аммон - Відновіть Моніку

1
Я дещо змінюю це у власному проекті. Що я роблю, - це обчислити кожен елемент та вагу та зберігати їх у масиві, [('gold', 1),('sword',4),...]підсумовувати всі ваги, а потім викочувати випадкове число від 0 до суми, потім ітератувати масив та обчислювати, де розташовується випадкове число (тобто а reduce). Відмінно працює для масивів, які оновлюються часто, і немає великих завитків пам'яті.

1
@Thebluefish Це рішення описано в іншій моїй відповіді "Програмне рішення з можливістю м'якого кодування"
Філіпп,

7

Рішення твердо закодованих ймовірностей

Найпростіший спосіб знайти випадковий предмет із зваженої колекції - пройти по ланцюжку тверджень if-else, де кожен if-else напевно збільшується, оскільки попередній не потрапляє.

int rand = random(100); //Random number between 1 and 100 (inclusive)
if(rand <= 5) //5% chance
{
    print("You found 10 gold!");
}
else if(rand <= 25) //20% chance
{
    print("You found a sword!");
}
else if(rand <= 70) //45% chance
{
    print("You found a shield!");
}
else if(rand <= 90) //20% chance
{
    print("You found armor!");
}
else //10% chance
{
    print("You found a potion!");
}

Причина, що умовні умови дорівнюють її шансу плюс усі попередні умовні умови, полягає в тому, що попередні умови вже виключили можливість того, що вони є тими предметами. Отже, для умовного щита else if(rand <= 70)70 дорівнює шансу 45% щита плюс 5% шансу на золото та 20% шансу меча.

Переваги:

  • Легко програмувати, оскільки не потребує структур даних.

Недоліки:

  • Важко в обслуговуванні, тому що вам потрібно підтримувати свої показники падіння в коді. Ви не можете визначити їх під час виконання. Тож якщо ви хочете чогось більшого підтвердження у майбутньому, слід перевірити інші відповіді.

3
Це було б справді прикро. Наприклад, якщо ви хочете вийняти золото і зробити зілля займає своє місце, вам потрібно відкоригувати ймовірність усіх предметів між ними.
Олександр - Відновіть Моніку

1
Щоб уникнути проблеми, про яку згадує @Alexander, ви можете замість цього відняти поточну швидкість на кожному кроці, а не додавати її до кожної умови.
AlexanderJ93

2

У C # ви можете використати сканування Linq, щоб запустити свій акумулятор, щоб перевірити випадкове число в діапазоні від 0 до 100,0f та. Перший () для отримання. Так, як один рядок коду.

Тож щось на кшталт:

var item = a.Select(x =>
{
    sum += x.prob;
    if (rand < sum)
        return x.item;
    else
        return null;
 }).FirstOrDefault());

sumє нульовим ініціалізованим цілим числом і aє списком проблем / елементів структур / кортежів / екземплярів. randявляє собою попередньо сформоване випадкове число в діапазоні.

Це просто накопичує суму над списком діапазонів, поки воно не перевищить раніше вибране випадкове число, і повертає або пункт, або нуль, де null буде повернуто, якщо діапазон випадкових чисел (наприклад, 100) менший за загальний діапазон зважування помилково , а вибране випадкове число знаходиться поза загальним діапазоном зважування.

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

Також приклад тут .

Іншим прикладом є те, що ви можете взяти випадкове значення від 90 градусів до 180 градусів, щоб отримати нижній правий квадрант кола, взяти компонент x за допомогою cos (r) і використовувати його для індексації у пріоритетний список.

За допомогою різних формул у вас може бути загальний підхід, коли ви просто вводите пріоритетний список будь-якої довжини (наприклад, N) і відображаєте результат формули (наприклад: cos (x) - 0 до 1) шляхом множення (наприклад: Ncos (x) ) = 0 до N), щоб отримати індекс.


3
Чи можете ви дати нам цей рядок коду, якщо це лише один рядок? Я не так знайомий з C #, тому я не знаю, що ти маєш на увазі.
HEGX64

@ HEGX64 додано, але використання мобільного та редактора не працює. Чи можете ви редагувати?
Sentinel

4
Чи можете ви змінити цю відповідь, щоб пояснити концепцію, що стоїть за нею, а не конкретну імплементацію певною мовою?
Раймунд Кремер

@ RaimundKrämer Erm, зробили?
Sentinel

Downvote без пояснень = марний та антисоціальний.
WGroleau

1

Ймовірності не потрібно чітко кодувати. Елементи та пороги можуть бути разом у масиві.

for X in itemsrange loop
  If items (X).threshold < random() then
     Announce (items(X).name)
     Exit loop
  End if
End loop

Вам доведеться все-таки накопичувати пороги, але ви можете це робити, створюючи файл параметрів, а не кодувати його.


3
Не могли б ви детальніше розглянути, як обчислити правильний поріг? Наприклад, якщо у вас є три елементи з шансом 33% кожен, як би ви створили цю таблицю? Оскільки кожен випадковий () генерується щоразу, першому знадобиться 0,3333, другому потрібно 0,5, а останньому 1,0. Або я неправильно прочитав алгоритм?
труба

Ви обчислюєте це так, як робили інші у своїх відповідях. Для рівних ймовірностей елементів X, перший поріг - 1 / X, другий, 2 / X тощо
WGroleau

При цьому для 3-х елементів цього алгоритму буде встановлено пороги 1/3, 2/3 та 3/3, але ймовірність результатів 1/3, 4/9 та 2/9 для першого, другого та третього пункту. Ви дійсно хочете мати виклик random()у циклі?
труба

Ні, це безумовно помилка. Кожна перевірка потребує однакового випадкового числа.
WGroleau

0

Я виконав цю функцію: https://github.com/thewheelmaker/GDscript_Weighted_Random Now! у вашому випадку ви можете використовувати його так:

on_normal_case([5,20,45,20,10],0)

Він дає лише число від 0 до 4, але ви можете помістити його в масив, де ви отримали предмети.

item_array[on_normal_case([5,20,45,20,10],0)]

Або у функції:

item_function(on_normal_case([5,20,45,20,10],0))

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

func on_normal_case(arrayy,transformm):
    var random_num=0
    var sum=0
    var summatut=0
    #func sumarrays_inarray(array):
    for i in range(arrayy.size()):
        sum=sum+arrayy[i]
#func no_fixu_random_num(here_range,start_from):
    random_num=randi()%sum+1
#Randomies be pressed down
#first start from zero
    if 0<=random_num and random_num<=arrayy[0]:
        #print(random_num)
        #print(array[0])
        return 0+ transformm
    summatut=summatut+arrayy[0]
    for i in range(arrayy.size()-1):
        #they must pluss together
        #if array[i]<=random_num and random_num<array[i+1]:
        if summatut<random_num and random_num<=summatut+arrayy[i+1]:
            #return i+1+transform
            #print(random_num)
            #print(summatut)
            return i+1+ transformm

        summatut=summatut+arrayy[i+1]
    pass

Це працює так: on_normal_case ([50,50], 0) Це дає 0 або 1, він має однакову імовірність.

on_normal_case ([50,50], 1) Це дає 1 або 2, це однакова ймовірність і для обох.

on_normal_case ([20,80], 1) Це дає 1 або 2, він має більші зміни, щоб отримати два.

on_normal_case ([20,80,20,20,30], 1) Це дає діапазон випадкових чисел 1-5, і більші числа швидше, ніж менші числа.

on_normal_case ([20,80,0,0,20,20,30,0,0,0,0,33], 45) Цей кидок кісток між числами 45,46,49,50,51,56 ви бачите, коли є це нуль, воно ніколи не виникає.

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

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