Beruflich Dokumente
Kultur Dokumente
Proyecto 1
Problema del Caballo
Integrantes:
Jaua, Nicolás
15-10732
Lerones, Marcos
15-10778
Introducción:
En este proyecto analizamos el problema del caballo o “knight’s tour”, donde dado un
tablero de tamaño n*n se busca un recorrido que pase por cada casilla del tablero una sola
vez, usando movimientos de un caballo de ajedrez. Con este trabajo conseguimos dos
modos de encontrar soluciones, a través de la fuerza bruta, y ayudados por la regla
Warnsdorff, y usando el método de dividir y conquistar o “divide and conquer”. También
creamos un algoritmo que permite al usuario buscar su propio recorrido.
Primero dibujamos el tablero en cada turno con dibujarTabMan, luego vemos si ya terminó
el camino, ya sea porque recorrió todas las casillas (ct==n2-1) o porque ya no tiene más
movimientos (función overMan). Luego se pide el siguiente movimiento, si desea deshacer,
se eliminan los últimos elementos de Hx y Hy y se vuelve a llamar a manual desde la
posición anterior. Si no desea deshacer, se ve si su movimiento es válido (función valid) y si
lo es se llama a manual desde esa posición. Si el movimiento es inválido, se vuelve a llamar
a manual desde Kx,Ky.
-Fuerza Bruta:
Creamos la función “brute” que recibe un alto y un ancho para el tablero, la posición del
caballo, un tablero con el orden de las posiciones visitadas y ‘ct’, un contador del número de
casillas visitadas. Al iniciar se ingresa el tablero lleno de ‘-1’ excepto en la posición (0,0), se
ingresa el contador en 1, y Kx, Ky, que son las coordenadas del caballo, en 0 cada una.
Dentro de la función lo primero que se hace es revisar si el contador alcanzó el valor de n2,
de modo que dibuja el tablero y devuelve 1, finalizando el programa. En caso de que esto
no se cumpla, revisa con la función over si el caballo llegó a una posición donde no tiene
más movimientos, de ser así cambia la casilla a -1, de manera de que se cuente como
vacía, y devuelve 0 para que pase a la siguiente opción posible, en vista de que a través de
ella no se llega al resultado.
Luego de esto el programa usa una heurística para ir hacia los caminos más probables
primero. Se crea un arreglo “moves” que guarda los numeros del 0 al 7, que al ser llamados
se usarán como índice de las listas vert y horiz, que indican cómo moverse desde la casilla
actual. También se crea un arreglo “wandsorf” de largo 8 cuyas casillas son 10, de manera
que siempre sea mayor al número de casillas a las que se puede acceder, que es máximo
8.
A continuación hay un ciclo donde se revisa cuántos movimientos hay desde cada una de
las casillas accesibles desde donde está el caballo. Esto será almacenado en “wandsorf”, y
en el caso de que una casilla no sea válida, tendrá el valor inicial que se le había dado a
toda la lista, que es 10.
Después del ciclo se usa la función quicksort randomizado hecha en el laboratorio, de
manera que ordena moves en base a lo guardado en “wandsorf”. De esta manera en moves
quedarán los movimientos posibles en orden de conveniencia.
Finalmente se hace un ciclo donde i se mueve en la lista moves, y se le asigna a ‘x’ y ‘y’ los
valores que resultan de vert y horiz en i. Se revisa si moverse a x,y sería válido y de ser así
se le asigna el contador a esta casilla y se llama recursivamente a la función brute, que si
devuelve un 1 significa que llega a completar el tablero y si devuelve 0, entonces se pondrá
la casilla donde se probó como vacía y se volverá al comienzo del ciclo. Si el ciclo llega a
terminar sin resultado esto será porque ninguna opción desde esa casilla permite terminar el
recorrido, por lo que se le asignará -1 a la casilla donde se probó y se devolverá 0. De haber
un recorrido se llegará a este y se dibujará el tablero y se retornará 1 hasta salir de la
función; y de no haberlo, se retornará 0 hasta salir de la función, donde que por devolver 0,
el programa imprimirá que no hay recorrido posible.
● m==n ^ m%2==1: En este tenemos dos casos, m//2 es par o m//2 es impar. Si es
par, se crea recursivamente un camino cerrado y estructurado para (m//2)x(m//2),
uno para (m//2)x(m//2 + 1), uno para (m//2 + 1)x(m//2) rotando el anterior, y uno para
(m//2 +1)x(m//2 +1). Si se desea que sea cerrado, estructurado y sin una esquina, se
unen con unir_simple, sino, se usa la función unir impar que une 4 tableros cerrados
y estructurados en el que uno no tiene una esquina, para crear un camino abierto.
unir_impar funciona de la siguiente forma:
Si m//2 es impar se sigue el mismo procedimiento, con la única diferencia de que
ahora el tablero que no tendrá una esquina es el (m//2)x(m//2) en lugar del (m//2
+1)x(m//2 +1)
● n==m+1: Este caso se usa para completar tableros n*n, donde n es impar. A su vez
se divide en 4 casos: si m es divisible entre 4, si n es divisible entre 4, si m es par y
no es divisible entre 4 y si n es par y no es divisible entre 4. En todos los casos se
creará una variable l=m//2 si m es par o l=n//2 si n es par. En el caso donde m es
divisible entre 4 se dividirán los lados del tablero. m quedará como l+l y n como
l+(l+1), y esto resultará en dos tableros l*l y dos tableros l*(l+1) que serán calculados
recursivamente y unidos con unir_simple. Si n es divisible entre 4 se resultarán dos
tableros (l-1)*l y dos tableros l*l, que también serán calculados recursivamente.
Estos funcionan ya que l es par, y siempre que un tablero tenga uno de sus lados
pares y sean mayores que 4 va a haber una solución cerrada que abarque todas las
casillas. En caso de que l sea impar, es decir, m o n sean pares pero no divisibles
entre 4, no sé podrá dividir de igual manera que en los casos anteriores, ya que
estos los tableros cuadrados l*l con l impar no tienen una solución cerrada. Por esto,
para m par y no divisible entre 4, se divide la altura del tablero (m) en l+1 y l-1, y el
ancho del tablero (n) en l y l+1. Esto resulta en cuatro tableros de diferente tamaño:
(l+1)*l,(l+1)*(l+1), (l-1)*l y (l-1)*l+1; en lo que todos tienen al menos un lado para y
serán unidos usando la función unir_simple. De igual manera, para n par pero no
divisibles entre 4 se divide la altura en l y l-1 y el ancho en l+1 y l-1, resultando en
cuatro subtableros: l*(l+1), l*(l-1), (l-1)*(l+1) y (l-1)*(l-1), que también serán unidos
por unir simple. En los casos como l*(l-1) o (l+1)*l se calculan tableros (l-1)*l y l*(l+1),
que luego se rotarán, debido a que divide and conquer solo calculate tableros donde
m es menor o igual que n, es decir, la altura menor o igual que el ancho.
● n==m+2 ^ n%2==0: Este caso se usa únicamente para calcular los tableros
(l-1)*(l+1) usados en el caso donde n==m+1. En vista de que ambos son pares
simplemente se calcularán recursivamente cuatro tableros (m//2)*(n//2), que serán
unidos con unir_simple.
Complejidad:
-BackTracking:
La complejidad de nuestra implementación para el back-tracking es de orden O(2(n^2)). Esta
cota fue calculada viendo la cantidad de posibles estados que puede tener un tablero nxn,
teniendo un 1 si la casilla ya fue visitada o 0 sino. Nuestro problema nunca puede alcanzar
estos 2(n^2) estados, pero aún así nos sirve como cota superior para el tiempo nuestro
algoritmo. En el mejor caso nuestra implementación corre en tiempo lineal con respecto a la
cantidad de casillas O(n2).
También añadimos a nuestra implementación del backtracking una heurística (la regla de
Warnsdorff) que consiste en visitar la siguiente casilla que tenga menos vecinos no
visitados, lo cual tiene un costo extra en operaciones para aplicarla, pero nos ayuda a
encontrar una solución más rápido, ya que debe probar menos casos.
-NotSoBrute:
Tiene la misma complejidad que el anterior y usa los mismo principios, pero tiene otras
consideraciones ya que debe producir una solución estructurada y cerrada para poder ser
usada como caso base en la recursión del D&C.
-D&C:
Nuestro algoritmo D&C tarda T(n) = 4T(n/4) + O(n2), porque se divide en 4 el tablero y el
proceso de unir los 4 toma O(n2) operaciones, entonces, por tercer caso del Teorema
Maestro, el algoritmo tarda O(n2). Nótese que los casos base fueron precalculados con otro
programa y almacenados en el archivo “base_case.txt”, por lo que responder un caso base
cuesta O(1), ya que solo hay que leer del archivo de texto.
Conclusiones:
-Logros:
Con este proyecto logramos los objetivos de calcular recorridos de caballos de ajedrez en
un tablero a través de 3 métodos diferentes. Creamos un modo manual que permite al
usuario buscar el recorrido por si mismo. Creamos un modo que usa backtracking para
conseguir un recorrido, que resuelve hasta tableros de tamaño 31*31. Y creamos un modo
que a través del uso de tableros más pequeños, los une permitiendo conseguir recorridos
en tableros de muy gran tamaño. Los tres modos fueron realizados exitosamente.
-Comparación de los algoritmos:
El algoritmo de fuerza bruta permite calcular recorridos abiertos que tengan hasta 31 de
tamaño, limitado por la cola de recursión de Python. Mientras tanto, el algoritmo de divide
and conquer logra hacer tableros, estructurados y cerrados para tamaños pares y abiertos
para impares para tamaños arbitrariamente grandes en tiempo O(n^2).
-Aprendizaje:
Realizando este proyecto nuestra experiencia con funciones recursivas y con el método
divide and conquer, ya que tuvimos que realizar funciones complejas de este tipo. Fue
necesario investigar profundamente sobre el tema, y a través de análisis profundo, ensayo y
error, e investigar fuimos consiguiendo lo necesario para crear las funciones y darnos
cuenta de lo que nos faltaba. Con este proyecto nos enfrentamos a múltiples llamadas de
error, que analizamos y exploramos el código para conseguir los errores.
-Recomendaciones:
Recomendamos utilizar la regla de Warnsdorff para acelerar el algoritmo de fuerza bruta.
Además, se deben tomar como casos base los tableros nxn para 5<=n<=9, nx(n+1) para
5<=n<=10 y nx(n+2) para n=6 y n=8.
Fuentes consultadas:
https://www.sciencedirect.com/science/article/pii/S0166218X04003488
“An efficient algorithm for the Knight’s your problem” de Ian Parberry