Генерация перетасованного диапазона с использованием PRNG, а не перетасовка
Есть ли какой-либо известный алгоритм, который может генерировать перетасованный диапазон [0..n) в линейном времени и постоянном пространстве (при выходе итеративно) при заданном начальном значении?
Предположим, что n может быть большим, например. в миллионах, поэтому требование потенциально производить всевозможные перестановки не требуется, не в последнюю очередь потому, что оно нецелесообразно (пространство семенных значений должно быть огромным). Это также является причиной требования постоянного пространства. (Итак, я специально не ищу алгоритм перетасовки массива, так как это требует, чтобы диапазон хранился в массиве длины n, и поэтому использовал бы линейное пространство.)
Я знаю вопрос 162606, но он не дает ответа на этот конкретный вопрос - сопоставления из индексов перестановок с перестановками, заданные в этом вопросе, будут требуется огромное пространство семенного значения.
В идеале он будет действовать как LCG с периодом и диапазоном n
, но искусство выбора a
и c
для LCG является тонким. Простое удовлетворение ограничений для a
и c
в течение полного периода LCG может удовлетворить мои требования, но мне интересно, есть ли какие-то лучшие идеи там.
Ответы
Ответ 1
На основе Jason answer, я сделал простую прямую реализацию на С#. Найдите следующую наибольшую степень два больше N. Это делает тривиальным создание a и c, так как c должно быть относительно простым (это означает, что он не может быть делимым на 2, aka odd) и (a-1) требует быть делящимся на 2, и (a-1) нужно делиться на 4. Статистически, для получения следующего числа должно потребоваться 1-2 сравнения (поскольку 2N >= M >= N).
class Program
{
IEnumerable<int> GenerateSequence(int N)
{
Random r = new Random();
int M = NextLargestPowerOfTwo(N);
int c = r.Next(M / 2) * 2 + 1; // make c any odd number between 0 and M
int a = r.Next(M / 4) * 4 + 1; // M = 2^m, so make (a-1) divisible by all prime factors, and 4
int start = r.Next(M);
int x = start;
do
{
x = (a * x + c) % M;
if (x < N)
yield return x;
} while (x != start);
}
int NextLargestPowerOfTwo(int n)
{
n |= (n >> 1);
n |= (n >> 2);
n |= (n >> 4);
n |= (n >> 8);
n |= (n >> 16);
return (n + 1);
}
static void Main(string[] args)
{
Program p = new Program();
foreach (int n in p.GenerateSequence(1000))
{
Console.WriteLine(n);
}
Console.ReadKey();
}
}
Ответ 2
Вот реализация Python Линейный конгруэнтный генератор из ответ FryGuy, Потому что мне все равно нужно было написать это и подумал, что это может быть полезно для других.
import random
import math
def lcg(start, stop):
N = stop - start
# M is the next largest power of 2
M = int(math.pow(2, math.ceil(math.log(N+1, 2))))
# c is any odd number between 0 and M
c = random.randint(0, M/2 - 1) * 2 + 1
# M=2^m, so make (a-1) divisible by all prime factors and 4
a = random.randint(0, M/4 - 1) * 4 + 1
first = random.randint(0, M - 1)
x = first
while True:
x = (a * x + c) % M
if x < N:
yield start + x
if x == first:
break
if __name__ == "__main__":
for x in lcg(100, 200):
print x,
Ответ 3
Похоже, вам нужен алгоритм, гарантирующий получение цикла от 0 до n-1 без повторений. В зависимости от ваших требований почти наверняка будет целая куча. теория групп была бы наиболее полезной ветвью математики, если вы хотите углубиться в теорию, лежащую за ней.
Если вы хотите быстро и не заботитесь о предсказуемости/безопасности/статистических шаблонах, LCG, вероятно, самый простой подход. Страница wikipedia, с которой вы связаны, содержит этот (довольно простой) набор требований:
Период общего LCG не более м, а для некоторых вариантов гораздо меньше чем это. LCG будет иметь полный период тогда и только тогда, когда:
- c и m взаимно просты,
- a - 1 делится на все простые множители m
- a - 1 кратно 4, если m кратно 4
В качестве альтернативы вы можете выбрать период N >= n, где N - наименьшее значение, которое имеет удобные численные свойства и просто отбрасывает любые значения, полученные между n и N-1. Например, наименьшее N = 2 k - 1 >= n позволило бы вам использовать линейные регистры сдвига обратной связи ( LFSR). Или найдите свой любимый криптографический алгоритм (RSA, AES, DES, что угодно) и задайте конкретный ключ, определите пространство N номеров, которое он переставляет, и для каждого шага применяйте шифрование один раз.
Если n мало, но вы хотите, чтобы безопасность была высокой, это, вероятно, самый сложный случай, так как любая последовательность S, вероятно, будет иметь период N, намного превышающий n, но также нетривиально выводить неповторяющуюся последовательность чисел с более короткий период, чем N. (например, если вы можете взять вывод S mod n и гарантировать непревзойденную последовательность чисел, что даст информацию о S, которую может использовать злоумышленник)
Ответ 4
См. мою статью о безопасные перестановки с блочными шифрами для одного из способов сделать это.
Ответ 5
Посмотрите на линейные регистры сдвига обратной связи, они могут быть использованы именно для этого.
Короткий способ объяснить их состоит в том, что вы начинаете с семени, а затем итерации, используя формулу
x = (x << 1) | f(x)
где f (x) может возвращать только 0 или 1.
Если вы выберете хорошую функцию f
, x проведет все значения между 1 и 2 ^ n-1 (где n - некоторое число), хорошим, псевдослучайным способом.
Примеры функций можно найти здесь, например. для 63 значений вы можете использовать
f(x) = ((x >> 6) & 1) ^ ((x >> 5) & 1)