Skip to content

Instantly share code, notes, and snippets.

@d-schmidt
Created July 18, 2018 20:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save d-schmidt/d33f454bef3db108f4f9e6ecb4f97a6b to your computer and use it in GitHub Desktop.
Save d-schmidt/d33f454bef3db108f4f9e6ecb4f97a6b to your computer and use it in GitHub Desktop.
A random Python list iterator without shuffle using the linear congruential generator (LCG) algorithm. A pseudorandom number generator producing all numbers < len(list) with a period == len(list) is created.
import random
import warnings
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)
# first 46 primes for coprime calculation.
# add or use more (than 3) if better randomness is required
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, 101, 103, 107,
109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
179, 181, 191, 193, 197, 199])
def create_new_seed(target):
"""
be aware, m might become > target
https://stackoverflow.com/a/12096699/6078370
1. m and the offset c are relatively prime,
2. a-1 is divisible by all prime factors of m,
3. a-1 is divisible by 4 if m is divisible by 4.
"""
# does 3. and 2. a < m
m, factors = __next_good_number(target)
# 2.
a = __multiply_numbers(factors) + 1
# 1. https://en.wikipedia.org/wiki/Coprime_integers
otherPrimes = [p for p in PRIMES if p not in factors]
if not otherPrimes:
raise ValueError('all target prime factors in prime set')
if len(otherPrimes) < 5:
warnings.warn("very few remaining primes, losing randomness")
# randomize c to get random results while following 1.
random.shuffle(otherPrimes)
# I just used arbitary 3 of the other primes (~60000 different periods)
c = __multiply_numbers(otherPrimes[:3])
# get random number < target as seed and first state
state = random.randint(0, target-1)
return state, m, a, c
def next_number(state, m, a ,c, limit):
""" linear congruential generator (LCG) algorithm """
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
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)
if __name__ == "__main__":
# random iterator (no shuffle)
for i in riter(list(range(10))):
print(i)
# or use as deterministic random number generator 0 <= state < target
target = 2000
state, m, a, c = create_new_seed(target)
# use hard-coded state (< target) and c=1 to be predictable
print('first={} m={} a={} c={} (target={})'.format(state, m, a, c, target))
checkSum = (target-1)*target//2 # sum(range(target))
checkList = []
randomSum = 0
for i in range(target):
state = newState = next_number(state, m, a ,c, target)
randomSum += newState
checkList.append(newState)
print(checkSum == randomSum) # true
print(len(checkList) == target) # true
print(len(set(checkList)) == len(checkList)) # true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment