2016-06-07 7 views
7

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

Первый алгоритм, с которым я столкнулся, состоял в том, чтобы выбрать случайное число r [0] от 1 до nk ... и затем выбрать последовательные случайные числа r [i] из r [i-1] +1 в n -k + i, только нужно хранить две записи для «r» в любой момент времени. Однако довольно простой анализ показывает, что вероятность выбора небольших чисел не согласуется с тем, что могло бы быть, если бы весь набор был равномерно распределен. Например, если n был миллиардом и k составлял полмиллиарда, вероятность выбора первой записи с подходом, который я только что описал, очень крошечная (1 в пол-миллиарда), где на самом деле, поскольку половина записей выбранный, первый должен быть выбран в 50% случаев. Даже если я использую внешнюю сортировку для сортировки k случайных чисел, мне придется отбросить любые дубликаты и повторить попытку. Когда k приближается к n, количество попыток продолжит расти, без гарантии прекращения.

Я хотел бы найти алгоритм O (k) или O (k log k) для этого, если это вообще возможно. Язык реализации, который я буду использовать, - это C++ 11, но описания в псевдокоде могут по-прежнему быть полезными.

+1

Генерируйте случайные целые числа как обычно (используя, например, 'std :: mt19937' и' std :: uniform_int_distribution') и сохраняйте результаты в 'std :: set ', чтобы не было дубликатов, и в результате контейнер сортируется по своей сути. – ArchbishopOfBanterbury

+0

Всегда ли нужно выбирать точно k элементов? Или это приемлемо для того, чтобы средний счет многих прогонов стремился к k? Если последний, то просто добавьте RND (0, 2n/k) к каждой предыдущей записи, пока не дойдете до конца списка. –

+0

Всегда восходящий. Нет хранения. Нет дублирования. Это трудно сделать. Мне придется подумать, возможно ли это. – user4581301

ответ

3

Вы можете решить эту проблему рекурсивно в O (к лог-к), если вы разделите в середине своего диапазона, и случайным образом выборку из hypergeometric probability distribution выбрать, сколько значения лежат выше и ниже средней точки (т.е. значения к каждой подпоследовательности), то для каждой рекурсии:

int sample_hypergeometric(int n, int K, int N) // samples hypergeometric distribution and 
// returns number of "successes" where there are n draws without replacement from 
// a population of N with K possible successes. 
// Something similar to scipy.stats.hypergeom.rvs in Python. 
// In this case, "success" means the selected value lying below the midpoint. 
{ 
    std::default_random_engine generator; 
    std::uniform_real_distribution<double> distribution(0.0,1.0); 

    int successes = 0; 
    for(int trial = 0; trial < n; trial++) 
    { 
     if((int)(distribution(generator) * N) < K) 
     { 
      successes++; 
      K--; 
     } 
     N--; 
    } 
    return successes; 
} 

select_k_from_n(int start, int k, int n) 
{ 
    if(k == 0) 
     return; 
    if(k == 1) 
    { 
     output start + random(1 to n); 
     return; 
    } 

    // find the number of results below the mid-point: 
    int k1 = sample_hypergeometric(k, n >> 1, n); 
    select_k_from_n(start, k1, n >> 1); 
    select_k_from_n(start + (n >> 1), k - k1, n - (n >> 1)); 
} 

Отбора проб из binomial distribution также может быть использованы для аппроксимации гипергеометрического распределения с р = (п >> 1)/N, отклоняющие образцами, где k1> (n >> 1).

+0

Прошу прощения, но я не знаю, как генерировать случайные числа в гипергеометрическом распределении вероятностей. Могли бы вы рассказать об этом посту, указав sample_hypergeometric в терминах либо равномерного распределения, либо в виде одного из других уже существующих распределений случайных чисел в C++ 11 (http://en.cppreference.com/ж/CPP/числовые/случайное)? Спасибо. – markt1964

+0

@ markt1964 Я добавил код для генерации случайных чисел (непроверенный) – samgak

+0

Спасибо. Можно ли определить sample_hypergeometric, используя только закрытые функции формы, или требуется ли это для цикла? – markt1964

2

Как упоминалось в моем комментарии, используйте std::set<int> для хранения случайных целых чисел, так что полученный контейнер по сути сортируется и не содержит дубликатов. Пример фрагмента кода:

#include <random> 
#include <set> 

int main(void) { 
    std::set<int> random_set; 
    std::random_device rd; 
    std::mt19937 mt_eng(rd()); 
    // min and max of random set range 
    const int m = 0; // min 
    const int n = 100; // max 
    std::uniform_int_distribution<> dist(m,n); 

    // number to generate 
    const int k = 50; 
    for (int i = 0; i < k; ++i) { 
     // only non-previously occurring values will be inserted 
     if (!random_set.insert(dist(mt_eng)).second) 
      --i; 
    } 
} 
+1

Это не гарантирует, что random_set будет содержать 50 элементов ... Какая разница со вторым алгоритмом OP? –

+0

@StefanHaustein Исправлена ​​первая проблема. – ArchbishopOfBanterbury

+1

Это хорошее решение 'k log k'. Вы можете сохранить переменное именование согласованным с вопросом. Я считаю, что ваш 'max' -' n' и 'n' -' k'. – luk32

0

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

IANAS, но я предполагаю, что если вы выберете случайное число r между 0 и 1 (вы можете масштабировать до полного оставшегося диапазона индекса после настройки), вы можете настроить его, вычислив r^(х) (сохраняя диапазон в 0..1, но увеличивая вероятность меньших чисел), причем х выбирается путем решения уравнения для вероятности первой записи?

0

Предполагая, что вы не можете хранить k случайных чисел в памяти, вам придется генерировать числа в строгом произвольном порядке. Один из способов сделать это - создать число от 0 до n/k. Позвоните по этому номеру x. Следующий номер, который вы должны сгенерировать, находится между x+1 и (n-x)/(k-1). Продолжайте таким образом, пока вы не наберете k чисел.

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

Пример. Вы хотите сгенерировать 3 числа от 0 до 99 включительно. Поэтому вы сначала генерируете число от 0 до 33. Скажите, что вы выбрали 10.

Итак, теперь вам нужно число от 11 до 99. Оставшийся диапазон состоит из 89 значений, и у вас есть два значения для выбора. Итак, 89/2 = 44. Вам нужно число от 11 до 54. Скажите, что вы выбрали 36.

Ваш оставший диапазон составляет от 37 до 99, и у вас есть один номер налево. Поэтому выберите номер в случайном порядке между 37 и 99.

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

Этот псевдокод показывает основную идею.

pick_k_from_n(n, k) 
{ 
    num_left = k 
    last_k = 0; 
    while num_left > 0 
    { 
     // divide the remaining range into num_left partitions 
     range_size = (n - last_k)/num_left 
     // pick a number in the first partition 
     r = random(range_size) + last_k + 1 
     output(r) 
     last_k = r 
     num_left = num_left - 1 
    } 
} 

Обратите внимание, что это занимает время O (k) и требует O (1) дополнительного пространства.

+0

Что вы делаете, когда x [i] == n перед i = k? – user4581301

+0

Разве это не делает выбор невозможным, если индекс не ниже 33 (для вашего примера) - вместо того, чтобы быть менее вероятным? –

+0

OP хочет строгий заказ. Это обеспечит его при отмеченной стоимости искаженного распределения, но это не удастся, если вы выберете последний номер до окончания выбора. – user4581301

0

Вы можете сделать это в O (k) времени с помощью алгоритма Флойда (не Floyd-Warshall, это кратчайший путь). Единственная структура данных, которая вам нужна, это 1-битная таблица, которая сообщит вам, был ли уже выбран номер. Поиск хеш-таблицы может быть O (1), поэтому это не будет бременем и может храниться в памяти даже для очень больших n (если n действительно огромно, вам придется использовать фильтр b-tree или bloom или что-то).

Для выбора K элементов из числа п:

for j = n-k+1 to n: 
    select random x from 1 to j 
    if x is already in hash: 
    insert j into hash 
    else 
    insert x into hash 

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

+0

Хорошая идея, хотя фильтр Bloom не будет работать из-за ложных срабатываний. –

+0

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

5

Если на практике к имеет тот же порядок величины, что и п, может быть очень простой О (п) алгоритм будет достаточно:

assert(k <= n); 
std::uniform_real_distribution rnd; 
for (int i = 0; i < n; i++) { 
    if (rnd(engine) * (n - i) < k) { 
     std::cout << i << std::endl; 
     k--; 
    } 
} 

Он производит все восходящие последовательности с равной вероятностью.

+0

Как вы гарантируете, что это выбирает именно элементы 'k'? –

+1

Спасибо, я заметил ошибку при ответе (должен быть 'rnd * (n - i) алгоритм выборки для генерации k из n элементов, а затем radix sorts их в базе √n. Вместо того, чтобы помнить, каковы фактические образцы, мы сделаем первый проход, где мы запускаем вариант Floyd, где мы помним только количество выборок в каждом ковше. Второй проход для каждого ведра в порядке, чтобы случайным образом перепрограммировать соответствующее количество элементов из диапазона ковша. Существует короткое доказательство условной вероятности того, что это дает равномерное распределение.

# untested Python code for illustration 
# b is the number of buckets (e.g., b ~ sqrt(n)) 
import random 
def first_pass(n, k, b): 
    counts = [0] * b # list of b zeros 
    for j in range(n - k, n): 
     t = random.randrange(j + 1) 
     if t // b >= counts[t % b]: # intuitively, "t is not in the set" 
      counts[t % b] += 1 
     else: 
      counts[j % b] += 1 
    return counts 
Смежные вопросы