python - Agregar factorización de rueda a un tamiz indefinido
primes infinite (2)
Esta es la versión que se me ocurrió. No es tan limpio como el de Ness , pero funciona. Lo estoy publicando para que haya otro ejemplo sobre cómo usar la factorización de la rueda en caso de que alguien venga. Me queda la posibilidad de elegir qué tamaño de rueda usar, pero es fácil establecer una más permanente: solo genera el tamaño que deseas y pégalo en el código.
from itertools import count
def wpsieve():
"""prime number generator
call this function instead of roughing or turbo"""
whlSize = 11
initPrms, gaps, c = wheel_setup(whlSize)
for p in initPrms:
yield p
primes = turbo(0, (gaps, c))
for p, x in primes:
yield p
def prod(seq, factor=1):
"sequence -> product"
for i in seq: factor *= i
return factor
def wheelGaps(primes):
"""returns list of steps to each wheel gap
that start from the last value in primes"""
strtPt = primes.pop(-1) # where the wheel starts
whlCirm = prod(primes) # wheel''s circumference
# spokes are every number that are divisible by primes (composites)
gaps = [] # locate where the non-spokes are (gaps)
for i in xrange(strtPt, strtPt + whlCirm + 1, 2):
if not all(map(lambda x: i%x, primes)): continue # spoke
else: gaps.append(i) # non-spoke
# find the steps needed to jump to each gap (beginning from the start of the wheel)
steps = [] # last step returns to start of wheel
for i, j in enumerate(gaps):
if i == 0: continue
steps.append(int(j - gaps[i-1]))
return steps
def wheel_setup(num):
"builds initial data for sieve"
initPrms = roughing(num) # initial primes from the "roughing" pump
gaps = wheelGaps(initPrms[:]) # get the gaps
c = initPrms.pop(-1) # prime that starts the wheel
return initPrms, gaps, c
def roughing(end):
"finds primes by trial division (roughing pump)"
primes = [2]
for i in range(3, end + 1, 2):
if all(map(lambda x: i%x, primes)):
primes.append(i)
return primes
def turbo(lvl=0, initData=None):
"""postponed prime generator with wheels (turbo pump)
Refs: http://stackoverflow.com/a/10733621
http://stackoverflow.com/a/19391111"""
gaps, c = initData
yield (c, 0)
compost = {} # found composites to skip
# store as current value: (base prime, wheel index)
ps = turbo(lvl + 1, (gaps, c))
p, x = next(ps)
psq = p*p
gapS = len(gaps) - 1
ix = jx = kx = 0 # indices for cycling the wheel
def cyc(x): return 0 if x > gapS else x # wheel cycler
while True:
c += gaps[ix] # add next step on c''s wheel
ix = cyc(ix + 1) # and advance c''s index
bp, jx = compost.pop(c, (0,0)) # get base prime and its wheel index
if not bp:
if c < psq: # prime
yield c, ix # emit index for above recursive level
continue
else:
jx = kx # swap indices as a new prime comes up
bp = p
p, kx = next(ps)
psq = p*p
d = c + bp * gaps[jx] # calc new multiple
jx = cyc(jx + 1)
while d in compost:
step = bp * gaps[jx]
jx = cyc(jx + 1)
d += step
compost[d] = (bp, jx)
dejar la opción para el tamaño de la rueda también le permite ver qué tan rápido las ruedas más grandes no hacen mucho. A continuación se muestra el código de prueba de cuánto tiempo se tarda en generar la rueda del tamaño seleccionado y qué tan rápido es el tamiz con esa rueda.
import time
def speed_test(num, whlSize):
print(''-''*50)
t1 = time.time()
initPrms, gaps, c = wheel_setup(whlSize)
t2 = time.time()
print(''2-{} wheel''.format(initPrms[-1]))
print(''setup time: {} sec.''.format(round(t2 - t1, 5)))
t3 = time.time()
prm = initPrms[:]
primes = turbo(0, (gaps, c))
for p, x in primes:
prm.append(p)
if len(prm) > num:
break
t4 = time.time()
print(''run time : {} sec.''.format(len(prm), round(t4 - t3, 5)))
print(''prime sum : {}''.format(sum(prm)))
for w in [5, 7, 11, 13, 17, 19, 23, 29]:
speed_test(1e7-1, w)
Así es como se ejecutó en mi computadora usando PyPy (compatible con Python 2.7) cuando está configurado para generar diez millones de primos:
2- 3 wheel
setup time: 0.0 sec.
run time : 18.349 sec.
prime sum : 870530414842019
--------------------------------------------------
2- 5 wheel
setup time: 0.001 sec.
run time : 13.993 sec.
prime sum : 870530414842019
--------------------------------------------------
2- 7 wheel
setup time: 0.001 sec.
run time : 7.821 sec.
prime sum : 870530414842019
--------------------------------------------------
2- 11 wheel
setup time: 0.03 sec.
run time : 6.224 sec.
prime sum : 870530414842019
--------------------------------------------------
2- 13 wheel
setup time: 0.011 sec.
run time : 5.624 sec.
prime sum : 870530414842019
--------------------------------------------------
2- 17 wheel
setup time: 0.047 sec.
run time : 5.262 sec.
prime sum : 870530414842019
--------------------------------------------------
2- 19 wheel
setup time: 1.043 sec.
run time : 5.119 sec.
prime sum : 870530414842019
--------------------------------------------------
2- 23 wheel
setup time: 22.685 sec.
run time : 4.634 sec.
prime sum : 870530414842019
Son posibles ruedas más grandes, pero puede ver que se vuelven bastante largas de instalar.
También existe la ley de rendimientos decrecientes a medida que las ruedas se hacen más grandes, no tiene mucho sentido pasar la rueda 2-13 ya que en realidad no lo hacen mucho más rápido.
También terminé encontrando un error de memoria más allá de la rueda 2-23 (que tenía unos 36 millones de números en su lista de
gaps
).
Estoy modificando un tamiz indefinido de Eratóstenes desde here por lo que utiliza la factorización de la rueda para omitir más compuestos que su forma actual de simplemente verificar todas las probabilidades.
He resuelto cómo generar los pasos a seguir para alcanzar todos los huecos a lo largo de la rueda. A partir de ahí, pensé que podría sustituir los + 2 por estos pasos de rueda, pero está causando que el tamiz se salte los cebos. Aquí está el código:
from itertools import count, cycle
def dvprm(end):
"finds primes by trial division. returns a list"
primes=[2]
for i in range(3, end+1, 2):
if all(map(lambda x:i%x, primes)):
primes.append(i)
return primes
def prod(seq, factor=1):
"sequence -> product"
for i in seq:factor*=i
return factor
def wheelGaps(primes):
"""returns list of steps to each wheel gap
that start from the last value in primes"""
strtPt= primes.pop(-1)#where the wheel starts
whlCirm= prod(primes)# wheel''s circumference
#spokes are every number that are divisible by primes (composites)
gaps=[]#locate where the non-spokes are (gaps)
for i in xrange(strtPt, strtPt+whlCirm+1, 2):
if not all(map(lambda x:i%x,primes)):continue#spoke
else: gaps.append(i)#non-spoke
#find the steps needed to jump to each gap (beginning from the start of the wheel)
steps=[]#last step returns to start of wheel
for i,j in enumerate(gaps):
if i==0:continue
steps.append(j - gaps[i-1])
return steps
def wheel_setup(num):
"builds initial data for sieve"
initPrms=dvprm(num)#initial primes from the "roughing" pump
gaps = wheelGaps(initPrms[:])#get the gaps
c= initPrms.pop(-1)#prime that starts the wheel
return initPrms, gaps, c
def wheel_psieve(lvl=0, initData=None):
''''''postponed prime generator with wheels
Refs: http://stackoverflow.com/a/10733621
http://stackoverflow.com/a/19391111''''''
whlSize=11#wheel size, 1 higher prime than
# 5 gives 2- 3 wheel 11 gives 2- 7 wheel
# 7 gives 2- 5 wheel 13 gives 2-11 wheel
#set to 0 for no wheel
if lvl:#no need to rebuild the gaps, just pass them down the levels
initPrms, gaps, c = initData
else:#but if its the top level then build the gaps
if whlSize>4:
initPrms, gaps, c = wheel_setup(whlSize)
else:
initPrms, gaps, c= dvprm(7), [2], 9
#toss out the initial primes
for p in initPrms:
yield p
cgaps=cycle(gaps)
compost = {}#found composites to skip
ps=wheel_psieve(lvl+1, (initPrms, gaps, c))
p=next(ps)#advance lower level to appropriate square
while p*p < c:
p=next(ps)
psq=p*p
while True:
step1 = next(cgaps)#step to next value
step2=compost.pop(c, 0)#step to next multiple
if not step2:
#see references for details
if c < psq:
yield c
c += step1
continue
else:
step2=2*p
p=next(ps)
psq=p*p
d = c + step2
while d in compost:
d+= step2
compost[d]= step2
c += step1
Estoy usando esto para verificarlo:
def test(num=100):
found=[]
for i,p in enumerate(wheel_psieve(), 1):
if i>num:break
found.append(p)
print sum(found)
return found
Cuando configuro el tamaño de la rueda en 0, obtengo la suma correcta de 24133 para los primeros cien números primos, pero cuando uso cualquier otro tamaño de rueda, termino con números primos faltantes, sumas incorrectas y compuestos. Incluso una rueda 2-3 (que utiliza pasos alternativos de 2 y 4) hace que el tamiz pierda el cebado. ¿Qué estoy haciendo mal?
Las probabilidades, es decir, 2 coprimes, se generan al
"hacer girar la rueda"
[2]
, es decir, mediante adiciones repetidas de 2, comenzando desde el valor inicial de 3 (de manera similar a partir de 5, 7, 9, ...),
n=3; n+=2; n+=2; n+=2; ... # wheel = [2]
3 5 7 9
Los 2-3 coprimos se generan mediante adiciones repetidas de 2, luego 4, y nuevamente 2, luego 4, y así sucesivamente:
n=5; n+=2; n+=4; n+=2; n+=4; ... # wheel = [2,4]
5 7 11 13 17
Aquí necesitamos saber por dónde comenzar a sumar las diferencias, 2 o 4, dependiendo del valor inicial. Para 5, 11, 17, ..., es 2 (es decir, el elemento número 0 de la rueda); para 7, 13, 19, ..., es 4 (es decir, primer elemento).
¿Cómo podemos saber por dónde empezar?
El punto para la optimización de la rueda es que solo trabajamos en esta secuencia de coprimos (en este ejemplo, 2-3 coprimos).
Entonces, en la parte del código donde obtenemos los primos generados recursivamente, también mantendremos el flujo de la rueda rodante y avanzaremos hasta que veamos el próximo cebado en él.
La secuencia de balanceo necesitará producir
dos
resultados: el valor y la posición de la rueda.
Por lo tanto, cuando vemos la prima, también obtenemos la posición correspondiente de la rueda, y podemos comenzar la generación de sus múltiplos a partir de esa posición en la rueda.
Multiplicamos todo por
p
por supuesto, para comenzar desde
p*p
:
for (i, p) # the (wheel position, summated value)
in enumerated roll of the wheel:
when p is the next prime:
multiples of p are m = p*p; # map (p*) (roll wheel-at-i from p)
m += p*wheel[i];
m += p*wheel[i+1]; ...
Por lo tanto, cada entrada en el dict tendrá que mantener su valor actual, su prima base y su posición actual de la rueda (ajustando a 0 para circularidad, cuando sea necesario).
Para producir los primos resultantes, rodamos otra secuencia coprimes, y conservamos solo aquellos elementos que no están en el dict, como en el código de referencia.
actualización: después de algunas iteraciones en codereview (¡ muchas gracias a los colaboradores allí!) He llegado a este código, usando itertools tanto como sea posible, para la velocidad:
from itertools import accumulate, chain, cycle, count
def wsieve(): # wheel-sieve, by Will Ness. ideone.com/mqO25A
wh11 = [ 2,4,2,4,6,2,6,4,2,4,6, 6,2,6,4,2,6,4,6,8,4,2, 4,
2,4,8,6,4,6,2,4,6,2,6, 6,4,2,4,6,2,6,4,2,4,2, 10,2,10]
cs = accumulate(chain([11], cycle(wh11))) # roll the wheel from 11
yield(next(cs)) # cf. ideone.com/WFv4f,
ps = wsieve() # codereview.stackexchange.com/q/92365/9064
p = next(ps) # 11
psq = p**2 # 121
D = dict(zip(accumulate(chain([0], wh11)), count(0))) # wheel roll lookup dict
mults = {}
for c in cs: # candidates, coprime with 210, from 11
if c in mults:
wheel = mults.pop(c)
elif c < psq:
yield c
continue
else: # c==psq: map (p*) (roll wh from p) = roll (wh*p) from (p*p)
i = D[(p-11) % 210] # look up wheel roll starting point
wheel = accumulate( chain( [psq],
cycle( [p*d for d in wh11[i:] + wh11[:i]])))
next(wheel)
p = next(ps)
psq = p**2
for m in wheel: # pop, save in m, and advance
if m not in mults:
break
mults[m] = wheel # mults[143] = wheel@187
def primes():
yield from (2, 3, 5, 7)
yield from wsieve()
A diferencia de la descripción anterior, este código calcula directamente dónde comenzar a girar la rueda para cada cebado, para generar sus múltiplos