Как я могу случайным образом перебирать большой диапазон?

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

class Array
    def shuffle
        ret = dup
        j = length
        i = 0
        while j > 1
            r = i + rand(j)
            ret[i], ret[r] = ret[r], ret[i]
            i += 1
            j -= 1
        end
        ret
    end
end

(0..9).to_a.shuffle.each{|x| f(x)}

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

Моя проблема в том, что shuffle нужно работать с массивом, что не очень хорошо, потому что я работаю с астрономически большими числами. Ruby быстро потребляет большое количество оперативной памяти, пытаясь создать чудовищный массив. Представьте себе замену (0..9) на (0..99**99). Вот почему следующий код не будет работать:

tried = {} # store previous attempts
bigint = 99**99
bigint.times {
    x = rand(bigint)
    redo if tried[x]
    tried[x] = true
    f(x) # some function
}

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

Какой алгоритм может выполнить то, что я пытаюсь сделать?

[Edit1]: Почему я хочу это сделать? Я пытаюсь исчерпать пространство поиска хеш-алгоритма для входной строки N-длины, ищущей частичные коллизии. Каждое число, которое я генерирую, эквивалентно уникальной входной строке, энтропии и всему остальному. По сути, я «подсчитываю», используя пользовательский алфавит.

[Edit2]: это означает, что f(x) в приведенных выше примерах — это метод, который создает хэш и сравнивает его с постоянным целевым хэшем для частичных конфликтов. Мне не нужно сохранять значение x после вызова f(x), поэтому память должна оставаться неизменной с течением времени.

[Edit3/4/5/6]: дополнительные разъяснения/исправления.

[Решение]. Следующий код основан на решении @bta. Для краткости next_prime не показан. Он производит приемлемую случайность и посещает каждое число только один раз. См. фактический пост для более подробной информации.

N = size_of_range
Q = ( 2 * N / (1 + Math.sqrt(5)) ).to_i.next_prime
START = rand(N)

x = START
nil until f( x = (x + Q) % N ) == START # assuming f(x) returns x

person Community    schedule 17.03.2010    source источник
comment
Очевидно, вы не сохраняете результат вызова вашей функции, так как это также займет много памяти. Так что именно вы делаете? Почему вам нужно делать это в случайном порядке? Если бы вы просто накапливали значения, порядок, скорее всего, не имел бы значения. Я хотел бы знать больше, если вы хотите решение.   -  person Turtle    schedule 17.03.2010
comment
Если вам не нужно возвращать результаты в массив, измените пример кода (0..9).sort_by{rand}.map{|x| f(x)}, чтобы использовать each вместо map. Это сделает вопрос более ясным.   -  person Harish Shetty    schedule 18.03.2010
comment
sort_by rand тоже не правильно; это даст необъективные результаты. См. robweir.com/blog/2010/02/microsoft. -random-browser-ballot.html (JavaScript, но та же концепция).   -  person Matthew Flaschen    schedule 18.03.2010
comment
Как писал @Matthew Flaschen, ваша попытка рандомизировать порядок списка ужасно нарушена и возвращает результаты, которые могут выглядеть случайными, но это не так. Его ссылка дает хорошее описание проблемы.   -  person Turtle    schedule 18.03.2010
comment
пустота, вы упустили суть. Эта ссылка была тем, чего нельзя делать. Вы не можете сортировать по какой-либо случайной функции (сдвинутая случайная функция ничем не лучше).   -  person Matthew Flaschen    schedule 18.03.2010
comment
Ладно, я понимаю, что ты говоришь. Я изменил пример, чтобы использовать перетасовку Фишера-Йейтса.   -  person void    schedule 18.03.2010
comment
Создал итератор из этого: gist.github.com/363914   -  person Colin Curtin    schedule 12.04.2010


Ответы (11)


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

  1. Определите диапазон от 0 до некоторого числа N
  2. Создать случайную начальную точку x[0] внутри N
  3. Создать итератор Q менее N
  4. Создавайте последовательные точки x[n], добавляя Q к предыдущей точке и при необходимости оборачиваясь. То есть x[n+1] = (x[n] + Q) % N
  5. Повторяйте, пока не создадите новую точку, равную начальной точке.

Хитрость заключается в том, чтобы найти итератор, который позволит вам пройти весь диапазон, не генерируя одно и то же значение дважды. Если я правильно помню, любые относительно простые N и Q будут работать (чем ближе число к границам диапазона, тем менее «случайный» ввод). В этом случае должно работать простое число, которое не является множителем N. Вы также можете поменять местами байты/полубайты в результирующем числе, чтобы изменить шаблон, с которым сгенерированные точки «прыгают» в N.

Этот алгоритм требует только сохранения начальной точки (x[0]), текущей точки (x[n]), значения итератора (Q) и предела диапазона (N).

Возможно, кто-то еще помнит этот алгоритм и может проверить, правильно ли я его помню?

person bta    schedule 18.03.2010
comment
Я думаю, что это так хорошо, как вы можете получить, если вы не будете хранить проверенные входы и не можете иметь дубликаты. На самом деле нет необходимости в случайном перемешивании, если вы собираетесь проверить все входные данные, и они не мешают. Чтобы максимально расширить выбор, используйте Q, близкий к золотому сечению (2N/(1+sqrt(5))). - person mckeed; 19.03.2010
comment
Это звучит почти так же, как то, что я хочу сделать. Я не слишком беспокоюсь о случайности, но это очень важно. Если кто-нибудь знает название этого алгоритма, это было бы здорово. - person void; 19.03.2010
comment
Я не уверен, есть ли у алгоритма название. Конкретный принцип, на котором он основан (математическое свойство простых чисел по отношению к модульной арифметике), может иметь название. - person bta; 19.03.2010
comment
См. en.wikipedia.org/wiki/Full_cycle (и, возможно, en.wikipedia.org/wiki/Linear_congruential_generator) - person Lars Haugseth; 09.05.2012

Как ответил @Turtle, у вашей проблемы нет решения. Решение @KandadaBoggu и @bta дает вам случайные числа в некоторых диапазонах, которые являются или не являются случайными. Вы получаете кластеры чисел.

Но я не знаю, почему вас волнует двойное появление одного и того же числа. Если (0..99**99) - ваш диапазон, то если бы вы могли генерировать 10^10 случайных чисел в секунду (если у вас процессор 3 ГГц и около 4 ядер, на которых вы генерируете одно случайное число за цикл ЦП - что невозможно, а ruby ​​даже замедлит это значительно меньше), то потребуется около 10^180 лет, чтобы исчерпать все числа. У вас также есть вероятность около 10^-180, что два одинаковых числа будут сгенерированы в течение целого года. В нашей Вселенной, вероятно, около 10 ^ 9 лет, поэтому, если бы ваш компьютер мог начать вычисления, когда началось время, то у вас была бы вероятность около 10 ^ -170, что были сгенерированы два одинаковых числа. Другими словами - практически это невозможно и вам не нужно об этом заботиться.

Даже если вы будете использовать Jaguar (первый из суперкомпьютеров www.top500.org) только для одной этой задачи, вы все еще нужно 10 ^ 174 лет, чтобы получить все числа.

Если ты мне не веришь, попробуй

tried = {} # store previous attempts
bigint = 99**99
bigint.times {
  x = rand(bigint)
  puts "Oh, no!" if tried[x]
  tried[x] = true
}

Я куплю тебе пива, если ты хоть раз увидишь "О, нет!" на вашем экране при жизни :)

person klew    schedule 18.03.2010
comment
Спасибо за полезную информацию. Диапазон (0..99**99) был просто примером. Алгоритм хеширования, который я тестирую, имеет пространство поиска, которое исчерпывается за реалистичное время для ввода реалистичной длины. Я просто хотел, чтобы мой алгоритм эффективно масштабировался, давая каждому числу одинаковую вероятность быть выбранным. Что касается пива, я думаю, что у солнца больше шансов спонтанно телепортироваться на другой конец галактики :) - person void; 18.03.2010
comment
Пространство поиска, которое я тестирую, равно (0..(80**N-1)) для входной длины N. - person void; 19.03.2010
comment
Для N = 11 потребуется 34 года, чтобы исчерпать все числа с той же скоростью, что и в моем примере выше. Поэтому, вероятно, когда вы используете ruby ​​и не только генерируете числа, но и выполняете с ними некоторые вычисления, вам не следует заботиться о повторяющихся числах, потому что для исчерпания всех возможностей потребуются целые годы. С другой стороны, для N = 6 вы можете хранить все проверенные числа в одном бите в массиве - это займет около 409 МБ. При N = 7 у вас должно быть около 32 ГБ памяти, поэтому, вероятно, вам следует хранить ее на жестком диске. Но опять же это займет много времени. - person klew; 19.03.2010
comment
На моем компьютере простой цикл вроде этого: a = 80**4; b = 0; a.times {b = b+1} занял около 16 секунд. Это означает, что при увеличении N на единицу это время увеличится в 80 раз, поэтому для N=6 это займет 24 минуты, для N=7 - 28 часов, для N=8 - более 9 дней. При таком расчете получается 13300 лет для N=11 (это реальный пример на одном ядре с частотой 2,13 ГГц). - person klew; 19.03.2010
comment
Похоже, ты запутался в математике. При переходе от N=7 к N=8 вы умножаете на 8 вместо 80. Фактическое время для N=8 чуть больше 3 месяцев. Учитывая достаточную случайность при выборе ключа для тестирования, среднее время рассмотрения дела сокращается вдвое. Использование преимущества многоядерного ЦП разделит среднее время обработки на количество имеющихся у вас ядер. Если требуется больше эффективности, я могу переключиться на другой язык. Подняв его на следующий уровень, я мог использовать свой графический процессор для потоковой обработки. - person void; 19.03.2010

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

Даже если вы используете только один бит на значение (было ли это значение опробовано да или нет), вам потребуется X/8 байтов памяти для хранения результата (где X — наибольшее число). Если предположить, что у вас есть 2 ГБ свободной памяти, у вас останется более 16 миллионов номеров.

person Turtle    schedule 17.03.2010

Разбейте диапазон на управляемые партии, как показано ниже:

def range_walker range, batch_size = 100
  size = (range.end - range.begin) + 1
  n = size/batch_size 
  n.times  do |i|
    x = i * batch_size + range.begin
    y = x + batch_size
    (x...y).sort_by{rand}.each{|z| p z}
  end
  d = (range.end - size%batch_size + 1)
  (d..range.end).sort_by{rand}.each{|z| p z }
end

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

PS: Это хорошая проблема для уменьшения карты. Каждая партия может обрабатываться независимыми узлами.

Ссылка:

Map-reduce в Ruby

person Harish Shetty    schedule 17.03.2010
comment
Даже если бы n и размер_пакета были одним и тем же числом (sqrt(n)), сгенерированные массивы были бы слишком большими для хранения в памяти. Хороший подход однако. Я думаю, что окончательный алгоритм должен делать что-то подобное, за исключением того, что массивы будут иметь управляемый размер. - person void; 17.03.2010
comment
В вашем вопросе не было ясно, хотите ли вы получить результаты в виде массива. Я думал, вы просто хотите случайным образом обрабатывать числа в диапазоне, гарантируя, что каждое число будет обработано. Это решение делает это независимо от размера диапазона. Если вы хотите вернуть эти числа в виде массива, у вас другая проблема. - person Harish Shetty; 17.03.2010
comment
Извините, что не уточнил. Мне не нужны результаты в виде массива. Где-то внутри этого цикла я хотел бы вызвать метод, который принимает сгенерированное случайное число в качестве входных данных. Использование памяти должно оставаться постоянным в долгосрочной перспективе. - person void; 17.03.2010
comment
Попробуйте вызвать range_walker(0..99**99) и вы поймете, что я имею в виду. - person void; 17.03.2010
comment
Я исправил проблему. Попробуйте еще раз. Потребление памяти останется прежним. ЦП приближается к 60% из-за непрерывной обработки. - person Harish Shetty; 17.03.2010
comment
Я правильно понимаю этот код? Ассортимент разбит на партии. Каждая партия имеет случайное распределение. Тем не менее, пакеты по-прежнему посещаются по порядку, когда их нужно посещать случайным образом. Теперь мы вернулись к той же проблеме. :-) - person void; 18.03.2010
comment
@void: это компромисс между случайностью и использованием памяти. Вы экономите довольно много памяти, посещая партии по порядку. Практически любое решение будет жертвовать случайностью ради использования памяти, если существует ограничение, согласно которому каждый вход посещается ровно один раз. - person bta; 18.03.2010
comment
@void: Другой способ взглянуть на это: пакеты посещаются не по порядку, они посещаются параллельно. Используйте многопроцессорную многоядерную машину и загрузите пакет на каждое ядро. Этот тип проблемы кажется чрезвычайно распараллеливаемым, и это решение, похоже, разбивает его на параллельные куски. - person bta; 19.03.2010
comment
Я согласен. Распараллеливание очень эффективно в этой ситуации. Я просто хотел, чтобы алгоритм хорошо масштабировался до чрезвычайно больших диапазонов без использования большого количества памяти, поэтому я привел нелепый пример. - person void; 19.03.2010

вы можете случайным образом перебирать массив методом перемешивания

a = [1,2,3,4,5,6,7,8,9]
a.shuffle!
=> [5, 2, 8, 7, 3, 1, 6, 4, 9]
person Community    schedule 08.05.2012

Вам нужен так называемый "итератор полного цикла"...

Вот псевдокод для самой простой версии, которая идеально подходит для большинства применений...

function fullCycleStep(sample_size, last_value, random_seed = 31337, prime_number = 32452843) {
if last_value = null then last_value = random_seed % sample_size
    return (last_value + prime_number) % sample_size
}

Если вы называете это так:

sample = 10
For i = 1 to sample
    last_value = fullCycleStep(sample, last_value)
    print last_value
next

Он будет генерировать случайные числа, циклически перебирая все 10, никогда не повторяясь. все равно никогда не будет дубликатов.

person Community    schedule 23.04.2014

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

person yfeldblum    schedule 17.03.2010

Насколько «случайным» должен быть ваш заказ? Если вам не нужно конкретное распределение ввода, вы можете попробовать рекурсивную схему, подобную этой, чтобы минимизировать использование памяти:

def gen_random_indices
  # Assume your input range is (0..(10**3))
  (0..3).sort_by{rand}.each do |a|
    (0..3).sort_by{rand}.each do |b|
      (0..3).sort_by{rand}.each do |c|
        yield "#{a}#{b}#{c}".to_i
      end
    end
  end
end

gen_random_indices do |idx|
  run_test_with_index(idx)
end

По сути, вы строите индекс, случайным образом генерируя по одной цифре за раз. В худшем случае потребуется достаточно памяти для хранения 10 * (количество цифр). Вы встретите каждое число в диапазоне (0..(10**3)) ровно один раз, но порядок будет псевдослучайным. То есть, если первый цикл устанавливает a=1, то вы столкнетесь со всеми трехзначными числами формы 1xx до того, как увидите изменение разряда сотен.

Другим недостатком является необходимость вручную создавать функцию на заданную глубину. В вашем случае (0..(99**99)) это, вероятно, будет проблемой (хотя я полагаю, вы могли бы написать скрипт для генерации кода для вас). Я уверен, что, вероятно, есть способ переписать это в рекурсивной манере с полным состоянием, но я не могу придумать это в голове (идеи, кто-нибудь?).

person bta    schedule 17.03.2010
comment
Настолько случайно, насколько это возможно. Это позволяет эффективно исчерпать пространство поиска. Это также то, что делает возможной атаку на день рождения, резко сокращая время поиска. Думайте об этом как о переборе комбинации с замком. - person void; 18.03.2010

[Изменить]: Принимая во внимание ответы @klew и @Turtle, лучшее, на что я могу надеяться, это наборы случайных (или близких к случайным) чисел.


Это рекурсивная реализация чего-то похожего на решение KandadaBoggu. По сути, пространство поиска (как диапазон) разбито на массив, содержащий N диапазонов одинакового размера. Каждый диапазон возвращается в случайном порядке как новое пространство поиска. Это продолжается до тех пор, пока размер диапазона не достигнет нижней границы. На данный момент диапазон достаточно мал, чтобы его можно было преобразовать в массив, перетасовать и проверить.

Несмотря на то, что это рекурсивно, я еще не взорвал стек. Вместо этого возникает ошибка при попытке разбить область поиска, размер которой превышает 10^19 ключей. Я связан с тем, что числа слишком велики, чтобы преобразовать их в long. Вероятно, это можно исправить:

# partition a range into an array of N equal-sized ranges
def partition(range, n)
    ranges = []
    first = range.first
    last = range.last
    length = last - first + 1
    step = length / n # integer division
    ((first + step - 1)..last).step(step) { |i|
        ranges << (first..i)
        first = i + 1
    }
    # append any extra onto the last element
    ranges[-1] = (ranges[-1].first)..last if last > step * ranges.length
    ranges
end

Я надеюсь, что комментарии к коду помогут пролить свет на мой первоначальный вопрос.

pastebin: полный исходный код

Примечание: PW_LEN под # options можно изменить на меньшее число, чтобы получить более быстрые результаты.

person void    schedule 18.03.2010
comment
Это мило, но вы видите, что это не настоящая перетасовка, верно? Первое число будет распределено случайным образом, а следующие числа BLOCK_SIZE будут из одного диапазона. - person mckeed; 19.03.2010
comment
Если я не ошибаюсь в вашем комментарии, Фишер-Йейтс - это настоящая перетасовка, и она используется правильно. Каждый блок разбивается и посещается в случайном порядке. Однако лучшее, что он может сделать, это наборы случайных чисел... - person void; 19.03.2010

Для непомерно большого пространства, например

space = -10..1000000000000000000000

Вы можете добавить этот метод в Range.

class Range

  M127 = 170_141_183_460_469_231_731_687_303_715_884_105_727

  def each_random(seed = 0)
    return to_enum(__method__) { size } unless block_given?
    unless first.kind_of? Integer
      raise TypeError, "can't randomly iterate from #{first.class}"
    end

    sample_size = self.end - first + 1
    sample_size -= 1 if exclude_end?
    j = coprime sample_size
    v = seed % sample_size
    each do
      v = (v + j) % sample_size
      yield first + v
    end
  end

protected

  def gcd(a,b)
    b == 0 ? a : gcd(b, a % b)
  end

  def coprime(a, z = M127)
    gcd(a, z) == 1 ? z : coprime(a, z + 1)
  end

end

Вы могли бы тогда

space.each_random { |i| puts i }

729815750697818944176
459631501395637888351
189447252093456832526
919263002791275776712
649078753489094720887
378894504186913665062
108710254884732609237
838526005582551553423
568341756280370497598
298157506978189441773
27973257676008385948
757789008373827330134
487604759071646274309
217420509769465218484
947236260467284162670
677052011165103106845
406867761862922051020
136683512560740995195
866499263258559939381
596315013956378883556
326130764654197827731
55946515352016771906
785762266049835716092
515578016747654660267
...

С хорошей степенью случайности, если ваше пространство на несколько порядков меньше, чем M127.

Кредит @nick-steele и @bta за подход.

person Community    schedule 22.09.2017

На самом деле это не специфичный для Ruby ответ, но я надеюсь, что это разрешено. Эндрю Кенслер приводит функцию C++ permute(), которая делает именно это в своей Коррелированная выборка с множественными дрожаниями. отчет.

Насколько я понимаю, точная функция, которую он предоставляет, действительно работает, только если ваш массив имеет размер до 2 ^ 27, но общую идею можно использовать для массивов любого размера.

Я сделаю все возможное, чтобы как-то объяснить это. Первая часть заключается в том, что вам нужен хеш, который является обратимым для любого домена с размером, равным степени двойки. Рассмотрим x = i + 1. Независимо от того, что такое x, даже если ваше целое число переполняется, вы можете определить, что такое i. Более конкретно, вы всегда можете определить младшие n битов числа i по младшим n битам числа x. Сложение — это обратимая хэш-операция, как и умножение на нечетное число, как и побитовое исключающее ИЛИ на константу. Если вы знаете определенный домен степени двойки, вы можете скремблировать биты в этом домене. Например. x ^= (x & 0xFF) >> 5) действителен для 16-битного домена. Вы можете указать этот домен с маской, например. mask = 0xFF, и ваша хеш-функция станет x = hash(i, mask). Конечно, вы можете добавить начальное значение в эту хэш-функцию, чтобы получить различные рандомизации. Кенслер излагает в статье более достоверные операции.

Итак, у вас есть обратимая функция x = hash(i, mask, seed). Проблема в том, что если вы хэшируете свой индекс, вы можете получить значение, превышающее размер вашего массива, то есть вашего домена. Вы не можете просто по модулю это или вы получите столкновения.

Обратимый хеш — это ключ к использованию метода, называемого циклическим ходьбой, представленного в Ciphers. с произвольными конечными доменами. Поскольку хэш является обратимым (т. е. 1-к-1), вы можете просто многократно применять один и тот же хэш, пока ваше хэш-значение не станет меньше, чем ваш массив! Поскольку вы применяете один и тот же хеш, а сопоставление — один к одному, любое значение, которое вы получите, будет отображаться ровно на один индекс, поэтому у вас не будет коллизий. Таким образом, ваша функция может выглядеть примерно так для 32-битных целых чисел (псевдокод):

fun permute(i, length, seed) {
  i = hash(i, 0xFFFF, seed)
  while(i >= length): i = hash(i, 0xFFFF, seed)
  return i
}

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

fun next_pow_2(length) {
  # This implementation is for clarity.
  # See Kensler's paper for one way to do it fast.
  p = 1
  while (p < length): p *= 2
  return p
}

permute(i, length, seed) {
  mask = next_pow_2(length)-1
  i = hash(i, mask, seed) & mask
  while(i >= length): i = hash(i, mask, seed) & mask
  return i
}

Вот и все! Очевидно, что здесь важно выбрать хорошую хеш-функцию, которую Кенслер предоставляет в статье, но я хотел разобрать объяснение. Если вы хотите каждый раз иметь разные случайные перестановки, вы можете добавить начальное значение в функцию перестановки, которая затем передается хеш-функции.

person Community    schedule 06.03.2021