Publiqué esta pregunta over at CodeReview, pero me di cuenta de que no es tanto una pregunta de Haskell como una cuestión de algoritmo.¿Hay alguna manera de evitar recursiones innecesarias?
El código de Haskell se puede encontrar on my github repo pero creo que el código no es tan importante como el concepto general.
Básicamente, el programa descubre el primer conjunto óptimo de movimientos en un juego de Kalaha (la variación sueca). Solo se considera el primer "giro", por lo que suponemos que puede comenzar y que no se computa nada desde el punto en que el oponente se mueve.
Kalaha board http://www.graf-web.at/mwm/kalaha.jpg
La junta comienza con tiendas vacías y una cantidad igual de canicas en cada maceta.
Comience su turno eligiendo una olla no vacía de su lado, recoja todas las canicas de esa olla y luego mueva alrededor de la tabla dejando caer una canica al pasar una olla. Si tus últimas tierras de mármol en la tienda, obtienes otro turno. Si aterrizas en un bote que no esté vacío, no guardas, recoges todo el contenido de ese bote y continúas. Finalmente, si aterrizas en una olla vacía, el turno pasa al oponente.
Hasta ahora he resuelto esto seleccionando todos los caminos posibles y luego ordenándolos según la cantidad de canicas en la tienda al final. Un camino significaría comenzar desde una de tus ollas, hacer todo lo necesario para moverte y ver si aterrizas en una tienda o en una olla vacía. Si aterrizas en la tienda, puedes continuar, y ahora hay tantas ramas nuevas como ollas no vacías a tu lado.
El problema radica en el hecho de que si comienzas con cinco canicas en las macetas, ya hay bastantes caminos. Saltar hasta seis y ghci se queda sin memoria.
La razón por la que no puedo entender cómo hacer que esto sea menos costoso es porque creo que cada ruta es necesaria durante el cálculo. Aunque solo necesitaré como máximo tres rutas (las mejores) de las miles (o millones) generadas, el resto debe revisarse para ver si en realidad son mejores que las anteriores.
Si uno es más largo (generalmente mejor), entonces eso es bueno, pero costoso. Si es más corto que cualquier ruta anterior, entonces el programa aún tenía que calcular esa ruta para descubrirlo.
¿Hay alguna forma de evitar esto o está computando todas las rutas necesarias por definición?
Para calcular el mejor movimiento sin intentar mirar hacia adelante a través de los movimientos del oponente, puede ser posible emplear programación dinámica aquí: cada vez que calcule el mejor movimiento desde un estado de juego particular, almacene el movimiento y el estado resultante en una mesa. Podría haber muchos estados (hasta tantos como la cantidad de formas de dividir 30 en 6 contenedores), pero creo que no se puede tener en cuenta el número de canicas en las tiendas. –
No tengo una prueba, pero creo que debería ser posible ganar el juego en un turno. Una regla que no se mencionó en el texto es que marque todas las canicas en los hoyos del oponente si se queda sin canicas jugables, por lo que el objetivo es quedarse sin canicas en un turno para un juego perfecto. Si los movimientos que terminan en hoyos vacíos no se tienen en cuenta, el problema se reducirá a una complejidad exponencial en lugar de al menos una dificultad NP. – dflemstr
Me gustaría señalar que "calcular todas las rutas" no necesariamente implica "quedarse sin memoria", si puede recoger las rutas que ya ha considerado. –