Aquí hay un problema de programación simple de SPOJ: http://www.spoj.com/problems/PROBTRES/ .
Básicamente, se le pide que genere el mayor ciclo de Collatz para números entre i y j. (El ciclo de Collatz de un número $ n $ es el número de pasos para llegar eventualmente de $ n $ a 1.)
He estado buscando una forma de Haskell para resolver el problema con un rendimiento comparativo que el de Java o C ++ (para que se ajuste al límite de tiempo de ejecución permitido). Aunque una solución Java simple que memoriza la duración del ciclo de cualquier ciclo ya calculado funcionará, no he tenido éxito en aplicar la idea para obtener una solución Haskell.
He probado Data.Function.Memoize, así como la técnica de memorización de tiempo de registro elaborada en casa utilizando la idea de esta publicación: /programming/3208258/memoization-in-haskell . Desafortunadamente, la memorización en realidad hace que el cálculo del ciclo (n) sea aún más lento. Creo que la desaceleración proviene de la parte superior del camino Haskell. (Intenté ejecutar con el código binario compilado, en lugar de interpretar).
También sospecho que simplemente iterar números de i a j puede ser costoso ($ i, j \ le10 ^ 6 $). Así que incluso intenté calcular previamente todo para la consulta de rango, usando la idea de http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html . Sin embargo, esto todavía da el error "Límite de tiempo excedido".
¿Puedes ayudar a informar un programa Haskell competitivo y ordenado para esto?
fuente
Respuestas:
Contestaré en Scala, porque mi Haskell no es tan nuevo, por lo que la gente creerá que esta es una pregunta general de algoritmo de programación funcional. Me atendré a las estructuras de datos y conceptos que son fácilmente transferibles.
Podemos comenzar con una función que genera una secuencia de collatz, que es relativamente sencilla, excepto por la necesidad de pasar el resultado como un argumento para hacerlo recursivo:
Esto realmente pone la secuencia en orden inverso, pero eso es perfecto para nuestro próximo paso, que es almacenar las longitudes en un mapa:
Llamaría esto con la respuesta del primer paso, la longitud inicial y un mapa vacío, como
calculateLengths(collatz(22), 1, Map.empty))
. Así es como se memoriza el resultado. Ahora necesitamos modificarcollatz
para poder usar esto:Eliminamos el
n == 1
cheque porque solo podemos inicializar el mapa1 -> 1
, pero necesitamos agregar1
a las longitudes que ponemos en el mapa dentrocalculateLengths
. Ahora también devuelve la longitud memorizada donde dejó de repetirse, que podemos usar para inicializarcalculateLengths
, como:Ahora que tenemos implementaciones relativamente eficientes de las piezas, necesitamos encontrar una manera de alimentar los resultados del cálculo anterior en la entrada del siguiente cálculo. Esto se llama a
fold
, y se ve así:Ahora para encontrar la respuesta real, solo necesitamos filtrar las claves en el mapa entre el rango dado, y encontrar el valor máximo, dando un resultado final de:
En mi REPL para rangos de tamaño 1000 más o menos, como la entrada de ejemplo, la respuesta regresa casi instantáneamente.
fuente
Karl Bielefeld ya ha respondido bien a la pregunta, solo agregaré una versión de Haskell.
Primero, una versión simple y no memorable del algoritmo básico para mostrar la recurrencia eficiente:
Eso debería ser casi autoexplicativo.
Yo también usaré un simple
Map
para almacenar los resultados.Siempre podemos buscar nuestros resultados finales en la tienda, por lo que para un solo valor la firma es
Comencemos con el caso final
Sí, podríamos agregar eso de antemano, pero no me importa. Siguiente caso simple por favor.
Si el valor está ahí, entonces está. Sigo sin hacer nada.
Si el valor no está ahí, tenemos que hacer algo . Pongamos el en una función local. Observe cómo esta parte se parece mucho a la solución "simple", solo que la recursión es un poco más compleja.
Ahora finalmente hacemos algo. Si encontramos el valor calculado en la
store''
(nota al margen: hay dos resaltadores de sintaxis de haskell, pero uno es feo, el otro se confunde con el símbolo primo. Esa es la única razón para el doble cebado), simplemente agregamos el nuevo valor. Pero ahora se pone interesante. Si no encontramos el valor, tenemos que calcularlo y hacer la actualización. ¡Pero ya tenemos funciones para ambos! EntoncesY ahora podemos calcular un solo valor de manera eficiente. Si queremos calcular varios, simplemente pasamos a la tienda a través de un pliegue.
(Es aquí donde puedes inicializar el caso 1/1).
Ahora todo lo que tenemos que hacer es extraer el máximo. Por ahora no puede haber un valor en la tienda que sea superior a uno en el rango, por lo que es suficiente decir
Por supuesto, si desea calcular varios rangos y compartir la tienda entre esos cálculos también (los pliegues son su amigo), necesitaría un filtro, pero ese no es el enfoque principal aquí.
fuente
Data.IntMap.Strict
debe utilizar.