отримати зважений випадковий предмет


51

У мене, наприклад, ця таблиця

+ ----------------- +
| фрукти | вага |
+ ----------------- +
| яблуко | 4 |
| помаранчевий | 2 |
| лимон | 1 |
+ ----------------- +

Мені потрібно повернути випадковий плід. Але яблуко слід збирати в 4 рази частіше, ніж лимон і в 2 рази частіше, ніж апельсин .

У більш загальному випадку це має бути f(weight)часто.

Який хороший загальний алгоритм реалізації такої поведінки?

А може, на Рубі є якісь готові дорогоцінні камені? :)

PS
Я реалізував поточний алгоритм в Ruby https://github.com/fl00r/pickup


11
це має бути та сама формула отримання випадкового лута в Diablo :-)
Jalayn

1
@Jalayn: Власне, ідея рішення інтервалу у моїй відповіді нижче випливає з того, що я пам’ятаю про бойові таблиці в World of Warcraft. :-D
Бенджамін Клостер



Я реалізував кілька простих зважених випадкових алгоритмів . Повідомте мене, якщо у вас є питання.
Леонід Ганелін

Відповіді:


50

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

fruits = [apple, apple, apple, apple, orange, orange, lemon]

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


Інший, трохи складніший підхід виглядав би так:

  1. Обчисліть сукупні суми ваг:

    intervals = [4, 6, 7]

    Якщо індекс нижче 4 позначає яблуко , 4 - нижче 6 - апельсин і 6 - нижче 7 - лимон .

  2. Створити випадкове число nв діапазоні 0до sum(weights).

  3. Знайдіть останній предмет, сумарна сума якого вище n. Відповідний плід - ваш результат.

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

Для будь-якого алгоритму етап налаштування можна зробити один раз для довільної кількості випадкових виборів.


2
рішення інтервалу здається приємним
Джалайн

1
Це була моя перша думка :). Але що робити, якщо у мене стіл зі 100 фруктами, а вага може бути близько 10 к? Це буде дуже великий масив, і це буде не так ефективно, як я хочу. Йдеться про перше рішення. Друге рішення виглядає добре
fl00r

1
Я реалізував цей алгоритм у Ruby github.com/fl00r/pickup
fl00r

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

30

Ось алгоритм (на C #), який може вибрати випадковий зважений елемент з будь-якої послідовності, лише повторивши його через один раз:

public static T Random<T>(this IEnumerable<T> enumerable, Func<T, int> weightFunc)
{
    int totalWeight = 0; // this stores sum of weights of all elements before current
    T selected = default(T); // currently selected element
    foreach (var data in enumerable)
    {
        int weight = weightFunc(data); // weight of current element
        int r = Random.Next(totalWeight + weight); // random value
        if (r >= totalWeight) // probability of this is weight/(totalWeight+weight)
            selected = data; // it is the probability of discarding last selected element and selecting current one instead
        totalWeight += weight; // increase weight sum
    }

    return selected; // when iterations end, selected is some element of sequence. 
}

Це ґрунтується на наступних міркуваннях: виберемо перший елемент нашої послідовності як "поточний результат"; потім, для кожної ітерації, зберігайте або відкидайте та вибирайте новий елемент як поточний. Ми можемо обчислити ймовірність того, що будь-який даний елемент буде обраний врешті-решт як добуток усіх ймовірностей, що його не буде відкинуто в наступних кроках, в рази ймовірність того, що він буде обраний в першу чергу. Якщо ви займаєтесь математикою, ви побачите, що цей продукт спрощує до (вага елемента) / (сума всіх ваг), що саме те, що нам потрібно!

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


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

1
@ Жан-Бернар Пеллерін Я це зробив, і це фактично швидше у великих списках. Якщо ви не використовуєте криптографічно сильний випадковий генератор (-8
Nevermind

Повинна бути прийнята відповідь imo. Мені це подобається краще, ніж підхід "інтервал" та "повторний вхід".
Vivin Paliath

2
Мені просто хотілося сказати, що я повернувся до цієї теми 3 або 4 рази за останні пару років, щоб використовувати цей метод. Цей метод неодноразово вдавав відповіді, які мені потрібні досить швидко для моїх цілей. Я б хотів, щоб я міг підтримати цю відповідь кожного разу, коли я повернувся, щоб використати її.
Джим Ярбро

1
Приємне рішення, якщо ви хочете вибрати лише один раз. В іншому випадку виконання попередньої роботи для рішення в першій відповіді один раз набагато ефективніше.
Дедуплікатор

22

Вже присутні відповіді хороші, і я трохи розгорну їх.

Як запропонував Бенджамін, у таких проблемах зазвичай використовуються накопичені суми:

+------------------------+
| fruit  | weight | csum |
+------------------------+
| apple  |   4    |   4  |
| orange |   2    |   6  |
| lemon  |   1    |   7  |
+------------------------+

Щоб знайти предмет у цій структурі, ви можете використовувати щось на зразок коду Nevermind. Цей фрагмент коду C #, який я зазвичай використовую:

double r = Random.Next() * totalSum;
for(int i = 0; i < fruit.Count; i++)
{
    if (csum[i] > r)
        return fruit[i];
}

Тепер до цікавої частини. Наскільки ефективний такий підхід і яке найефективніше рішення? Мій фрагмент коду вимагає O (n) пам’яті та працює в O (n) час. Я не думаю, що це можна зробити з меншим, ніж O (n) простором, але складність у часі може бути набагато нижчою, O (log n) насправді. Хитрість полягає у використанні двійкового пошуку замість регулярного циклу.

double r = Random.Next() * totalSum;
int lowGuess = 0;
int highGuess = fruit.Count - 1;

while (highGuess >= lowGuess)
{
    int guess = (lowGuess + highGuess) / 2;
    if ( csum[guess] < r)
        lowGuess = guess + 1;
    else if ( csum[guess] - weight[guess] > r)
        highGuess = guess - 1;
    else
        return fruit[guess];
}

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


Гарний момент про двійковий пошук
fl00r

Відповідь Nevermind не потребує додаткового місця, тому це O (1), але додає складності виконанню, багаторазово генеруючи випадкові числа та оцінюючи функцію ваги (що залежно від основної проблеми може бути дорогим).
Бенджамін Клостер

1
Те, що ви стверджуєте, що є "більш читаною версією" мого коду, насправді ні. Ваш код повинен заздалегідь знати загальну суму ваг та сукупні суми; мій ні.
забудьте

@Benjamin Kloster Мій код викликає функцію ваги лише один раз на елемент - ви не можете зробити нічого кращого за це. Ти маєш рацію щодо випадкових чисел.
забудьте

@Nevermind: Ви викликаєте його лише один раз за виклик функції pick, тому якщо користувач викликає його двічі, функція ваги знову викликається для кожного елемента. Звичайно, ви можете це зробити кеш, але тоді ви вже не O (1) для складності простору.
Бенджамін Клостер

8

Це проста реалізація Python:

from random import random

def select(container, weights):
    total_weight = float(sum(weights))
    rel_weight = [w / total_weight for w in weights]

    # Probability for each element
    probs = [sum(rel_weight[:i + 1]) for i in range(len(rel_weight))]

    slot = random()
    for (i, element) in enumerate(container):
        if slot <= probs[i]:
            break

    return element

і

population = ['apple','orange','lemon']
weights = [4, 2, 1]

print select(population, weights)

У генетичних алгоритмах ця процедура вибору називається пропорційним вибором фітнесу або вибором колеса рулетки, оскільки:

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

Вибір колеса рулетки

Типові алгоритми мають складність O (N) або O (log N), але ви також можете виконати O (1) (наприклад, вибір колеса рулетки шляхом стохастичного прийняття ).


Чи знаєте ви, що таке оригінальне джерело для цього зображення? Я хочу використовувати його для паперу, але мені потрібно переконатися в приписуванні.
Малькольм Маклауд

@MalcolmMacLeod Вибачте, він використовується у багатьох документах / сайтах GA, але я не знаю, хто автор.
manlio

0

Ця суть робить саме те, що ви просите.

public static Random random = new Random(DateTime.Now.Millisecond);
public int chooseWithChance(params int[] args)
    {
        /*
         * This method takes number of chances and randomly chooses
         * one of them considering their chance to be choosen.    
         * e.g. 
         *   chooseWithChance(0,99) will most probably (%99) return 1
         *   chooseWithChance(99,1) will most probably (%99) return 0
         *   chooseWithChance(0,100) will always return 1.
         *   chooseWithChance(100,0) will always return 0.
         *   chooseWithChance(67,0) will always return 0.
         */
        int argCount = args.Length;
        int sumOfChances = 0;

        for (int i = 0; i < argCount; i++) {
            sumOfChances += args[i];
        }

        double randomDouble = random.NextDouble() * sumOfChances;

        while (sumOfChances > randomDouble)
        {
            sumOfChances -= args[argCount -1];
            argCount--;
        }

        return argCount-1;
    }

Ви можете використовувати його так:

string[] fruits = new string[] { "apple", "orange", "lemon" };
int choosenOne = chooseWithChance(98,1,1);
Console.WriteLine(fruits[choosenOne]);

Наведений вище код, швидше за все, (% 98) поверне 0, що є індексом 'apple' для даного масиву.

Також цей код перевіряє метод, передбачений вище:

Console.WriteLine("Start...");
int flipCount = 100;
int headCount = 0;
int tailsCount = 0;

for (int i=0; i< flipCount; i++) {
    if (chooseWithChance(50,50) == 0)
        headCount++;
    else
        tailsCount++;
}

Console.WriteLine("Head count:"+ headCount);
Console.WriteLine("Tails count:"+ tailsCount);

Це дає щось таке:

Start...
Head count:52
Tails count:48

2
Програмісти це про концептуальних питаннях і відповідях , як очікується , пояснити речі. Закидання скидів коду замість пояснення - це як копіювання коду з IDE на дошку: це може здатися знайомим і навіть часом бути зрозумілим, але це виглядає дивно ... просто дивно. У дошці немає компілятора
gnat

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