Отрегулируйте элементы, которые можно выбрать из списка

У меня есть список элементов. Когда я создаю список, каждый элемент имеет равные шансы на выбор. Но по мере того как элемент выбран, его шансы снижаются, в то время как другие шансы повышаются. Если во время процесса добавляется новый элемент, он должен иметь максимальную возможность выбора, при этом его шансы падают по мере его выбора. Я ищу хороший алгоритм, который может выполнить это С#.

Обобщенная идея: У меня есть 5 предметов, со временем все 5 предметов будут выбраны 20% времени. Я стараюсь, чтобы выборы были близки к 20%, насколько это возможно, сократившись на outliers. Если он существует, он будет выбран больше/меньше, чтобы вернуть его в линию.

Ответ 1

Здесь мы создадим генератор случайных чисел, который имеет распределение, которое благоприятствует низким значениям. Вы можете использовать его для предпочтения элементов в начале списка. Чтобы уменьшить вероятность выбора выбранного объекта, переместите этот элемент вниз по списку. У вас есть несколько вариантов того, как вы хотите переместить элемент в списке. Давайте сначала рассмотрим преобразование случайных величин.

Применяя следующую функцию к равномерной случайной величине между 0 и 1:

index = Int(l*(1-r^(0.5)) # l=length, r=uniform random var between 0 and 1

Вы получаете классное распространение, которое резко снижает вероятность большего индекса

p(0)=0.09751
p(1)=0.09246
p(2)=0.08769
p(3)=0.08211
p(4)=0.07636
p(5)=0.07325
p(6)=0.06772
p(7)=0.06309
p(8)=0.05813
p(9)=0.05274
p(10)=0.04808
p(11)=0.04205
p(12)=0.03691
p(13)=0.03268
p(14)=0.02708
p(15)=0.02292
p(16)=0.01727
p(17)=0.01211
p(18)=0.00736
p(19)=0.00249

Вот список для списка размером 2

0.75139
0.24862

Размер 3

0.55699
0.33306
0.10996

Размер 4

0.43916
0.31018
0.18836
0.06231

Теперь давайте обсудим два варианта перемещения элементов вниз по списку. Я проверил два:

  • ToEnd - перенос последнего выбранного элемента в конец списка

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

Я создал симуляцию для выбора из списка и рассмотрю стандартное отклонение подсчета, которое выбрали каждый элемент. Чем ниже стандартное отклонение, тем лучше. Например, 1 симуляция для списка из 10 элементов, где 50 выборок, где сделано, создали спред:

{"a"=>5, "b"=>5, "c"=>6, "d"=>5, "e"=>4, "f"=>4, "g"=>5, "h"=>5, "i"=>6, "j"=>5}

Стандартное отклонение для этого моделирования было

0.63

С возможностью запуска моделирования я затем вычислил некоторую мета-статистику, выполнив симуляцию 500 раз и обеспечив среднее стандартное отклонение для каждого метода: ToEnd и Sort. Я ожидал, что стандартное отклонение будет высоким при низких # выборках, но на самом деле для алгоритма ToEnd стандартное отклонение увеличилось с количеством выборков. Метод сортировки исправил это.

Testing ["a", "b", "c", "d", "e"]
-------------------------
Picks    ToEnd (StdDev) Sort (StdDev)
5       0.59        0.57
10      0.76        0.68
15      0.93        0.74
20      1.04        0.74
25      1.20        0.73
30      1.28        0.73
35      1.34        0.74
40      1.50        0.75
45      1.53        0.75
45      1.56        0.77
80      2.04        0.79
125     2.52        0.74
180     3.11        0.77
245     3.53        0.79
320     4.05        0.75
405     4.52        0.76
500     5.00        0.78

Ниже приведены некоторые результаты теста для большего набора.

Testing ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
-------------------------
Picks  ToEnd (StdDev)  Sort (StdDev)
10          0.68        0.65
20          0.87        0.77
30          1.04        0.80
40          1.18        0.82
50          1.30        0.85
60          1.43        0.84
70          1.57        0.87
80          1.65        0.88
90          1.73        0.87
90          1.71        0.87
160         2.30        0.89
250         2.83        0.88
360         3.44        0.88
490         3.89        0.88
640         4.54        0.87
810         5.12        0.88
1000        5.66        0.85

С хорошей тестовой структурой я не мог удержаться от попыток преобразования случайных чисел. Мое предположение заключалось в том, что если я возьму корень куба вместо квадратного корня из x, мое стандартное отклонение уменьшится. Это было действительно, но я был обеспокоен тем, что это уменьшит случайность. Здесь вы можете наблюдать несколько симуляций при изменении формулы на

index = Int(l*(1-r^(0.33)) # l=length, r=uniform random var between 0 and 1

Теперь проверьте фактические выборки. Как я думал, он очень взвешен до начала списка. Если вы хотите весить это тяжело, вы должны, вероятно, рандомизировать свой список, прежде чем начать.

StdDev = 0.632455532033676
{"a"=>10, "b"=>10, "c"=>11, "d"=>9, "e"=>10}
a d e c b c e b a d b e c a d d e b a e e c c b d a d c b c e b a a d d b e a e a b c b d c a c e c

StdDev = 0.0
{"a"=>10, "b"=>10, "c"=>10, "d"=>10, "e"=>10}
b c d a a d b c b a d e c d e c b e b a e e d c c a b a d a e e b d b a e c c e b a c c d d d a b e

StdDev = 0.632455532033676
{"a"=>9, "b"=>10, "c"=>10, "d"=>10, "e"=>11}
b d a e b c a d c e e b a c a d d c b c e d a e b b a c d c d a e a e e b d c b e a b c b c d d e e

StdDev = 0.0
{"a"=>10, "b"=>10, "c"=>10, "d"=>10, "e"=>10}
a b e d c e a b d c b c c a d b e e b e a d d c e a d b b c c a a a b e d d e c c a b b e a c d e d

StdDev = 0.632455532033676
{"a"=>11, "b"=>10, "c"=>9, "d"=>10, "e"=>10}
b a d c d c a e b e a e b c d b c a a d e e d c d e c b a b b e d c d b c e a a a d b c e b e a d a

Ответ 2

Использовать взвешенную очередь в корзине: Вместо того, чтобы использовать список, разделите свою коллекцию на ведра - каждый ведро имеет связанную частоту извлечения. Элементы перемещаются из высокочастотных ковшей на более низкие частоты по мере их выбора.

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

Алгоритм:

Изначально все элементы начинаются с того же самого (верхнего уровня).

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

При добавлении нового элемента добавьте его в самый верхний (наиболее часто используемый) ковш.

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

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

Реализация С# (первый разрез) общей ведомой взвешенной очереди:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Utility
{
    public class BucketWeightedQueue<T>
    {
        private int m_MaxFallOff;
        private readonly double m_FallOffRate;
        private readonly List<List<T>> m_Buckets;
        private readonly List<int> m_FallOffFactors;
        private readonly Random m_Rand = new Random();

        public BucketWeightedQueue(IEnumerable<T> items, double fallOffRate )
        {
            if( fallOffRate < 0 ) 
                throw new ArgumentOutOfRangeException("fallOffRate");

            m_MaxFallOff = 1;
            m_FallOffRate = fallOffRate;
            m_Buckets = new List<List<T>> { items.ToList() };
            m_FallOffFactors = new List<int> { m_MaxFallOff };
        }

        public T GetRandomItem()
        {
            var bucketIndex = GetRandomBucketIndex();
            var bucket = m_Buckets[bucketIndex];
            var itemIndex = m_Rand.Next( bucket.Count );
            var selectedItem = bucket[itemIndex];

            ReduceItemProbability( bucketIndex, itemIndex );
            return selectedItem;
        }

        private int GetRandomBucketIndex()
        {
            var randFallOffValue = m_Rand.Next( m_MaxFallOff );
            for (int i = 0; i < m_FallOffFactors.Count; i++ )
                if( m_FallOffFactors[i] <= randFallOffValue )
                    return i;
            return m_FallOffFactors[0];
        }

        private void ReduceItemProbability( int bucketIndex, int itemIndex )
        {
            if( m_FallOffRate <= 0 )
                return; // do nothing if there is no fall off rate...

            var item = m_Buckets[bucketIndex][itemIndex];
            m_Buckets[bucketIndex].RemoveAt( itemIndex );

            if( bucketIndex <= m_Buckets.Count )
            { 
                // create a new bucket...
                m_Buckets.Add( new List<T>() );
                m_MaxFallOff = (int)(m_FallOffFactors[bucketIndex] / m_FallOffRate);
                m_FallOffFactors.Add( m_MaxFallOff );
            }

            var nextBucket = m_Buckets[bucketIndex + 1];
            nextBucket.Add( item );

            if (m_Buckets[bucketIndex].Count == 0) // drop empty buckets
                m_Buckets.RemoveAt( bucketIndex );
        }
    }
}

Ответ 3

Все зависит от того, как вероятность выбора должна отличаться при выборе выбранного элемента.

Простым способом достижения этого является двойное рисование, в котором вы начинаете с списка ввода (который никогда не изменяется) и пустого списка, в который вы добавляете элементы, выбранные случайным образом. После каждого нормального розыгрыша (из первого списка) вы извлекаете один элемент из второго списка. Если снова появляется один и тот же элемент (или, скорее, значение элемента), он не выбран (т.е. Неверный ничья, заново заново), в противном случае счетчик розыгрыша и соответствующее значение добавляются ко второму списку.

Общая концепция, по-видимому, связана с эргодическими источниками.

DigitalJoe прокомментировал очевидный недостаток этого подхода. В двух словах, Джо отметил, что вероятность предотвращения повторения (не обязательно последовательных повторов, просто "повторение" ) ранее сделанного значения элемента (значение, найденного во втором списке алгоритма) сильно колеблется в первых нескольких сотнях рисунков, Еще один неявный комментарий заключался в том, что после того, как второй список содержит несколько десятков значений, вероятность предотвращения такого дублирования крайне низка. Они являются действительными точками и требуют квалификации.

Эта аргументация соответствует нашему интуитивному пониманию того, как работает второй список: чем больше значений элементов, тем меньше шансов, что нам нужно "дважды привлечь", следовательно, чтобы предотвратить повторение. Это верно, но он фокусируется только на втором списке. Вероятность получения [из первого списка] элемента, который ранее был замечен, должен учитываться. Мы можем использовать два примера, чтобы понять эту идею, но это, конечно, градиент:

  • Случай "A": вероятность рисования данного значения элемента относительно невелика по сравнению с вероятностью рисования чего-то еще. Таким образом, объединенная вероятность рисования этого элемента несколько раз в течение первых нескольких рисунков является низкой, и вероятность этого, а затем отбрасывания из-за рисунка (-ов) в списке 2 еще ниже (хотя последняя вероятность может быть высокий, из-за небольшого количества элементов во втором списке).
  • Случай "B": вероятность рисования данного объекта высока относительно вероятности рисования других предметов. В этом случае вероятность повторения в первых нескольких розыгрышах высока, и поскольку вероятность предотвращения фактического повтора (с рисунком во втором списке) также высока (достоверность для второго рисунка, вероятность 50% на втором рисунке... 10% -ый шанс на 11-м рисунке), общая вероятность "наказания" весьма вероятного предмета высока. Это, однако, не является проблемой, поскольку относительная вероятность рисования элемента из первого списка гарантирует, что будет статистически множество других возможностей для создания дубликатов, которые не будут столь "насильственно" подавлены по мере продвижения числа чертежей.

В обоих случаях важно, чтобы общее распределение фактических чертежей соответствовало распределению распределения во входном списке. Это особенно верно, поскольку количество рисунков становится более статистически значимым.

В вопросе о возможности слишком слабого повторного фильтра это тоже становится менее актуальным, так как второй список все больше отражает распределение первого списка. Может быть, способ понять все это - рассмотреть альтернативные способы достижения целей, описанных в вопросе ОП.

Альтернативные алгоритмы:
Algo 1: Рисование без замены из первого списка. Для начала мы будем использовать копию исходного списка, и каждый раз, когда данный элемент будет нарисован, мы удалим его из этого списка, поэтому сделать его в целом менее вероятно, что снова появится такое же значение. К тому времени, когда мы нарисовали все предметы, мы воспроизвели точно распределение исходного списка.
Algo 2: Рисование без замены, из списка, в котором ремаркетинг был реплицирован определенное количество раз. Это похоже на вышеизложенное, но вводит немного больше случайности, т.е. Требует большего количества чертежей, чтобы иметь подход к распределению чертежей и сопоставлять их с исходным списком.

В некотором смысле предложенный мной алгоритм из двух списков, предложенный мной первоначально (Draw с заменой из исходного списка и управляющим вторым списком, иногда предотвращающим повторы), похож на Algo 2, так как это приводит к тому, что распределение чертежа сходится к оригинальный список. Преимущество оригинального алгоритма состоит в том, что он упрощает управление списками (хотя справедливости в том, что простой способ сделать это состоит в том, чтобы заменить нарисованные элементы "нулевым" значением сортировок и повторно нарисовать, а затем нанести такой "пустая ячейка", которая фактически является одной и той же, наоборот, для перерисовки, когда рисунок во втором списке производит одно и то же значение элемента.)

Ответ 4

Вы можете сделать что-то вроде создания пользовательского класса (для своего списка), в котором хранится Item плюс вес.

По умолчанию взвешивание до 1, когда вы добавляете элемент, и сохраняете (в "списке" ) общее количество всех весов всех предметов.

Тогда вы можете выбрать случайный случай, просто выберите число между 0 → общий вес всех элементов в списке и пройдите по списку, чтобы найти элемент в этой "позиции" (по весу), Уменьшите вес этого элемента (это может быть некоторый спад, т.е. Умножьте его на 0,8 или 0,5 - у вас будет большой контроль над тем, насколько быстро вероятность отклонения выбирается) и вернуть его.

Недостатком здесь, если у вас много предметов, является то, что ваш выбор - O (n) (поскольку вам нужно пройти через список). Из-за этого я, вероятно, использовал бы связанный список для хранилища (вам все равно придется ходить для извлечения, так что это дает вам более быструю установку/удаление).

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

Ответ 5

Используйте прошедшее время, так как элемент был вставлен или последний выбран в качестве приоритетного mofifier... Установите приоритет каждого элемента = количество времени с момента его установки или последнего выбора, а затем отрегулируйте его, чтобы его можно было выбрать путем умножения на этот приоритет.

Как только у вас есть все шансы на вещи, нормализуйте их (приложите их все к одному и тому же расчетному отношению, чтобы получить их все, чтобы добавить до 1.000), а затем используйте эти значения в качестве их вероятности выбора.

Ответ 6

общая стратегия выбора взвешенного случайного элемента из списка такова: дать каждому элементу вес. нормализовать, так что общий вес равен 1. (так что для начала каждый элемент имеет вес 1/n). сортируйте список по весу. теперь выберите случайное число от 0 до 1 и сходите вниз по списку, накапливая итоговые значения, пока вы не пересечете свой номер. например если ваши веса равны [0,4, 0,3, 0,2, 0,1], а ваше случайное число - 0,63215, ваши первые два шага имеют итоговое value = 0,4, всего = 0,7, а затем обратите внимание, что 0,7 больше 0,63215, поэтому вы возвращаете второй элемент.

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

обратите внимание, что это довольно неэффективно, если у вас много элементов, так как это O (n) в длине списка, но на практике это не имеет значения, если вы не делаете этого во внутреннем цикле, который вам нужен быть сильно оптимизированным или похожим. если это окажется проблемой, вы можете изучить геометрические структуры данных, такие как деревья диапазона, чтобы оптимизировать поиск.