Lógica: práctica 1

Resolviendo el 8 puzzle


En esta práctica vamos a implementar un programa en Prolog para resolver el 8-puzzle usando tres algoritmos de planificación basados en búsqueda "ciega" o "no informada" (esto es, exploración de todas las posibilidades sin uso de información heurística).

El 8-puzzle es un problema muy sencillo en el que tenemos un tablero de 3 x 3 posiciones con 8 pequeñas fichas cuadradas numeradas del 1 al 8 además de una posición hueca. En cada transición, se puede mover hacia el hueco una cualquiera de las fichas adyacentes. El problema consiste en alcanzar una configuración final predeterminada, a partir de una configuración inicial "desordenada". Por ejemplo, podríamos tener la situación de esta figura:

8
                    puzzle

En el estado inicial, los posibles movimientos son tres: mover la ficha 8 a la derecha, la 2 hacia abajo o la 6 hacia la izquierda. Por simplicidad, representaremos más bien el movimiento del hueco (en direcciones cardinales n, s, w, e), de modo que las acciones posibles en este estado inicial serían w (mover el hueco al oeste, intercambiando con el 8), n (mover el hueco al norte, intercambiando con el 2) o e (mover el hueco al este, intercambiando con el 6). Se desea obtener la secuencia de movimientos que nos permita llegar al estado final de la figura derecha, que supondremos que siempre es ese mismo estado meta predertimado. Para el estado inicial del ejemplo, se puede comprobar que un plan válido sería la secuencia de movimientos [w,n,n,e,s,e,s] (de hecho, ese es el plan más corto para llegar a la meta).

Programando el planificador en Prolog

Para representar un estado del puzle, usaremos una lista de 9 números, de modo que el 0 represente la posición del hueco. Por ejemplo, el estado inicial de arriba correspondería a la lista [4,1,3,7,2,5,8,0,6]. Igualmente, el estado meta será siempre la lista [1,2,3,4,5,6,7,8,0].

El predicado principal que debemos implementar será plan(Modo,S0,Plan) donde: Modo es un dato de entrada fijando el tipo de algoritmo que vamos a usar (podrá ser iter, anchura, profund), S0 es una lista de entrada conteniendo el estado inicial, y Plan es una lista de acciones conteniendo el plan final obtenido. Ese predicado hará llamadas a otros, dependiendo el tipo de algoritmo a programar.

Otro predicado esencial del que haremos uso es expandir(S0, Accion, S1) que nos debe devolver, para cualquier estado S0, todas las combinaciones de Accion válida y estado siguiente S1 al que nos lleva esa acción. Como ejemplo de funcionamiento, tendríamos:

?- expandir([4,1,3,7,2,5,8,0,6],A,S).
A = n,
S = [4, 1, 3, 7, 0, 5, 8, 2, 6] ;
A = e,
S = [4, 1, 3, 7, 2, 5, 8, 6, 0] ;
A = w,
S = [4, 1, 3, 7, 2, 5, 0, 8, 6] ;
false.

Para hacer pruebas, usaremos estos 3 posibles estados iniciales:

initial1([1,2,3,7,4,6,5,0,8]).
initial2([4,1,3,7,2,5,0,8,6]).
initial3([1,6,2,3,0,8,4,7,5]).

Algoritmos de anchura y de profundidad

Para los algoritmos anchura y profund usaremos un mismo predicado llamado buscar que tiene el siguiente aspecto buscar(Modo,Nodos,Vistados,Plan) cuyos argumentos son:

  • Modo = será anchura o profund
  • Nodos = es una lista de elementos de la forma "nodo(P,S)" donde P es el plan parcial obtenido hasta el momento, y S es un estado dado. Esta lista contiene los estados que todavía se van a explorar. Inicialmente, la lista tendrá el aspecto [nodo([],S0)] de modo que S0 es el estado inicial y el plan parcial por ahora es vacío.
  • Visitados  = es una lista con todos los estados que hemos expandido hasta el momento. Inicialmente será vacía [].
  • Plan = es el plan final, con la solución del problema

El modo de operar del predicado buscar es el siguiente:

  • Tomamos el primer nodo n(P,S) de Nodos. Si el estado S es el meta, podemos parar y devolvemos el plan P
  • Si el estado S no es el meta, pero ya existe en la lista de Visitados, entonces lo ignoramos y pasamos a contemplar el siguiente nodo en Nodos
  • Si S no es meta ni ha sido visitado, entonces recopilamos en una lista de Expandidos todos los nodos que se pueden expandir a partir de S. Para ello, podemos usar

    findall(nodo([M | P],S1),expandir(S,M,S1),Expandidos)

    Si tenemos el algoritmo de profund, la lista de Expandidos se añadirá a la cabeza de la de Nodos, mientras que si tenemos el de anchura, los expandidos se añaden al final de los Nodos. Continuamos la búsqueda anotando el estado S como visitado y llamando de nuevo a buscar.

Algoritmo de profundización iterativa

El tercer algoritmo iter implementará la búsqueda por profundización iterativa. La idea es usar un algoritmo de backtracking con un límite de profundidad. Programaremos el predicado busca_limite(N,Max,S0,Plan0,Plan) que realizará una búsqueda por backtracking hasta un límite Max. El argumento N es la profundidad actual (comenzaremos en 0), mientras que S0 es el estado inicial. La lista Plan0 lleva el plan obtenido hasta ahora (de forma acumulada) y comienza siendo []. Por último, Plan devuelve el plan final obtenido. La idea es que, si N>Max, la búsqueda se detiene obligatoriamente.

Una vez que busca_limite esté funcionando, la búsqueda iter realizará llamadas sucesivas a busca_limite comenzando con Max=0 e incrementando el valor de Max de uno en uno hasta que aparezca algún plan.

Entrega y evaluación

Esta práctica tiene una puntuación máxima de 2 puntos (20% del total del curso). La práctica se realizará preferentemente en grupos de 2 alumnos. La entrega se realizará en una tarea del Campus Virtual (moodle): es suficiente con realizar la entrega en una de las dos cuentas de alumnos, pero el código debe incluir en las primeras líneas en comentarios los nombres de los dos componentes del grupo. Fecha de entrega: 24 de abril de 2026 (23:59).


Maintained by Pedro Cabalar.