Python 2 usando pypy y pp: n = 15 en 3 minutos
También solo una simple fuerza bruta. Es interesante ver que casi obtengo la misma velocidad que Kuroi Neko con C ++. Mi código puede alcanzarn = 12
en unos 5 minutos. Y solo lo ejecuto en un núcleo virtual.
editar: reducir el espacio de búsqueda por un factor de n
Noté que un vector cíclico A*
de A
produce los mismos números que las probabilidades (los mismos números) que el vector original A
cuando repito B
. Ej El vector (1, 1, 0, 1, 0, 0)
tiene las mismas probabilidades como cada uno de los vectores (1, 0, 1, 0, 0, 1)
, (0, 1, 0, 0, 1, 1)
, (1, 0, 0, 1, 1, 0)
, (0, 0, 1, 1, 0, 1)
y (0, 1, 1, 0, 1, 0)
al elegir una al azar B
. Por lo tanto, no tengo que iterar sobre cada uno de estos 6 vectores, sino solo alrededor de 1 y reemplazar count[i] += 1
con count[i] += cycle_number
.
Esto reduce la complejidad de Theta(n) = 6^n
a Theta(n) = 6^n / n
. Por n = 13
lo tanto , es aproximadamente 13 veces más rápido que mi versión anterior. Se calcula n = 13
en aproximadamente 2 minutos y 20 segundos. porn = 14
todavía es un poco lento. Tarda unos 13 minutos.
editar 2: programación multi-core
No estoy muy contento con la próxima mejora. Decidí también intentar ejecutar mi programa en múltiples núcleos. En mis núcleos 2 + 2 ahora puedo calcular n = 14
en aproximadamente 7 minutos. Solo un factor de mejora 2.
El código está disponible en este repositorio de github: Link . La programación multinúcleo es un poco fea.
editar 3: Reducción del espacio de búsqueda para A
vectores y B
vectores
Noté la misma simetría de espejo para los vectores A
que Kuroi Neko. Todavía no estoy seguro, por qué esto funciona (y si funciona para cada uno n
).
La reducción del espacio de búsqueda de B
vectores es un poco más inteligente. Reemplacé la generación de los vectores ( itertools.product
), con una función propia. Básicamente, comienzo con una lista vacía y la pongo en una pila. Hasta que la pila esté vacía, elimino una lista, si no tiene la misma longitud n
, genero otras 3 listas (agregando -1, 0, 1) y empujándolas a la pila. Si una lista tiene la misma longitud que n
, puedo evaluar las sumas.
Ahora que genero los vectores yo mismo, puedo filtrarlos dependiendo de si puedo alcanzar la suma = 0 o no. Por ejemplo, si mi vector A
es (1, 1, 1, 0, 0)
, y mi vector se B
ve (1, 1, ?, ?, ?)
, lo sé, que no puedo llenar los ?
valores, de modo que A*B = 0
. Así que no tengo que iterar sobre todos esos 6 vectores B
de la forma(1, 1, ?, ?, ?)
.
Podemos mejorar esto si ignoramos los valores para 1. Como se señaló en la pregunta, los valores para i = 1
son la secuencia A081671 . Hay muchas formas de calcularlos. Elijo la repetición simple: a(n) = (4*(2*n-1)*a(n-1) - 12*(n-1)*a(n-2)) / n
. Como i = 1
básicamente podemos calcular en poco tiempo, podemos filtrar más vectores para B
. Por ejemplo A = (0, 1, 0, 1, 1)
y B = (1, -1, ?, ?, ?)
. Podemos ignorar los vectores, donde los primeros ? = 1
, porque los A * cycled(B) > 0
, para todos estos vectores. Espero que puedas seguir. Probablemente no sea el mejor ejemplo.
Con esto puedo calcular n = 15
en 6 minutos.
editar 4:
Implementé rápidamente la gran idea de kuroi neko, que dice eso B
y -B
produce los mismos resultados. Aceleración x2. Sin embargo, la implementación es solo un truco rápido. n = 15
en 3 minutos
Código:
Para ver el código completo, visite Github . El siguiente código es solo una representación de las características principales. Omití importaciones, programación multinúcleo, impresión de resultados, ...
count = [0] * n
count[0] = oeis_A081671(n)
#generating all important vector A
visited = set(); todo = dict()
for A in product((0, 1), repeat=n):
if A not in visited:
# generate all vectors, which have the same probability
# mirrored and cycled vectors
same_probability_set = set()
for i in range(n):
tmp = [A[(i+j) % n] for j in range(n)]
same_probability_set.add(tuple(tmp))
same_probability_set.add(tuple(tmp[::-1]))
visited.update(same_probability_set)
todo[A] = len(same_probability_set)
# for each vector A, create all possible vectors B
stack = []
for A, cycled_count in dict_A.iteritems():
ones = [sum(A[i:]) for i in range(n)] + [0]
# + [0], so that later ones[n] doesn't throw a exception
stack.append(([0] * n, 0, 0, 0, False))
while stack:
B, index, sum1, sum2, used_negative = stack.pop()
if index < n:
# fill vector B[index] in all possible ways,
# so that it's still possible to reach 0.
if used_negative:
for v in (-1, 0, 1):
sum1_new = sum1 + v * A[index]
sum2_new = sum2 + v * A[index - 1 if index else n - 1]
if abs(sum1_new) <= ones[index+1]:
if abs(sum2_new) <= ones[index] - A[n-1]:
C = B[:]
C[index] = v
stack.append((C, index + 1, sum1_new, sum2_new, True))
else:
for v in (0, 1):
sum1_new = sum1 + v * A[index]
sum2_new = sum2 + v * A[index - 1 if index else n - 1]
if abs(sum1_new) <= ones[index+1]:
if abs(sum2_new) <= ones[index] - A[n-1]:
C = B[:]
C[index] = v
stack.append((C, index + 1, sum1_new, sum2_new, v == 1))
else:
# B is complete, calculate the sums
count[1] += cycled_count # we know that the sum = 0 for i = 1
for i in range(2, n):
sum_prod = 0
for j in range(n-i):
sum_prod += A[j] * B[i+j]
for j in range(i):
sum_prod += A[n-i+j] * B[j]
if sum_prod:
break
else:
if used_negative:
count[i] += 2*cycled_count
else:
count[i] += cycled_count
Uso:
Tienes que instalar pypy (para Python 2 !!!). El módulo paralelo de Python no está portado para Python 3. Luego debe instalar el módulo paralelo de Python pp-1.6.4.zip . Extraerlo cd
en la carpeta y llamar pypy setup.py install
.
Entonces puedes llamar a mi programa con
pypy you-do-the-math.py 15
Determinará automáticamente el número de CPU. Puede haber algunos mensajes de error después de finalizar el programa, simplemente ignórelos. n = 16
debería ser posible en su máquina.
Salida:
Calculation for n = 15 took 2:50 minutes
1 83940771168 / 470184984576 17.85%
2 17379109692 / 470184984576 3.70%
3 3805906050 / 470184984576 0.81%
4 887959110 / 470184984576 0.19%
5 223260870 / 470184984576 0.05%
6 67664580 / 470184984576 0.01%
7 30019950 / 470184984576 0.01%
8 20720730 / 470184984576 0.00%
9 18352740 / 470184984576 0.00%
10 17730480 / 470184984576 0.00%
11 17566920 / 470184984576 0.00%
12 17521470 / 470184984576 0.00%
13 17510280 / 470184984576 0.00%
14 17507100 / 470184984576 0.00%
15 17506680 / 470184984576 0.00%
Notas e ideas:
- Tengo un procesador i7-4600m con 2 núcleos y 4 hilos. No importa si uso 2 o 4 hilos. El uso de la CPU es del 50% con 2 subprocesos y del 100% con 4 subprocesos, pero aún requiere la misma cantidad de tiempo. No se porque. Verifiqué que cada subproceso solo tiene la mitad de la cantidad de datos, cuando hay 4 subprocesos, verifiqué los resultados, ...
- Yo uso muchas listas. Python no es muy eficiente en el almacenamiento, tengo que copiar muchas listas, así que pensé en usar un número entero. Podría usar los bits 00 (para 0) y 11 (para 1) en el vector A, y los bits 10 (para -1), 00 (para 0) y 01 (para 1) en el vector B. Para el producto de A y B, solo tendría que calcular
A & B
y contar los bloques 01 y 10. El ciclismo se puede hacer cambiando el vector y usando máscaras, ... De hecho, implementé todo esto, puedes encontrarlo en algunos de mis compromisos más antiguos en Github. Pero resultó ser más lento que con las listas. Supongo que pypy realmente optimiza las operaciones de la lista.