Sie sind auf Seite 1von 7

24 de mayo de 2018

Laboratorio de Algoritmos y Estructuras II


CI2692

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.

Como correr el programa:


Para correr este programa debemos correrlo desde la consola. Al abrirlo te pedirá los
números uno, dos o tres para escoger qué modalidad se quiere usar.
Al escoger 1 se entra en el modo manual, donde se le pedirá al usuario el tamaño que
quiere para el tablero y las coordenada en las que quiere comenzar. Después de esto se
cargará el tablero con el tamaño especificado y con las coordenadas dadas ocupadas por
una ‘k’, que representa la posición del caballo y el resto del tablero ocupado por ‘o’ o por
números del 0 al 7. Tanto las ‘o’ como los números son casillas vacías. Los números
representan casillas accesibles desde la casilla actual, y al ingresar alguno de los números
visibles en el tablero el caballo se moverá a esa casilla, dejando una x en las casillas ya
visitadas. Después de un movimiento se puede deshacer la jugada ingresando ‘-1’ a la
consola. Al rellenar el tablero, se mostrará lleno de x, excepto por la última posición del
caballo, donde estará una k y culminará el programa. Si se llega a una posición sin salida en
la que el tablero no está resuelto, el programa también culminará.
Al escoger la opción 2 se entra en el modo backtracking, donde se le pedirá al usuario el
tamaño del tablero y devolverá un tablero relleno con números del 0 al n​2​-1, donde estos
describen el orden, siendo el 0 la casilla inicial, el 1 la siguiente y así consecutivamente.
Después de imprimir el tablero, el programa culmina. Si para el n dado no existen recorridos
posibles, el programa devolverá “No existe un recorrido posible para el n dado”. La opción 3
ofrecerá respuestas de la misma forma que la opción 2.

Como se modeló el tablero:


Creamos un tablero en la consola usando ‘|’ para separar cada casilla. En el modo manual
el tablero tiene al inicio 'o’ en las casillas vacías y 'k’ en la casilla ocupada por el caballo. Al
moverse las casillas por las que ya se pasó van a tener una x. En las casillas que pueden
ser accedidas desde donde está el caballo se mostrarán números del 0 al 7, que al ser
ingresados en la consola harán que el caballo se mueva a esa posición. El tablero se
imprimirá en cada turno.
En el modo Brute y Divide and Conquer el tablero se imprime una sola vez, de haber un
recorrido posible. Mostrará números del 0 al n​2​-1 siendo el 0 la posición inicial del caballo, y
para cada i, i+1 va a ser la posición siguiente. n​2​-1 va a ser la posición final del caballo, y si
desde el n​2​-1 se puede llegar al 0, el recorrido es cerrado.

Como se almacenó el recorrido:


Para la modalidad manual, se almacena el recorrido con las listas Hx y Hy que te dicen las
posiciones por las que ha pasado el caballo en orden. Se hizo así para facilitar la
implementación de la función de Undo o Deshacer. Además se tiene un arreglo que
representa el tablero, y tiene 1 si ya fue visitado ó 0 sino.
Por otro lado, en las otras dos modalidades se usa un arreglo cuadrado nxn inicializado en
-1 (número que indica que una casilla no ha sido visitada) y a medida que se va resolviendo
el tablero se enumeran las casillas por donde pasa el caballo de 0 a n​2​-1. Esto se hizo así
para facilitar la unión de varios tableros en el D&C.

Como se implementaron las 3 modalidades:


-Manual:
Creamos una función manual que recibe 7 argumentos:
● n: El tamaño del tablero
● Kx,Ky: La posición actual del caballo
● V: El tablero lógico con las posiciones visitadas
● ct: La cantidad de movimientos realizados
● Hx,Hy: Historial de los movimientos realizados por el jugador

Primero dibujamos el tablero en cada turno con dibujarTabMan, luego vemos si ya terminó
el camino, ya sea porque recorrió todas las casillas (ct==n​2​-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 n​2​,
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.

-Divide and Conquer:


Antes de hacer la función del Divide and Conquer se calcularon los casos base con la
función “NotSoBrute”, que funciona como la usada en la Fuerza Bruta, pero de manera que
forme recorridos estructurado, es decir que siempre que el caballo pueda acceder a una de
las esquinas, entre a esta, y que si está en una de las casillas vecina a una esquina vaya a
la casilla que está a dos posiciones de esa esquina. También revisa que los recorridos
resultantes son cerrados, es decir que desde la última casilla del recorrido se podría
acceder a la primera a través de un movimiento de caballo. Se usó esta función para
calcular todos los casos base: 6*6, 8*8, 5*6, 6*7, 7*8, 8*9, 9*10, 10*11, 6*8, 8*10, y 5*5, 7*7
y 9*9, pero estos tres últimos con dos casos diferentes, uno en el que el recorrido no pasa
por una esquina, pero por el resto de las casillas que pasa forma un recorrido cerrado y
estructurado, y otro caso donde resuelve el tablero completo pero el recorrido no es
cerrado, equivalente a un resultado de 5*5, 7*7 o 9*9 con la función brute. Estos casos son
guardados en un archivo .txt que después será leído durante la función de Divide and
Conquer.
La función en si solo pide el largo y ancho del tablero, y una variable booleana que
almacena si esta es la primera vez que se llama a la función, para saber cómo devolver los
tableros con largo y ancho impar. La función va a devolver un tablero con el recorrido de la
misma forma en que lo devuelve brute. Luego la función presenta a través de ifs los
diferentes casos que se pueden dar en la búsqueda de un recorrido de un tablero n*n.
Si el tablero es uno de los casos base, se carga el resultado del archivo “base_case.text”. Si
no, para resolver un tablero m*n, hay varios casos:

● m==n ^ m%4==0: En este caso, se calcula recursivamente el resultado del tablero


(n/2)*(n/2) y se crean 4 tableros iguales con esta solución. Como los 4 son cerrados
y estructurados, podemos unirlos para crear un tablero n*n cerrado y estructurado, y
esto lo hacemos mediante la función unir_simple. unir_simple recibe 4 tableros con
caminos estructurados y cerrados y los une de la siguiente forma.
● m==n ^ m%2==0 ^ m%2!=0: En este caso se calcula recursivamente un camino en
un tablero (n/2)x(n/2) que no pasa por la esquina inferior derecha, y que es cerrado y
estructurado. Se crean otros 3 tableros rotando 90, 180 y 270 grados el
anteriormente calculado. Luego se unen con unir complex que lo hace de la
siguiente forma.

● 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(n​2​).
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(n​2​), porque se divide en 4 el tablero y el
proceso de unir los 4 toma O(n​2​) operaciones, entonces, por tercer caso del Teorema
Maestro, el algoritmo tarda O(n​2​). 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

Das könnte Ihnen auch gefallen