You are on page 1of 9

LECCIÓN 10.

BÚSQUEDAS CON RETROCESO.

En esta lección se aplican las técnicas y los recursos expuestos en las lecciones anteriores
para implementar algunos algoritmos de búsqueda con retroceso (retroceso, profundización
progresiva, IDA*).

“Rectificar es de sabios.”
(Anónimo)

I.A.I.C.. 2006/2007 10.1 Dpto. Leng. Ciencias Comp.(U. Málaga)


I.A.I.C.. 2006/2007 10.2 Dpto. Leng. Ciencias Comp.(U. Málaga)
R.10.1. Búsqueda con retroceso a ciegas.
a) Implementar el algoritmo general de búsqueda con retroceso mediante una función
backtrack.
b) Implementar backtrack-c, una versión de backtrack con límite de profundidad en la
búsqueda. Con ayuda de dicha función implementar la función bid, que realice una búsqueda
con profundización progresiva.
c) Resolver el problema de los misioneros y caníbales (R.2.9) mediante bid.
**************************

SOLUCION:
a) El algoritmo de búsqueda con retroceso se puede describir así:
Comenzar con el estado inicial; si hemos alcanzado un estado final, devolver el camino desde el
estado inicial; en otro caso, si hemos alcanzado un estado para el que no hay más posibles
sucesores, retroceder al estado anterior; en otro caso, avanzar, generando un nuevo sucesor del
estado actual y pasando a él.
La estrategia de búsqueda con retroceso considera el nodo más profundo generado en cada
momento y selecciona un sucesor por donde continuar la búsqueda. Sólo en caso de que la
búsqueda a partir de dicho sucesor fracase se considera otro sucesor por donde continuar. De este
modo, la búsqueda conserva en todo momento un único camino o secuencia de estados, aunque
para cada estado es necesario también llevar la cuenta de cuáles de sus sucesores fueron ya
generados.
Supondremos que la clase que implementa los estados cumple con la interfaz definida para la
búsqueda a ciegas en R9.1.
Existen múltiples formas de implementar la búsqueda con retroceso. Examinaremos a continuación
una implementación sencilla inspirada en el procedimiento recursivo backtrack de (Nilsson,
1980). La búsqueda parte de un estado e y avanza probando ordenadamente cada uno de sus
sucesores. La secuencia de estados que se está considerando en cada momento corresponde a la
secuencia de llamadas recursivas. Dicho de otro modo, la pila de la recursión es la que almacena
en cada entorno los estados generados.

(defun backtrack (e)


(cond ((finalp e) (list e)) ;éxito
(t (dolist (e2 (hijos e) nil) ;retroceso
(let ((cam (backtrack e2))) ;avance
(when cam
(return (cons e cam)))))))) ;éxito

b) La implementación anterior es bastante concisa, pero adolece de un grave inconveniente: no


es posible realizar comprobación de ciclos. Por este motivo no será aplicable en la práctica a
muchos problemas, como el de los misioneros y caníbales.
Una alternativa es incorporar un límite de profundidad que, si bien no elimina el sobre-esfuerzo
de búsqueda provocado por los ciclos, al menos impide caer en una computación infinita (en la
práctica, un desbordamiento de pila).

(defun backtrack-c (e cota &optional (prof 0))


(cond ((finalp e) (list e))
((>= prof cota) nil) ;poda
(t (dolist (e2 (hijos e) nil)
(let ((cam (backtrack-c e2 cota (1+ prof))))
(when cam
(return (cons e cam))))))))

I.A.I.C.. 2006/2007 10.3 Dpto. Leng. Ciencias Comp.(U. Málaga)


El principal inconveniente de backtrack-c radica en la necesidad de estimar la cota de
profundidad a la que se realizará la búsqueda: si es menor que la profundidad de la solución la
búsqueda fracasará; si es mucho mayor podría requerir un enorme esfuerzo de búsqueda.
Este inconveniente puede paliarse empleando el procedimiento de retroceso con profundización
iterativa BID (Backtrack with Iterative Deepening). A continuación se muestra una versión
recursiva por la cola.

(defun bid (e &optional (cota 0))


(print cota)
(or (backtrack-c e cota)
(bid e (1+ cota))))

c) Podemos utilizar una solución similar a la presentada en R.2.9, aunque esta vez
emplearemos clases y la interfaz (finalp e) (hijos e) definida en R.9.1. Nótese que no
es necesario definir la operación (clave e) si no pretendemos utilizar la función (iguales e
e2).

(defclass mc ()
((m :reader misioneros :initarg :m)
(c :reader canibales :initarg :c)
(b :reader barcas :initarg :b)))

(defun hacer-mc (b m c)
(make-instance 'mc :b b :m m :c c))

(defmethod ver ((e mc))


(print (list (barcas e) (misioneros e) (canibales e))))

(defconstant *num-misioneros* 3)
(defconstant *num-canibales* 3)
(defconstant *num-barcas* 1)

(defmethod otra-b ((e mc))


(if (zerop (barcas e))1 0))

(defmethod otros-misioneros ((e mc))


(- *num-misioneros* (misioneros e)))

(defmethod otros-canibales ((e mc))


(- *num-canibales* (canibales e)))

(defmethod finalp ((e mc))


(and (zerop (misioneros e))
(zerop (canibales e))))

(defmethod signo-incr-der ((e mc))


(if (zerop (barcas e)) 1 -1))

(defmethod hijos ((e mc))


(remove-if #'imposiblep
(list (hacer-mc (otra-b e) ;pasa un misionero
(+ (misioneros e) (* (signo-incr-der e) 1)) (canibales e))
(hacer-mc (otra-b e) ;pasan dos misioneros
(+ (misioneros e) (* (signo-incr-der e) 2)) (canibales e))
(hacer-mc (otra-b e) ;pasa un canibal
(misioneros e)(+ (canibales e) (* (signo-incr-der e) 1)))
(hacer-mc (otra-b e) ;pasan dos canibales
(misioneros e) (+ (canibales e) (* (signo-incr-der e) 2)))
(hacer-mc (otra-b e) ;pasan uno y uno
(+ (* (signo-incr-der e) 1) (misioneros e))
(+ (* (signo-incr-der e) 1) (canibales e))))))

I.A.I.C.. 2006/2007 10.4 Dpto. Leng. Ciencias Comp.(U. Málaga)


(defmethod imposiblep ((e mc))
(or (not (<= 0 (misioneros e) *num-misioneros*))
(not (<= 0 (canibales e) *num-canibales*))
(not (<= 0 (barcas e) *num-barcas*))
(and (> (misioneros e) 0)
(> (canibales e) (misioneros e)))
(and (> (otros-misioneros e) 0)
(> (otros-canibales e) (otros-misioneros e)))))
El problema de los misioneros y caníbales se resuelve entonces simplemente con la función:

(defun mc () (dolist (e (bid (hacer-mc 1 3 3)))


(ver e)))

La solución se encuentra a profundidad 11:

> (mc)
0
1
2
3
4
5
6
7
8
9
10
11
(1 3 3)
(0 3 1)
(1 3 2)
(0 3 0)
(1 3 1)
(0 1 1)
(1 2 2)
(0 0 2)
(1 0 3)
(0 0 1)
(1 1 1)
(0 0 0)
NIL

I.A.I.C.. 2006/2007 10.5 Dpto. Leng. Ciencias Comp.(U. Málaga)


R.10.2 Algoritmo IDA*.
a) Definir una función backa-simple que realice una búsqueda con retroceso que calcule el
coste acumulado g(e) del camino explorado hasta llegar al estado actual e. La búsqueda
recibirá una cota de coste, y forzará un retroceso tan pronto como el valor de f(e) = g(e) +
h(e) supere dicha cota.
b) Definir una función ida que realice una búsqueda IDA* (Iterative Deepening A*), y resolver
con ella el puzzle-8 propuesto en la lección 9.

**************************
SOLUCION:
a) La función backa-simple recibe el estado e a explorar, la cota de coste, y el coste g
acumulado para llegar hasta e. Si el valor f(e) = g(e) + h(e) excede la cota de coste, se
fuerza un retroceso con fracaso. El procedimiento es muy similar al backtrack-c expuesto en
R.10.1. Simplemente hay que llevar la cuenta del coste acumulado del camino.

(defun backa-simple (e cota &optional (g 0))


(cond ((> (+ g (h e)) cota) nil)
((finalp e) (list e))
(t (dolist (e2 (hijos e) nil)
(let ((cam (backa-simple e2 cota (+ g (coste e e2)))))
(when cam
(return (cons e cam))))))))
Nótese que este procedimiento no es admisible. Si tiene éxito, únicamente garantiza que el
coste de la solución no excederá la cota, aunque el coste de la solución encontrada dependerá
del orden de exploración de caminos. Si termina con fracaso, es posible aún que existan
soluciones con coste mayor a la cota.

b) La búsqueda IDA* realiza llamadas sucesivas a un procedimiento de búsqueda con retroceso


acotado por un nivel o cota de coste (cota), y fuerza un retroceso si el valor valor f(e) del
camino actualmente en exploración excede la cota de coste.
En principio podría parecer que la función backa-simple definida anteriormente podría servir
a este propósito. Sin embargo, dicho procedimiento no es suficiente. Si se produce un fracaso,
la búsqueda IDA* sigue adelante cambiando la cota de coste por el menor de los valores f(e)
que excedieron la cota en la búsqueda anterior.
Definiremos por tanto una nueva función backa adecuada para las necesidades de IDA*. Se
tratará de una función multivaluada. El primer valor devuelto es bien fracaso (nil) o bien el
camino solución. El segundo valor es el menor de los valores de f(e) evaluados que
excedieron la cota de poda. Para poder calcular este valor adecuadamente, lo pasaremos como
un argumento adicional a la función backa.

(defun backa (e cota &optional (g 0)


(sig-poda most-positive-long-float))
(cond ((> (+ g (h e)) cota) (values nil (+ g (h e))))
((finalp e) (list e))
(t (dolist (e2 (hijos e) (values nil sig-poda))
(multiple-value-bind (cam otra-poda)
(backa e2 cota (+ g (coste e e2)) sig-poda)
(setf sig-poda (min sig-poda otra-poda))
(when cam
(return (cons e cam))))))))

I.A.I.C.. 2006/2007 10.6 Dpto. Leng. Ciencias Comp.(U. Málaga)


Ahora es fácil definir la función ida:

(defun ida (e &optional (poda (h e)))


(print poda)
(multiple-value-bind (sol sig-poda) (backa e poda)
(or sol (= poda sig-poda) (ida e sig-poda))))

Nótese que la función puede terminar con éxito, devolviendo la solución, con fracaso (si la poda
es igual al valor de sig-poda), o repetir la búsqueda con retroceso con el nuevo valor de sig-
poda.

Ahora podemos resolver el problema del puzzle-8 presentado en la lección 9 con la siguiente
llamada:

> (ida *estado-ini*)

I.A.I.C.. 2006/2007 10.7 Dpto. Leng. Ciencias Comp.(U. Málaga)


EJERCICIOS PROPUESTOS.

P.10.1. Disponemos de dos jarras, una de 5 litros de capacidad y la otra de 2 litros. La jarra grande
está llena y la pequeña vacía. Queremos que la jarra pequeña contenga 1 litro de agua. Para ello
podemos vaciar una jarra en la otra, o bien en el suelo, pero no emplear más agua que los 5 litros
iniciales. Se pide representar el problema como una búsqueda en espacio de estados y resolverlo
aplicando una búsqueda con retroceso.

P.10.2. a) Resolver nuevamente el problema P.9.2 utilizando la función BID. Comparar los
resultados con los obtenidos en P.9.2.
b) Resolver el problema del puzzle-8 utilizando las funciones bid e ida. Realizar pruebas de
búsqueda a distintas profundidades y comparar los resultados con los obtenidos en P.9.4.b-d.

P.10.3. El Mini Su Doku consiste en un tablero de 2x2 cajas. Cada caja contiene a su vez una rejilla
de 2x2 celdas. El problema consiste en rellenar cada celda con un dígito del 1 al 4 (ambos
inclusive) de modo que:
• Cada fila del tablero debe contener 4 dígitos distintos.
• Cada columna del tablero debe contener 4 dígitos distintos.
• Cada caja del tablero debe contener 4 dígitos distintos.
Por ejemplo, el tablero de la izquierda tiene como solución el tablero de la derecha:

3 4 2 3 1
2 4 1 3 2 4

3 1 3 1 4 2
2 2 4 1 3

Se pide lo siguiente:
a) Definir una clase adecuada para representar los posibles estados del Mini Su Doku.
b) Establecer un orden entre las casillas del tablero para la asignación de valores.
c) Escribir los métodos necesarios para resolver el problema mediante la función
backtrack. Concretamente, la función hijos devolverá una lista con tantos
sucesores como asignaciones consistentes existan para la siguiente casilla vacía en
el orden de asignación de valores (NIL si no hay ninguna). Por ejemplo. Para la
figura anterior de la izquierda, existirán dos sucesores posibles:

1 3 4 3
2 4 2 4

3 1 3 1
2 2

I.A.I.C.. 2006/2007 10.8 Dpto. Leng. Ciencias Comp.(U. Málaga)


d) Aplicar el procedimiento de búsqueda en árbol (backtrack) para encontrar la solución
del siguiente puzzle.

1
4

2
3 1

P.10.4 Considérese el problema de las N-Reinas donde se dispone de un tablero de ajedrez de N ×


N y se desean colocar N reinas R(1), R(2),...R(N) una en cada fila de modo que ninguna de ellas
ataque a otra (dos reinas se atacan mutuamente si se encuentran en la misma fila, columna o
diagonal).
a) Definir una clase adecuada para representar los posibles estados del problema.
b) Suponer que el estado inicial es un tablero vacío, y que los sucesores se
obtienen asignando una reina en las posiciones libres de la primera fila vacía.
c) Escribir los métodos necesarios para resolver el problema mediante la función
backtrack.

P.10.5. Modificar la implementación del algoritmo backtrack-c de R.10.1 de forma que se


devuelvan varias soluciones al problema de búsqueda. Supóngase que hay definida una constante
n. La nueva versión de backtrack-c devolverá una lista formada por las n primeras soluciones
encontradas. Si el número de soluciones es menor que n, se devolverá una lista con todas ellas.

P.10.6. a) Modificar la implementación del algoritmo backa de R.10.2 de forma que en cada
llamada se reciba el estado e a explorar, así como su antecesor inmediato en la búsqueda.
Cuando se generen los hijos de e se eliminará el ciclo sobre su antecesor.
b) Modificar la implementación de la función ida para que emplee este nuevo procedimiento de
búsqueda más eficiente.
c) Comparar el rendimiento de la nueva función sobre el problema del puzzle-8.

____________________________________________________________________________________
Las erratas y comentarios sobre estos apuntes son bienvenidos en: lawrence@lcc.uma.es

I.A.I.C.. 2006/2007 10.9 Dpto. Leng. Ciencias Comp.(U. Málaga)

You might also like