Cualquier indicador sobre cómo resolver eficientemente la siguiente función en Haskell, para grandes números (n > 108)
f(n) = max(n, f(n/2) + f(n/3) + f(n/4))
He visto ejemplos de memorización en Haskell para resolver números de Fibonacci, lo que implicaba calcular (perezosamente) todos los números de Fibonacci hasta el n requerido. Pero en este caso, para un n dado, solo necesitamos calcular muy pocos resultados intermedios.
Gracias
haskell
memoization
Angel de Vicente
fuente
fuente
Respuestas:
Podemos hacer esto de manera muy eficiente haciendo una estructura que podamos indexar en tiempo sub-lineal.
Pero primero,
Definamos
f
, pero hagamos que use 'recursión abierta' en lugar de llamarse a sí mismo directamente.Puede obtener un no conmemorado
f
usandofix f
Esto le permitirá probar que
f
hace lo que quiere decir con valores pequeñosf
llamando, por ejemplo:fix f 123 = 144
Podríamos memorizar esto definiendo:
Eso funciona aceptablemente bien y reemplaza lo que iba a tomar tiempo O (n ^ 3) con algo que memoriza los resultados intermedios.
Pero todavía lleva tiempo lineal solo indexar para encontrar la respuesta memorable
mf
. Esto significa que resultados como:son tolerables, pero el resultado no escala mucho mejor que eso. ¡Podemos hacerlo mejor!
Primero, definamos un árbol infinito:
Y luego definiremos una forma de indexarlo, para que podamos encontrar un nodo con índice
n
en tiempo O (log n) en su lugar:... y podemos encontrar un árbol lleno de números naturales que sea conveniente para no tener que jugar con esos índices:
Como podemos indexar, puede convertir un árbol en una lista:
Puede verificar el trabajo hasta ahora verificando que
toList nats
le da[0..]
Ahora,
funciona igual que con la lista anterior, pero en lugar de tomar tiempo lineal para encontrar cada nodo, puede perseguirlo en tiempo logarítmico.
El resultado es considerablemente más rápido:
De hecho, es mucho más rápido que puede pasar y reemplazar
Int
con elInteger
anterior y obtener respuestas ridículamente grandes casi instantáneamentefuente
f_tree
debería definirse en unawhere
cláusula para evitar guardar rutas innecesarias en el árbol a través de las llamadas.La respuesta de Edward es una joya tan maravillosa que la he duplicado y proporcioné implementaciones
memoList
ymemoTree
combinadores que memorizan una función en forma recursiva abierta.fuente
No es la forma más eficiente, pero recuerda:
cuando se solicita
f !! 144
, se verifica quef !! 143
existe, pero no se calcula su valor exacto. Todavía se establece como un resultado desconocido de un cálculo. Los únicos valores exactos calculados son los necesarios.Inicialmente, en cuanto a cuánto se ha calculado, el programa no sabe nada.
Cuando hacemos la solicitud
f !! 12
, comienza a hacer alguna coincidencia de patrones:Ahora comienza a calcular
Esto recursivamente hace otra demanda en f, entonces calculamos
Ahora podemos hacer una copia de seguridad de algunos
Lo que significa que el programa ahora sabe:
Continuando a gotear:
Lo que significa que el programa ahora sabe:
Ahora continuamos con nuestro cálculo de
f!!6
:Lo que significa que el programa ahora sabe:
Ahora continuamos con nuestro cálculo de
f!!12
:Lo que significa que el programa ahora sabe:
Por lo tanto, el cálculo se realiza con bastante pereza. El programa sabe que
f !! 8
existe algún valor para , que es igual ag 8
, pero no tiene idea de quég 8
es.fuente
g n m = (something with) f!!a!!b
Esta es una adición a la excelente respuesta de Edward Kmett.
Cuando probé su código, las definiciones de
nats
yindex
parecían bastante misteriosas, así que escribí una versión alternativa que me resultó más fácil de entender.Defino
index
ynats
en términos deindex'
ynats'
.index' t n
se define sobre el rango[1..]
. (Recuerde queindex t
se define sobre el rango[0..]
). Funciona busca en el árbol al tratarlon
como una cadena de bits y leer los bits a la inversa. Si el bit es1
, toma la rama de la derecha. Si el bit es0
, toma la rama de la izquierda. Se detiene cuando alcanza el último bit (que debe ser a1
).Así como
nats
se define para,index
esoindex nats n == n
siempre es cierto,nats'
se define paraindex'
.Ahora,
nats
yindex
son simplesnats'
yindex'
pero con los valores desplazados por 1:fuente
Como se indica en la respuesta de Edward Kmett, para acelerar las cosas, debe almacenar en caché los costosos cálculos y poder acceder a ellos rápidamente.
Para mantener la función no monádica, la solución de construir un árbol perezoso infinito, con una forma adecuada de indexarlo (como se muestra en publicaciones anteriores) cumple ese objetivo. Si renuncia a la naturaleza no monádica de la función, puede usar los contenedores asociativos estándar disponibles en Haskell en combinación con mónadas "estatales" (como State o ST).
Si bien el inconveniente principal es que obtiene una función no monádica, ya no tiene que indexar la estructura usted mismo, y solo puede usar implementaciones estándar de contenedores asociativos.
Para hacerlo, primero debe volver a escribir su función para aceptar cualquier tipo de mónada:
Para sus pruebas, aún puede definir una función que no memorice usando Data.Function.fix, aunque es un poco más detallado:
Luego puede usar State Mónada en combinación con Data.Map para acelerar las cosas:
Con cambios menores, puede adaptar el código para que funcione con Data.HashMap en su lugar:
En lugar de estructuras de datos persistentes, también puede probar estructuras de datos mutables (como Data.HashTable) en combinación con la mónada ST:
En comparación con la implementación sin ninguna memorización, cualquiera de estas implementaciones le permite, para grandes entradas, obtener resultados en microsegundos en lugar de tener que esperar varios segundos.
Utilizando Criterion como punto de referencia, pude observar que la implementación con Data.HashMap en realidad funcionó un poco mejor (alrededor del 20%) que Data.Map y Data.HashTable para los cuales los tiempos fueron muy similares.
Los resultados del punto de referencia me parecieron un poco sorprendentes. Mi sensación inicial fue que HashTable superaría la implementación de HashMap porque es mutable. Puede haber algún defecto de rendimiento oculto en esta última implementación.
fuente
Un par de años después, miré esto y me di cuenta de que hay una manera simple de memorizar esto en tiempo lineal usando
zipWith
una función auxiliar:dilate
tiene la práctica propiedad quedilate n xs !! i == xs !! div i n
.Entonces, suponiendo que se nos dé f (0), esto simplifica el cálculo a
Se parece mucho a nuestra descripción original del problema y da una solución lineal (
sum $ take n fs
tomará O (n)).fuente
Otra adición a la respuesta de Edward Kmett: un ejemplo autónomo:
Úselo de la siguiente manera para memorizar una función con un solo argumento entero (por ejemplo, fibonacci):
Solo se almacenarán en caché los valores para argumentos no negativos.
Para almacenar también valores de caché para argumentos negativos, use
memoInt
, definido de la siguiente manera:Para almacenar en caché los valores de las funciones con dos argumentos de uso entero
memoIntInt
, definidos de la siguiente manera:fuente
Una solución sin indexación, y no basada en Edward KMETT.
Factorizo subárboles comunes a un padre común (
f(n/4)
se comparte entref(n/2)
yf(n/4)
, yf(n/6)
se comparte entref(2)
yf(3)
). Al guardarlos como una sola variable en el padre, el cálculo del subárbol se realiza una vez.El código no se extiende fácilmente a una función de memorización general (al menos, no sabría cómo hacerlo), y realmente tiene que pensar cómo se superponen los subproblemas, pero la estrategia debería funcionar para múltiples parámetros generales no enteros . (Lo pensé para dos parámetros de cadena).
El memo se descarta después de cada cálculo. (Nuevamente, estaba pensando en dos parámetros de cadena).
No sé si esto es más eficiente que las otras respuestas. Cada búsqueda es técnicamente solo uno o dos pasos ("Mire a su hijo o al hijo de su hijo"), pero puede haber mucho uso de memoria adicional.
Editar: esta solución aún no es correcta. El intercambio es incompleto.Editar: Debería estar compartiendo los hijos secundarios correctamente ahora, pero me di cuenta de que este problema tiene una gran cantidad de compartir no trivial:
n/2/2/2
yn/3/3
podría ser el mismo. El problema no encaja bien con mi estrategia.fuente