Выбор A, C и M для линейного конгруэнтного генератора

Я надеюсь реализовать простой генератор псевдослучайных чисел (PRNG), который имеет указанный период и гарантирует отсутствие коллизий в течение этого периода. После некоторого исследования я наткнулся на очень известный LCG, который идеально подходит. Проблема в том, что у меня проблемы с пониманием того, как правильно его настроить. Вот моя текущая реализация:

    function LCG (state)
    {
        var a = ?;
        var c = ?;
        var m = ?;

        return (a * state + c) % m;
    }

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

  1. c и m относительно простые
  2. a-1 делится на все простые множители м
  3. a-1 кратно 4, если m кратно 4

1 и 3 просты для понимания и проверки. Однако насчет 2, я не совсем понимаю, что это значит или как это проверить. А как насчет C, это может быть ноль? что если оно ненулевое?

В целом мне нужно выбрать A, C и M таким образом, чтобы у меня был период 48 ^ 5 - 1. М равен периоду, я не уверен насчет А и С.

Ответы

Ответ 1

Из Википедии:

При условии, что c отличное от нуля, LCG будет иметь полный период для всех начальных значений тогда и только тогда, когда:

  • c и m взаимно просты,
  • a-1 делится на все простые множители m,
  • a-1 кратно 4, если m кратно 4.

Вы сказали, что хотите период 48 5 -1, поэтому вы должны выбрать m≥48 5 -1. Попробуем выбрать m = 48 5 -1 и посмотреть, что это заставляет нас. Условия из статьи в Википедии запрещают вам выбирать c = 0, если вы хотите, чтобы период был м.

Заметим, что 11, 47, 541 и 911 являются первичными факторами 48 5 -1, так как они все простые и 11 * 47 * 541 * 911 = 48 5 -1.

Пропустите каждое из этих условий:

  • Для того чтобы c и m были относительно простыми, c и m не должны иметь общих простых факторов. Итак, выберите любые простые числа, отличные от 11, 47, 541 и 911, затем умножьте их вместе, чтобы выбрать свой c.
  • Вам нужно выбрать такое, что a-1 делится на все простые множители m, т.е. a = x * 11 * 47 * 541 * 911 + 1 для любого x вашего выбора.
  • Ваш m не кратен 4, поэтому вы можете игнорировать третье условие.

Вкратце:

  • m = 48 5 -1,
  • c = любое произведение простых чисел, отличных от 11, 47, 541 и 911 (также c должно быть меньше m),
  • a = x * 11 * 47 * 541 * 911 + 1, для любого неотрицательного x по вашему выбору (также, a должно быть меньше m).

Здесь меньший тестовый пример (в Python) с использованием периода 48 2 -1 (который имеет простые множители 7 и 47):

def lcg(state):
    x = 1
    a = x*7*47 + 1
    c = 100
    m = 48**2 - 1
    return (a * state + c) % m

expected_period = 48**2 - 1
seeds = [5]
for i in range(expected_period):
    seeds.append(lcg(seeds[-1]))
print(len(set(seeds)) == expected_period)

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

Ответ 2

Основываясь на ответе Snowball и комментариях, я создал полный пример. Вы можете использовать сравнение set == list для меньших чисел. Я не смог уместить 48^5-1 в память.

Чтобы обойти проблему a < m, я увеличиваю цель несколько раз, чтобы найти число, где a может быть < m (где m имеет дублированные основные факторы). Удивительно, но +2 достаточно для многих цифр. Несколько дополнительных чисел позже пропускаются во время итерации.

import random

def __prime_factors(n):
    """
    https://stackoverflow.com/a/412942/6078370
    Returns all the prime factors of a positive integer
    """
    factors = []
    d = 2
    while n > 1:
        while n % d == 0:
            factors.append(d)
            n //= d
        d += 1
        if d * d > n:
            if n > 1: factors.append(n)
            break
    return factors

def __multiply_numbers(numbers):
    """multiply all numbers in array"""
    result = 1
    for n in numbers:
        result *= n
    return result

def __next_good_number(start):
    """
    https://en.wikipedia.org/wiki/Linear_congruential_generator#c%E2%89%A00
    some conditions apply for good/easy rotation
    """
    number = start
    factors = __prime_factors(number)
    while len(set(factors)) == len(factors) or number % 4 == 0:
        number += 1
        factors = __prime_factors(number)
    return number, set(factors)

# primes < 100 for coprime calculation. add more if your target is large
PRIMES = set([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41,
        43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97])

def create_new_seed(target):
    """be aware, m might become > target"""
    m, factors = __next_good_number(target)
    a = __multiply_numbers(factors) + 1
    # https://en.wikipedia.org/wiki/Coprime_integers
    otherPrimes = [p for p in PRIMES if p not in factors]
    # the actual random part to get differnt results
    random.shuffle(otherPrimes)
    # I just used arbitary 3 of the other primes
    c = __multiply_numbers(otherPrimes[:3])
    # first number
    state = random.randint(0, target-1)
    return state, m, a, c

def next_number(state, m, a ,c, limit):
    newState = (a * state + c) % m
    # skip out of range (__next_good_number increases original target)
    while newState >= limit:
        newState = (a * newState + c) % m

    return newState

if __name__ == "__main__":
    target = 48**5-1
    state, m, a, c = create_new_seed(target)
    print(state, m, a, c, 'target', target)

    # list and set can't fit into 16GB of memory
    checkSum = sum(range(target))
    randomSum = 0
    for i in range(target):
        state = newState = next_number(state, m, a ,c, target)
        randomSum += newState
    print(checkSum == randomSum) # true

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

def riter(alist):
    """ iterate list using LCG """
    target = len(alist)
    state, m, a, c = create_new_seed(target)
    for _ in range(target):
        yield alist[state]
        state = next_number(state, m, a ,c, target)

Легко сохранить состояние между шагами итерации:

savedState = '18:19:25:6:12047269:20'
print('loading:', savedState)
i, state, m, a, c, target = (int(i) for i in savedState.split(':'))
state = next_number(state, m, a, c, target)
i += 1
print('i:', i, 'is random number:', state, 'list done:', i+1 == target)
print('saving:', '{}:{}:{}:{}:{}:{}'.format(i, state, m, a, c, target))