Выберите 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 заранее не известно.

Во всяком случае, это работает!


  1. К. Т. Фан, М. Э. Мюллер и И. Резуча, Дж. Амер. Стат. Доц. 57 (1962), с. 387 - 402
  2. Т. Джонс, 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 элементы.