Генерация перетасованного диапазона с использованием 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, которую может использовать злоумышленник)

Ответ 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)