Взвешенный рандомизированный порядок

Проблема:

У меня есть предметы, которые имеют вес. Чем выше вес, тем больше шансов, что предмет попадет первым. Мне нужен чистый и простой способ сделать это, основанный на базовой Java (без сторонних библиотек, jar-файлов и т. д.).

Я сделал это для 2 предметов, суммируя веса, а затем случайным образом выбирая число, используя Math.random() в пределах этого диапазона. Очень простой. Но для элементов, превышающих 2, я могу либо сделать больше выборок в том же диапазоне с вероятностью промахов, либо я могу пересчитать сумму весов оставшихся элементов и выбрать снова (рекурсивный подход). Я думаю, что может быть что-то, что может сделать это быстрее/чище. Этот код будет использоваться снова и снова, поэтому я ищу эффективное решение.

По сути, это как рандомизированные перестановки веса.

Некоторые примеры:

  1. A имеет вес 1, B имеет вес 99. Если бы я запускал симуляцию с этим, я ожидал бы получить BA 99% времени и AB 1% времени.

  2. A имеет вес 10, B имеет вес 10, а C имеет вес 80. Если бы я запускал симуляции с этим, я бы ожидал, что C будет первым элементом в заказе в 80% случаев, в этих случаях , A и B будут иметь равные шансы стать следующим персонажем.

Дополнительные сведения:

Для моей конкретной проблемы существует небольшое количество предметов с потенциально большими весами. Скажем, от 20 до 50 элементов с весами, которые хранятся в виде длинного числа, где минимальный вес составляет не менее 1000. Количество элементов также может немного увеличиться, поэтому, если мы сможем найти решение, которое не требует предметы должны быть маленькими, что было бы предпочтительнее.


person James Oravec    schedule 31.05.2014    source источник
comment
Если вы надеетесь улучшить существующее решение, возможно, вы могли бы опубликовать его на codereview.stackexchange.com?   -  person PakkuDon    schedule 31.05.2014
comment
Код не был написан для случаев, когда у меня более 2 элементов. Просто придумал пару решений, которые могут работать. Я хотел бы посмотреть, есть ли у кого-нибудь опыт, который делает эту проблему очень простой. Если нет, я, вероятно, выберу рекурсивное решение...   -  person James Oravec    schedule 31.05.2014
comment
Ограничен ли вес от 1 до 100 (или другим способом)?   -  person KonradOliwer    schedule 31.05.2014
comment
Для моего случая есть небольшое количество предметов с потенциально большими весами. Скажем, от 20 до 50 предметов с весами, которые хранятся в виде лонгов, где минимальный вес составляет не менее 1000.   -  person James Oravec    schedule 01.06.2014


Ответы (4)


Кажется, это работает нормально:

// Can do a weighted sort on weighted items.
public interface Weighted {
    int getWeight();
}

/**
 * Weighted sort of an array - orders them at random but the weight of each
 * item makes it more likely to be earlier.
 *
 * @param values
 */
public static void weightedSort(Weighted[] values) {
    // Build a list containing as many of each item to make up the full weight.
    List<Weighted> full = new ArrayList<>();
    for (Weighted v : values) {
        // Add a v weight times.
        for (int i = 0; i < v.getWeight(); i++) {
            full.add(v);
        }
    }
    // Shuffle it.
    Collections.shuffle(full);
    // Roll them out in the order required.
    int i = 0;
    do {
        // Get the first one in the shuffled list.
        Weighted next = full.get(0);
        // Put it back into the array.
        values[i++] = next;
        // Remove all occurrences of that one from the list.
        full.remove(next);
    } while (!full.isEmpty());
}

// A bunch of weighted items.
enum Heavies implements Weighted {

    Rare(1),
    Few(3),
    Common(6);
    final int weight;

    Heavies(int weight) {
        this.weight = weight;
    }

    @Override
    public int getWeight() {
        return weight;
    }
}

public void test() {
    Weighted[] w = Heavies.values();
    for (int i = 0; i < 10; i++) {
        // Sort it weighted.
        weightedSort(w);
        // What did we get.
        System.out.println(Arrays.toString(w));
    }
}

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

Последний тестовый запуск дал:

[Rare, Common, Few]
[Common, Rare, Few]
[Few, Common, Rare]
[Common, Few, Rare]
[Common, Rare, Few]
[Few, Rare, Common]

что кажется правильным.

NB — этот алгоритм не сработает, по крайней мере, при следующих условиях:

  1. В исходном массиве один и тот же объект содержится более одного раза.
  2. Вес предметов безумно велик.
  3. Нулевые или отрицательные веса почти наверняка испортят результаты.

Добавлен

Это реализует идею Россума - пожалуйста, не забудьте отдать ему должное за алгоритм.

public static void weightedSort2(Weighted[] values) {
    // Calculate the total weight.
    int total = 0;
    for (Weighted v : values) {
        total += v.getWeight();
    }
    // Start with all of them.
    List<Weighted> remaining = new ArrayList(Arrays.asList(values));
    // Take each at random - weighted by it's weight.
    int which = 0;
    do {
        // Pick a random point.
        int random = (int) (Math.random() * total);
        // Pick one from the list.
        Weighted picked = null;
        int pos = 0;
        for (Weighted v : remaining) {
            // Pick this ne?
            if (pos + v.getWeight() > random) {
                picked = v;
                break;
            }
            // Move forward by that much.
            pos += v.getWeight();
        }
        // Removed picked from the remaining.
        remaining.remove(picked);
        // Reduce total.
        total -= picked.getWeight();
        // Record picked.
        values[which++] = picked;
    } while (!remaining.isEmpty());
}
person OldCurmudgeon    schedule 31.05.2014
comment
Это очень мило, это похоже на то, о чем я думал. Проблема, с которой я сталкиваюсь, заключается в том, что мое приложение начинается с веса 1000 и становится только больше (веса хранятся в длинном), поэтому создание приоритета для каждого веса или токена было бы излишним для моего предполагаемого использования. Однако количество предметов, которые я сравниваю, невелико, скажем, 20 предметов с весами. Это может быть полезно :) - person James Oravec; 01.06.2014
comment
@VenomFangs - Вы можете сначала пройтись по весам и учесть их, разделив на их HCF - может ли это уменьшить ваши числа? решение rossum не имеет этой проблемы. - person OldCurmudgeon; 01.06.2014
comment
Спасибо за предложение. У меня есть опасения по поводу HCF, хотя, как и в случае с несколькими элементами, не может быть ни одного больше 1, особенно если в списке есть простое число. Это также создаст много дополнительных вычислений... Я дам ему еще немного времени, если я не получу что-то проще/чище, я соглашусь с решением Россум... - person James Oravec; 01.06.2014
comment
Не знаю, кто дал мне -1, но если вы используете флаги для маркировки элементов на месте вместо нового ArrayList(Arrays.asList(values)) и rest.remove(picked), это будет более эффективно. Управление памятью и GC не бесплатны. - person ggurov; 04.06.2014

У вас есть предметы с весами:

  • Деталь А, вес 42
  • Деталь Б, вес 5
  • Изделие С, вес 96
  • Деталь D, вес 33

Сначала сложите все веса: 42 + 5 + 96 + 33 = 176.

Теперь выберите случайное число r от 0 до суммы весов: 0 ‹= r ‹ 176. Я использовал целые числа, но при необходимости вы можете использовать вещественные числа.

Сравните r с диапазонами, определяемыми весами:

  • 0 ‹= r ‹ 42: выберите пункт A.
  • 42 ‹= r ‹ 47 (= 42 + 5): выберите пункт B.
  • 47 ‹= r ‹ 143 (= 47 + 96): выберите пункт C.
  • 143 ‹= r ‹ 176 (= 143 + 33): выберите пункт D.

Когда вы выбрали первый предмет, повторите процесс с тремя оставшимися предметами и уменьшенной суммой всех весов. Продолжайте повторять, пока не останется предметов для выбора.

person rossum    schedule 31.05.2014
comment
Это рекурсивный подход, о котором я упоминал;) - person James Oravec; 31.05.2014

public class RandomPriorityQueue {

    private TreeMap<Integer, List<WeightedElement>> tree = new TreeMap();
    private Random random = new Random();

    public void add(WeightedElement e) {
        int priority = random.nextInt(e.getWeight());
        if (tree.containsKey(priority)) {
            List<WeightedElement> list = new LinkedList();
            list.add(e);
            tree.put(priority, list);
        } else {
            List<WeightedElement> list = tree.get(priority);
            list.add(random.nextInt(list.size()), e);
        }
    }

    public WeightedElement poll() {
        Map.Entry<Integer, List<WeightedElement>> entry = tree.lastEntry();
        if (entry == null){
            return null;
        }
        List<WeightedElement> list = entry.getValue();
        if (list.size() == 1){
            tree.remove(entry.getKey());
        }
        return list.remove(0);
    }
}

Конечно, у нас была бы лучшая производительность, если бы мы переписали TreeMap, чтобы мы могли добавлять ключи дублирования, у нас была бы лучшая производительность.

person KonradOliwer    schedule 31.05.2014
comment
Ваше решение выглядит как приоритетная очередь, в которой используются случайные числа. Исходя из этого, это не решит указанную проблему. - person James Oravec; 01.06.2014
comment
В какой момент это на самом деле не решает вашу проблему. Это удовлетворит ваш образец данных. Проблема в том, что это очередь, или что-то еще? Если это проблема, возможно, вы могли бы указать, что вы хотите сделать, когда у вас будет этот упорядоченный набор элементов. - person KonradOliwer; 01.06.2014
comment
Пара ошибок, которые вы могли бы исправить... 1) Ваша строка if (tree.containsKey(priority)) нуждается в ! в начале. 2) После этого вы можете запускать свой код много раз. Используйте A с весом 1000 и B с весом 9000. Вы ожидаете 10% как победу для A, но когда вы запускаете большие симуляции (скажем, размером 10000000), вы увидите, что получаете результат, близкий к 5,57% вместо ожидаемого. 10%. Это связано с тем, как вы создаете случайные приоритеты, возможно, вы сможете это исправить, если вы можете, я хотел бы увидеть обновление и, возможно, использовать его. :) - person James Oravec; 01.06.2014

В любом случае, для N элементов вам понадобится N-1 случайных чисел (как минимум). Затем давайте подумаем об эффективном способе выбора элемента по случайному номеру.

Если элементов не слишком много, я бы использовал итеративный метод, аналогичный вашему рекурсивному подходу. Я бы добавил логический флаг к элементам для пропуска выбранных в предыдущих итерациях. Когда я выбираю один из них в текущей итерации, я устанавливаю его флаг в значение true, и в следующий раз я пропускаю его из вычислений. Вычтите его вес из суммы и перейдите к следующей итерации.

Если предметов много и один и тот же набор будет использоваться много раз, то лучше использовать другой подход. Составьте из них отсортированный список и используйте копию этого списка в своем рекурсивном подходе. И на каждом шаге рекурсии - бинарный поиск в нем, потом удаление выбранного элемента.

Собственно, последнее тоже можно делать итеративно.

person ggurov    schedule 31.05.2014