Este desafío es en parte un desafío de algoritmos, involucra algunas matemáticas y es en parte simplemente un desafío de código más rápido.
Para algún entero positivo n
, considere una cadena aleatoria de forma uniforme 1
s y 0
s de longitud n
y llamarlo A
. Ahora también considere una segunda cadena aleatoria de longitud elegida uniformemente n
cuyos valores son -1
, 0,
o 1
y llámela B_pre
. Ahora deja B
ser B_pre
+ B_pre
. Eso se B_pre
concatena a sí mismo.
Consideremos ahora el producto interno de A
y B[j,...,j+n-1]
y llamarlo Z_j
y el índice de 1
.
Tarea
El resultado debe ser una lista de n+1
fracciones. El i
ésimo término en la salida debe ser la exacta probabilidad de que todo de los primeros i
términos Z_j
con j <= i
igual 0
.
Puntuación
El más grande n
para el que su código proporciona la salida correcta en menos de 10 minutos en mi máquina.
Desempate
Si dos respuestas tienen el mismo puntaje, gana la que presentó primero.
En el caso (muy, muy poco probable) de que alguien encuentre un método para obtener puntajes ilimitados, se aceptará la primera prueba válida de tal solución.
Insinuación
No intentes resolver este problema matemáticamente, es demasiado difícil. En mi opinión, la mejor manera es volver a las definiciones básicas de probabilidad de la escuela secundaria y encontrar formas inteligentes de obtener el código para hacer una enumeración exhaustiva de las posibilidades.
Idiomas y bibliotecas
Puede usar cualquier idioma que tenga un compilador / intérprete / etc. para Linux y cualquier biblioteca que también esté disponible gratuitamente para Linux.
Mi máquina Los tiempos se ejecutarán en mi máquina. Esta es una instalación estándar de ubuntu en un procesador AMD FX-8350 de ocho núcleos. Esto también significa que necesito poder ejecutar su código. Como consecuencia, solo use software gratuito fácilmente disponible e incluya instrucciones completas sobre cómo compilar y ejecutar su código.
Algunas salidas de prueba. Considere solo el primer resultado para cada uno n
. Eso es cuando i=1
. Para n
de 1 a 13 que deberían ser.
1: 4/6
2: 18/36
3: 88/216
4: 454/1296
5: 2424/7776
6: 13236/46656
7: 73392/279936
8: 411462/1679616
9: 2325976/10077696
10: 13233628/60466176
11: 75682512/362797056
12: 434662684/2176782336
13: 2505229744/13060694016
También puede encontrar una fórmula general i=1
en http://oeis.org/A081671 .
Tabla de clasificación (dividida por idioma)
- n = 15. Python + python paralelo + pypy en 1min49s por Jakube
- n = 17. C ++ en 3min37s por Keith Randall
- n = 16. C ++ en 2min38s por kuroi neko
fuente
Respuestas:
C ++, n = 18 en 9 min en 8 hilos
(Avíseme si dura menos de 10 minutos en su máquina).
Aprovecho varias formas de simetría en la matriz B. Esos son cíclicos (desplazamiento en una posición), inversión (invertir el orden de los elementos) y signo (tomar el negativo de cada elemento). Primero calculo la lista de Bs que necesitamos probar y su peso. Luego, cada B se ejecuta a través de una rutina rápida (usando instrucciones de conteo de bits) para todos los 2 ^ n valores de A.
Aquí está el resultado para n == 18:
Compile el siguiente programa con
g++ --std=c++11 -O3 -mpopcnt dot.cc
fuente
-pthread
nuevo. Llego an=17
mi máquina.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 alcanzar
n = 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*
deA
produce los mismos números que las probabilidades (los mismos números) que el vector originalA
cuando repitoB
. 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 azarB
. Por lo tanto, no tengo que iterar sobre cada uno de estos 6 vectores, sino solo alrededor de 1 y reemplazarcount[i] += 1
concount[i] += cycle_number
.Esto reduce la complejidad de
Theta(n) = 6^n
aTheta(n) = 6^n / n
. Porn = 13
lo tanto , es aproximadamente 13 veces más rápido que mi versión anterior. Se calculan = 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 yB
vectoresNoté 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 unon
).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 longitudn
, genero otras 3 listas (agregando -1, 0, 1) y empujándolas a la pila. Si una lista tiene la misma longitud quen
, 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 seB
ve(1, 1, ?, ?, ?)
, lo sé, que no puedo llenar los?
valores, de modo queA*B = 0
. Así que no tengo que iterar sobre todos esos 6 vectoresB
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
. Comoi = 1
básicamente podemos calcular en poco tiempo, podemos filtrar más vectores paraB
. Por ejemploA = (0, 1, 0, 1, 1)
yB = (1, -1, ?, ?, ?)
. Podemos ignorar los vectores, donde los primeros? = 1
, porque losA * 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 minutosCó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, ...
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 llamarpypy 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:
Notas e ideas:
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.fuente
matón lanudo - C ++ - demasiado lento
Bueno, ya que un mejor programador asumió la implementación de C ++, estoy llamando a dejar de fumar para este.
Construyendo el ejecutable
Es una fuente independiente de C ++ 11 que se compila sin advertencias y se ejecuta sin problemas en:
Si compila con g ++, use: g ++ -O3 -pthread -std = c ++ 11
olvidarse
-pthread
producirá un volcado de núcleo agradable y amigable.Optimizaciones
El último término Z es igual al primero (Bpre x A en ambos casos), por lo que los dos últimos resultados son siempre iguales, lo que dispensa el cálculo del último valor Z.
La ganancia es despreciable, pero codificarla no cuesta nada, por lo que bien podría usarla.
Como descubrió Jakube, todos los valores cíclicos de un vector A dado producen las mismas probabilidades.
Puede calcularlos con una sola instancia de A y multiplicar el resultado por el número de sus posibles rotaciones. Los grupos de rotación se pueden precalcular fácilmente en un tiempo descuidado, por lo que esta es una gran ganancia de velocidad neta.
Dado que el número de permutaciones de un vector de longitud n es n-1, la complejidad cae de o (6 n ) a o (6 n / (n-1)), básicamente yendo un paso más allá para el mismo tiempo de cálculo.
Parece que los pares de patrones simétricos también generan las mismas probabilidades. Por ejemplo, 100101 y 101001.
No tengo ninguna prueba matemática de eso, pero intuitivamente cuando se me presentan todos los patrones B posibles, cada valor A simétrico se enredará con el valor B simétrico correspondiente para el mismo resultado global.
Esto permite reagrupar algunos vectores A más, para una reducción aproximada del 30% del número de grupos A.
Incorrecto
Por alguna razón semi-misteriosa, todos los patrones con solo uno o dos bits establecidos producen el mismo resultado. Esto no representa tantos grupos distintos, pero aun así pueden fusionarse prácticamente sin costo.Los vectores B y -B (B con todos los componentes multiplicados por -1) producen las mismas probabilidades.
(por ejemplo [1, 0, -1, 1] y [-1, 0, 1, -1]).
Excepto por el vector nulo (todos los componentes son iguales a 0), B y -B forman un par de vectores distintos .
Esto permite reducir a la mitad el número de valores de B considerando solo uno de cada par y multiplicando su contribución por 2, agregando la contribución global conocida de B nulo a cada probabilidad solo una vez.
Cómo funciona
El número de valores B es enorme (3 n ), por lo que precalcularlos requeriría cantidades indecentes de memoria, lo que ralentizaría el cálculo y eventualmente agotaría la RAM disponible.
Desafortunadamente, no pude encontrar una manera simple de enumerar el medio conjunto de valores B optimizados, así que recurrí a la codificación de un generador dedicado.
El poderoso generador B fue muy divertido de codificar, aunque los lenguajes que admiten mecanismos de rendimiento habrían permitido programarlo de una manera mucho más elegante.
Lo que hace en pocas palabras es considerar el "esqueleto" de un vector Bpre como un vector binario donde los 1 representan valores reales de -1 o +1.
Entre todos estos valores potenciales + 1 / -1, el primero se fija en +1 (seleccionando así uno de los posibles vectores B / -B), y se enumeran todas las combinaciones posibles + 1 / -1 restantes.
Por último, un sistema de calibración simple garantiza que cada hilo de trabajo procesará un rango de valores de aproximadamente el mismo tamaño.
Los valores A están muy filtrados para reagruparse en trozos equiprobables.
Esto se hace en una fase previa a la computación que la fuerza bruta examina todos los valores posibles.
Esta parte tiene un tiempo de ejecución de O (2 n ) descuidado y no necesita ser optimizado (¡el código ya es lo suficientemente ilegible como es!).
Para evaluar el producto interno (que solo necesita ser probado contra cero), los componentes -1 y 1 de B se reagrupan en vectores binarios.
El producto interno es nulo si (y solo si) hay un número igual de + 1s y -1s entre los valores B correspondientes a valores A distintos de cero.
Esto se puede calcular con operaciones simples de enmascaramiento y conteo de bits, ayudado por
std::bitset
eso producirá un código de conteo de bits razonablemente eficiente sin tener que recurrir a instrucciones intrínsecas feas.El trabajo se divide por igual entre los núcleos, con afinidad forzada de la CPU (cada poquito ayuda, o eso dicen).
Resultado de ejemplo
Actuaciones
Multithreading debería funcionar perfectamente en esto, aunque solo los núcleos "verdaderos" contribuirán completamente a la velocidad de cálculo. Mi CPU tiene solo 2 núcleos para 4 CPU, y la ganancia sobre una versión de subproceso único es "solo" aproximadamente 3.5.
Compiladores
Un problema inicial con subprocesos múltiples me llevó a creer que los compiladores de GNU estaban funcionando peor que Microsoft.
Después de pruebas más exhaustivas, parece que g ++ gana el día una vez más, produciendo un código aproximadamente un 30% más rápido (la misma proporción que noté en otros dos proyectos pesados en computación).
En particular, la
std::bitset
biblioteca se implementa con instrucciones de recuento de bits dedicadas por g ++ 4.8, mientras que MSVC 2013 usa solo bucles de cambios de bits convencionales.Como era de esperar, compilar en 32 o 64 bits no hace ninguna diferencia.
Más refinamientos
Noté algunos grupos A que producen las mismas probabilidades después de todas las operaciones de reducción, pero no pude identificar un patrón que permitiera reagruparlos.
Aquí están los pares que noté para n = 11:
fuente
terminate called after throwing an instance of 'std::system_error' what(): Unknown error -1 Aborted (core dumped)