Выберите n записей в случайном порядке из набора из N
Мне нужно выбрать n
записи случайным образом из набора n
(где 0 < n < N
).
Возможный алгоритм:
Итерации по списку и для каждого элемента, сделайте вероятность выбора = (number needed) / (number left)
Итак, если у вас было 40 элементов, у первого был бы шанс 5/40
на выбор.
Если это так, следующая имеет шанс 4/39
, в противном случае имеет шанс 5/39
. К тому времени, когда вы доберетесь до конца у вас будут ваши 5 предметов, и часто вы будете иметь их все до этого.
Предполагая хороший генератор псевдослучайных чисел, правильно ли этот алгоритм?
Примечание
В stackoverflow есть много таких вопросов (многие из них отмечены как дубликаты Выберите N случайных элементов из списка <T> в С#).
Этот алгоритм часто предлагается (например, Kyle Cronin) и
он всегда подвергается сомнению (например, см.
здесь, здесь, здесь, здесь...).
Могу ли я получить последнее слово по этому поводу?
Ответы
Ответ 1
Алгоритм абсолютно правильный.
Это не внезапное изобретение хорошего плаката, это хорошо известная техника под названием " Выборочная выборка/Алгоритм S" (открыта Фаном, Мюллером и Резучей (1) и независимо Джонсом (2) в 1962 году), хорошо описанная в TAOCP - Том 2 - Получисленные алгоритмы - § 3.4.2.
Как говорит Кнут:
Этот алгоритм может показаться ненадежным на первый взгляд и, на самом деле, неверным. Но тщательный анализ показывает, что он полностью заслуживает доверия.
Алгоритм выбирает n
элементов из набора размером N
и t + 1
й элемент выбирается с вероятностью (n - m)/(N - t)
, когда уже выбрано m
элементов.
Легко видеть, что мы никогда не покидаем конец набора до выбора n
элементов (поскольку вероятность будет равна 1
когда у нас будет k
элементов для выбора из оставшихся k
элементов).
Также мы никогда не выбираем слишком много элементов (вероятность будет равна 0
как только n == m
).
Немного сложнее продемонстрировать, что образец абсолютно беспристрастен, но это
... верно, несмотря на то, что мы не выбираем t + 1
й элемент с вероятностью n/N
Это вызвало некоторую путаницу в опубликованной литературе
(так что не только на Stackoverflow!)
Дело в том, что мы не должны путать условные и безусловные вероятности:
Например, рассмотрим второй элемент; если в выборке был выбран первый элемент (это происходит с вероятностью n/N
), второй элемент выбирается с вероятностью (n - 1)/(N - 1)
; если первый элемент не был выбран, второй элемент выбирается с вероятностью n/(N - 1)
.
Общая вероятность выбора второго элемента составляет (n/N) ((n - 1)/(N - 1)) + (1 - n/N)(n/(N - 1)) = n/N
TAOCP - Том 2 - Раздел 3.4.2 упражнение 3
Помимо теоретических соображений, алгоритм S (и алгоритм R/ выборка из пласта) используется во многих известных библиотеках (например, оригинальная реализация STL SGI, std::experimental::sample
, random.sample
в Python...).
Конечно, алгоритм S не всегда лучший ответ:
- это
O(N)
(даже если нам обычно не придется проходить через все N
записей: среднее число записей, рассматриваемых при n=2
составляет около 2/3 N
; общие формулы приведены в TAOCP - Том 2 - § 3.4.2 - бывшие 5/6); - его нельзя использовать, если значение
N
заранее не известно.
Во всяком случае, это работает!
- К. Т. Фан, М. Э. Мюллер и И. Резуча, Дж. Амер. Стат. Доц. 57 (1962), с. 387 - 402
- Т. Джонс, CACM 5 (1962), стр. 343
РЕДАКТИРОВАТЬ
как вы случайным образом выбираете этот предмет, с вероятностью 7/22
[РЕЗАТЬ]
В редких случаях вы можете выбрать 4 или 6 элементов, когда захотите 5
Это из N3925 (небольшие изменения, чтобы избежать общего интерфейса/отправки тегов):
template<class PopIter, class SampleIter, class Size, class URNG>
SampleIter sample(PopIter first, PopIter last, SampleIter out, Size n, URNG &&g)
{
using dist_t = uniform_int_distribution<Size>;
using param_t = typename dist_t::param_type;
dist_t d{};
Size unsampled_sz = distance(first, last);
for (n = min(n, unsampled_sz); n != 0; ++first)
{
param_t const p{0, --unsampled_sz};
if (d(g, p) < n) { *out++ = *first; --n; }
}
return out;
}
Здесь нет поплавков.
- Если вам нужно 5 элементов, вы получите 5 элементов;
- если
uniform_int_distribution
"работает как рекламируется ", то смещения нет.
Ответ 2
Хотя описанный алгоритм технически правильный, это зависит от наличия алгоритма для возврата bool с произвольной вероятностью, определяемой отношением двух ints, Например, как вы выбираете этот элемент с вероятностью 7/22? Для того, чтобы говорить, назовите его методом bool RandomSelect(int x, int y)
или просто методом RS(x,y)
, предназначенным для возврата true
с вероятностью x/y
. Если вы не очень обеспокоены точностью, часто задаваемый ответ заключается в использовании return Random.NextDouble() < (double)x/(double)y;
, который является неточным, потому что Random.NextDouble()
является неточным и не совсем однородным, а деление (double)x/(double)y
также является неточным. Выбор <
или <=
должен быть неактуальным (но это не так), потому что в теории невозможно случайным образом выбрать случайное число бесконечной точности, точно равное указанной вероятности. Хотя я уверен, что алгоритм может быть создан или найден, чтобы точно реализовать метод RS(x,y)
, который позволит вам правильно реализовать описанный алгоритм, я думаю, что просто ответить на этот вопрос как "да, алгоритм верен", будет вводить в заблуждение, поскольку он ввел в заблуждение так много людей до этого, чтобы вычислить и выбрать элементы, используя double
, не подозревая о предвзятости, которую они представляют.
Не поймите меня неправильно - я не говорю, что каждый должен избегать использования описанного алгоритма. Я говорю только, что, если вы не найдете более точный способ реализации алгоритма RS(x,y)
, ваши выборы будут слегка искажены в преимущество некоторых элементов чаще, чем другие элементы.
Если вы заботитесь о справедливости (равной вероятности всех возможных результатов), я думаю, что лучше и проще понять, вместо этого использовать другой алгоритм, как я описал ниже:
Если вы считаете, что единственным источником случайности, который у вас есть, являются случайные биты, вы должны определить метод случайного выбора, который обеспечивает равную вероятность, учитывая двоичные случайные данные. Это означает, что если вы хотите выбрать случайное число в диапазоне, который имеет мощность 2, вы просто выбираете случайные биты и возвращаете их. Но если вам нужно случайное число в диапазоне, не равном 2, вы должны получить больше случайных бит и отбросить результаты, которые не могли бы отображаться на справедливые результаты (выбросить случайное число и повторить попытку). Я писал об этом с пикторальными представлениями и примером кода С# здесь: https://nedharvey.com/blog/?p=284 Повторите случайный выбор из своей коллекции, пока у вас не будет n
unique элементы.