Proceedings Template
Transcription
Proceedings Template
Implementación del Juego puzzle-n por medio de dos algortimos: Backtracking y A* Guillermo Rodea Palomares Álvaro Hernández Benedicto NIA:10047435 NIA:100047311 ABSTRACT En este documento trataremos la implementación en java del juego puzzle-n, como generalización del juego puzzle-8, para ello, usaremos dos algoritmos distintos: Un algoritmo de tipo backtracking, del cual podremos ver que no resulta del todo eficiente; y uno de tipo A*, que como se verá resulta mucho mejor para abordar el problema planteado. Categories and Subject Descriptors Usaremos la distribución de implementación de los algoritmos. Java J2SE para la General Terms - Algoritmo: Lista de operaciones bien definida, finita y ordenada que permite encontrar la solución a un problema. Un algoritmo está caracterizado por 5 para propiedades: a) Carácter finito. b) Precisión: Cada paso del algoritmo está fijado de manera inequivoca. c) Entrada: Un algoritmo tiene cero o más entradas. d) Salida: un algoritmo tiene una o más salidas, que tienen una relación específica con las entradas. e) Eficacia. Figura 1 Grafo dirigido con pesos -Árbol: Se trata de un grafo conexo y sin ciclos. Consta de un nodo raíz del que surgen una serie de nodos hijos, a su vez de cada uno de estos nodos hijo pueden surgir o no una serie de nodos hijos. En la siguiente figura tenemos un ejemplo de árbol: Existen varias formas de expresar un algoritmo: a) Descripción de alto nivel: Se explica el algoritmo verbalmente o ayudado por dibujos. Se omiten detalles. Su objetivo es dar a conocer los fundamentos del algoritmo y el funcionamiento básico del mismo sin entrar en detalles. b) Pseudocódigo: También se conoce como descriptor formal, describe paso a paso el algoritmo en un lenguaje de tipo pseudocódigo. c) Implemnetación: Se muestra el algoritmo expresado en un lenguaje de programación específico o en cualquier otro soporte capaz de realizar opecariones. - Grafo: Grafo es un conjunto de nodos o vértices unidos por un conjunto de aristas o arcos, que permiten representar relaciones entre los elementos de un conjunto. Cada nodo y cada arista pueden o no tener un peso o coste asociados a su recorrido. En la siguiente figura tenemos un ejemplo de grafo con pesos en las aristas: Figura 2 Ejemplo de árbol - Puzzle-8: Se trata de un juego clásico en inteligencia artificial, su objetivo es, en un tablero de 3x3, conseguir alcanzar una colocación específica de las fichas a partir de un estado inicial dado. Para ello el único movimiento posible es “arrastrar” a la casilla vacía una de las fichas adyacentes. En la siguiente figura vemos un ejemplo: se alcanza una solución y en caso contrario, realiza una marcha atrás (backtracking) para explorar otra posible solución. El algoritmo prosigue para cada nodo que tiene uno o más hijos sin explorar. Al tratarse de un algoritmo de búsqueda en profundidad, su mayor problema es el tiempo de cómputo. Figura 3 Ej. 8Puzzle En la siguiente figura mostramos un ejemplo de los posibles movimientos y de cómo el árbol de búsqueda va desarrollándose con cada movimiento que realizamos: Con este algoritmo tenemos dos posibles condiciones de parada que determinarán la complejidad del algoritmo, tamaño de búsqueda y tiempo de cómputo. Si la condición de parada es la de encontrar simplemente la solución, no está garantizado encontrar la mejor solución. En caso de investigar todo el árbol completo, está garantizado encontrar la mejor solución, pero el tiempo de cómputo puede dispararse para estructuras muy grandes. En la siguiente figura vemos gráficamente el crecimiento de la complejidad de la solución con el número de pasos que se den, razón de que los tiempos de cómputo sean elevados para este algoritmo. Como puede apreciarse el algoritmo de backtracking recorre la solución en forma de árbol: Figura 4 Posibles movimientos de un 8 puzzle, y árbol desarrrollado. Este problema puede generalizarse aumentando las dimensiones del cuadrado a cualquier número tal que al sumarle uno y realizar su raíz cuadrada el resultado es un número natural, esto es, por ejemplo: 15, 24… - Complejidad computacional: Es el conjunto de recursos requeridos por un algoritmo para alcanzar una solución. Los principales recursos requeridos serán tiempo y memoria. - Pseudocódigo: Es un conjunto de normal léxicas y gramaticales similares los lenguajes de programación, pero sin llegar a la rigidez que tienen estos, está a medio camino entre el lenguaje coloquial y el de programación. Se utiliza para la explicación de manera más sencilla de algoritmos. - Algoritmo de Backtracking: Se trata de un recorrido en profundidad del problema a través de su grafo implícito. El algoritmo va desarrollando los pasos posibles en cada fase del problema, creando de esta manera un árbol de búsqueda. Para cada rama del árbol que es desarrollada el algoritmo comprueba si Figura 5 Arbol de ejecución de algoritmo de backtracking - Algoritmo A*: Es un algoritmo para encontrar caminos en grafos. Se basa en el hecho de que a veces para llegar a la solución hay que dar pasos que tienen un mayor coste que otros pasos posibles. Se trata de un algoritmo completo: Si hay solución la encuentra. Es un algoritmo de búsqueda que combina profundidad y anchura y por lo tanto, su mayor problema es la cantidad de memoria que necesita para ser almacenado. Pero aunque combina ambas estrategias, encuentra la solución óptima sin necesidad de tener que investigar todos los nodos. Lo explicamos a continuación. Como posible pseudocódigo tenemos el siguiente: ABIERTA=I, CERRADA=Vacío, EXITO=Falso Hasta que ABIERTA esté vacía O ÉXITO Quitar el primer nodo de ABIERTA, N y meterlo en CERRADA SI N es Estado-final ENTONCES EXITO=Verdadero SI NO Expandir N, generando el conjunto S de sucesores de N, que no son antecesores de N en el grafo Generar un nodo en G por cada s de S Establecer un puntero a N desde aquellos s de S que no estuvieran ya en G Añadirlos a ABIERTA Para cada s de S que estuviera ya en ABIERTA o CERRADA decidir si redirigir o no sus punteros hacia N Para cada s de S que estuviera ya en CERRADA decidir si redirigir o no los punteros de los nodos en sus subárboles Reordenar ABIERTA según f (n) Si EXITO Entonces Solución=camino desde I a N a través de los punteros de G Si no Solución=Fracaso Figura 6 Resultado obtenido por el algoritmo A* Aunque el algoritmo A* sirve para resolver grafos, al ser los árboles un tipo concreto de grafo, puede ser aplicado perfectamente a nuestro problema. Explicación: Se incializan dos listas de nodos (abiertos y cerrados), teniendo primero en la lista de abierta el nodo inicial del árbol. En el momento en que no queden nodos en abierta: no se alcanza la solución, si quedan nodos en abierta, se elige el que mejor heurística tenga y se mete en cerrados. Si este nodo es solución, el algoritmo concluye, independientemente de que queden más nodos en abierta, por lo que no será necesario investigar todo el árbol completo. Si no, se generan los sucesores de este nodo, se introducen en la lista abierta y se prosigue con el algoritmo. El algoritmo emplea una estimación de distancia a la solución de tipo f(n) = h(n) + g(n), siendo g(n) el coste del camino seguido para llegar al nodo en el que se encuentra y h(n) el valor estimado del nodo actual con respecto al objetivo, obtenido a partir de la heurística que hayamos definido. Si hacemos h(n) = 0 la búsqueda se haría como si se realizara a coste uniforme, sin tener información (Dijkstra) y si hiciéramos g(n) = 0 se trata de un algoritmo de búsqueda voraz, basado solo en el peso estimado del nodo actual, por lo que obtendríamos una técnica estricta de mejor nodo primero. Una vez que se establecen los hijos del nodo sobre el que estamos investigando, se buscan estos hijos en la lista abierta y en cerrada. Si se deben actualizar los nodos de abierta o cerrada porque hemos encontrado un mejor camino, se actualiza. Y si se actualiza un nodo de la lista cerrada, hay que decidir si se deben actualizar también los hijos. La complejidad del problema estará estrechamente ligada a la heurística elegida, pudiendo variar entre ser exponencial y ser lineal. La rapidez del algoritmo dependerá enormemente de la heurística elegida para estimar la distancia desde nuestro nodo hasta la solución final. (Heurística: (del griego “heurisko": “yo encuentro") es un conocimiento parcial sobre un problema/dominio que permite resolver problemas eficientemente en ese problema/dominio). Las funciones heurísticas se descubren resolviendo modelos simplificados del problema real. En este caso, se definen 2 heurísticas comunes: • Distancia de Manhattan. • Número de casillas mal colocadas. En la siguiente figura vemos el resultado obtenido por el algoritmo A* en el problema de encontrar el camino más corto entre dos números a través de una matriz, usando solamente números adyacentes: -Best Node First (BNF): Con este algoritmo lo único que queremos obtener es una solución, independientemente de que esta sea la mejor solución, o no. Este algoritmo, por supuesto, nos da una velocidad de resolución del problema muy alta. Dependiendo del problema que se deba solucionar, o de las restricciones y necesidades del problema, puede ser aceptable encontrar una solución cualquiera, independientemente de si es la correcta o no. Para nuestro caso, viendo los resultados que se obtienen con el algoritmo empleado A* para el caso 3x3, no es necesario reducir el tiempo, en contra de obtener la solución óptima. Sin embargo para dimensiones mayores del tablero, sería muy interesante considerar esta solución. -Algoritmo IDA: Es un tipo de algoritmo para búsqueda de caminos en grafos que intenta reducir la memoria total utilizada, para ello fija un límite de coste y recorre cada uno de los caminos hasta alcanzar este límite de coste. Si no encuentra solución, aumenta el límite de coste y repite el proceso hasta encontrar una solución o llegar al coste máximo posible. Es un algoritmo limitado en profundidad, aunque el control de la memoria no es del todo estricto. práctica. Por otro lado los tiempos de resolución eran demasiado elevados en cualquier caso. Keywords Algoritmo, Puzzle-8, Backtracking, A*, Java 1. INTRODUCTION El objetivo de nuestro trabajo es resolver el problema puzzlen, como generalización del problema puzzle-8; para ello decidimos usar y proponemos dos algoritmos distintos: Backtracking (búsqueda en profundidad) y A*(Búsqueda en anchura y profundidad combinadas). De esta manera podremos comprobar cual de ellos es mejor y realizar una comparación. Partimos de varias condiciones a cerca del puzzle-8: a) Al desarrollar los posibles movimientos de puzzle-8 nos encontramos con un árbol, en el que cada nodo tiene 4 o menos hijos. La profundidad de este árbol aumentará con la dimensión del tablero. b) No todos los estados son alcanzables desde un estado dado, aproximadamente pueden construirse la mitad de los tablero posibles partiendo de un tablero específico. Esto lo descubrimos al realizar las implementaciones y lo corroboramos con información externa. c) d) e) f) Existen un gran número de tableros que es posible formar. Este número es del orden de n! siendo n la dimensión del tablero, para un tablero de 3x3 (n=9) hay unos 300000 tableros, pero, para uno de 4x4 (n=16) hay aproximadamente 20900000000000 posibles tableros. Como puede apreciarse la complejidad computacional del problema crece de manera muy rápida con las dimensiones del tablero. Al explorar el árbol o grafo de movimientos nos encontraremos frecuentemente con tableros por los que ya hemos pasado, es conveniente llevar una cuenta de los tableros ya visitados para no añadir cálculos extras al problema. El problema tiene dos puntos de complejidad: El tiempo de cómputo y la memoria utilizada. Nos centraremos en el aspecto tiempo para considerar buena o mala una solución y para realizar la comparación entre ellas. Como detalles de implementación: usaremos vectores para las listas de nodos y las clases internas de Java para medir los tiempos de resolución de cada puzzle. Elegimos como alternativa el algoritmo A* porque es el mejor para resolver un problema de este tipo, que al final acaba siendo una búsqueda en un grafo. Podría haberse mejorado aún más, sobre todo en términos de memoria consumida, utilizando un algoritmo de tipo IDA, pero como nuestro parámetro de estudio fue el tiempo de resolución, decidimos dejarlo así. En cualquier caso, eligiendo un estimación f(n) = h(n), no se obtiene la mejor solución, pero el tiempo de cómputo se reduce notablemente. En nuestro caso queremos obtener la mejor solución, por lo que no estaremos en exceso atentos al tiempo de cómputo, y elegiremos A*. Por último cabe destacar que el algoritmo A* alcanza la solución óptima, es decir, para este problema conseguir alcanzar el tablero buscado en el número mínimo posible de movimientos. Trataremos en los siguientes puntos de explicar las implementaciones realizadas y la evolución seguida en las mismas. 2. IMPLEMENTACIÓN CON BACKTRACKING En este apartado explicaremos la implementación con backtracking. Transcribimos aquí el código del método de búsqueda, ya que consideramos que tiene interés para la explicación del mismo. El resto de código puede consultar o ejecutarse en los archivos adjuntos entregados con la memoria: public boolean solucionar(int numIteraciones, int [][] tablero){ int[][] tAuxiliar= copiar(tablero); boolean ret=false; Vector movPosibles = getPosibles(tAuxiliar); int i=0; int[] posOriginal= posicionCero; while(numIteraciones < i<movPosibles.size() && !resuelto){ limIteraciones numIteraciones++; int[] [])movPosibles.elementAt(i); movimiento=(int mover(movimiento,tAuxiliar); 1.1 Elección del algoritmo String aux= aString(tAuxiliar); if(visitados.contains(aux)){ El algoritmo de backtracking resulta más intuitivo a nuestro modo de ver, y debido a que podemos podar las ramas del árbol en las que aparezcan tableros por los que ya hemos pasado pensamos que este algoritmo utilizando dicha condición sería suficiente para resolver el problema con una eficiencia razonable. Como veremos posteriormente la solución por medio de este algoritmo no siempre convergía en un número de operaciones dado, y si no se limitaba el número de operaciones el tiempo de resolución se hacia demasiado elevado e inabordable en la && deshacerMovimiento(movimiento,tAuxiliar); }else{ visitados.add(aux); solucion.add(tAuxiliar); if(solucionado(tAuxiliar)){ System.out.println("solucionado!!!"); resuelto=true; return true; El pseudocódigo sería: }else{ ret= solucionar(numIteraciones,tAuxiliar); CONTADOR = 0 } PARA CADA CASILLA DE TABLERO_LINEAL if(!ret){ TABLERO_LINEAL:=TRANSFORMAR_TABLERO(TABLERO) deshacerMovimiento(movimiento,tAuxiliar); //visitados.remove(aux); solucion.remove(tAuxiliar); } } i++; Si (TABLERO_LINEAL(i)< TABLERO_LINEAL(i+i) AUMENTAR CONTADOR SI POSICIONCASILLAVACIA1 * POSICIONCASILLAVACIA2 ES DIVISIBLE ENTRE DOS AUMENTAR CONTADOR SI CONTADOR ES DIVISIBLE ENTRE DOS EL TABLERO TIENE SOLUCIÓN } SINO return ret;} EL TABLERO NO TIENE SOLUCIÓN Y el método transformar tablero sería: En esencia, el método se ejecuta para cada posible movimiento, comprueba si el tablero resultante ya ha sido recorrido y si no es así continua con el árbol que se crea a partir del nuevo nodo. Puede apreciarse como el método se llama recursivamente una vez por cada movimiento posible que hay en cada paso, así que en el peor de los casos, en cada movimiento o nivel del árbol, el número de nodos del árbol crece en 4*n, siendo n el número de nodos que había anteriormente, si resolviéramos la sucesión nos encontraríamos con que el crecimiento es exponencial. El algoritmo sólo se detiene cuando alcanza una solución, esto también es un error del mismo. 2.1 Resultados obtenidos PARA CADA COLUMNA DE TABLERO PARA CADA FILA DE TABLERO TABLERO_LINEAL:=TABLERO(FILA)(COLUMNA) El tiempo medio para obtener la solución sigue siendo elevado, por encima de 15 segundos para tableros de 3x3 y prácticamente inabordable para tableros de dimensión superior. En la siguiente tabla mostramos el tiempo tardado en función del número de iteración, para un tablero de 3x3: Fijamos como límite de tiempo razonable 15 segundos. Esta es la primera aproximación al problema, poco después de conseguir ejecutarla comprobamos que en un número elevado de ocasiones el problema no convergía a una solución en un tiempo razonable, mientras que en otros casos el algoritmo encontraba la solución más o menos rápidamente. Número de prueba Investigando llegamos a la conclusión que debía de haber tableros no alcanzables desde los tableros iniciales que generábamos aleatoriamente. Elaboramos un método para comprobar, antes de comenzar el algoritmo de búsqueda, si el tablero tiene solución. El método tiene la siguiente idea, si el tablero es solucionable, al sumar el número de casillas mayores que su casilla inmediatamente anterior y añadirle uno si el producto de las posiciones x,y de la casilla vacía es divisible entre dos obtendremos un número divisible entre dos, y no divisible entre dos en caso contrario. La casilla inmediatamente inferior se obtiene asumiendo un orden de lectura del tablero de izquierda a derecha y de arriba abajo. (Puede encontrarse dicho método en los ficheros de código entregados). Tiempo consumido 1 6,110 segundos 2 >15 segundos (92) 3 >15 segundos (25) 4 >15 segundos 5 >15 segundos 6 9,879 segundos 7 >15 segundos 8 7,812 segundos 9 >15 segundos 10 >15 segundos 11 >15 segundos (92) 12 >15 segundos (16) 13 7,639 segundos 14 >15 segundos 15 >15 segundos Número de prueba Tiempo consumido Número de Prueba Tiempo consumido 16 8,567 segundos 7 >15 segundos 17 >15 segundos 8 7,594 segundos 18 >15 segundos 9 >15 segundos 19 > 15 segundos 10 >15 segundos 20 >15 segundos 11 5,147 segundos media - 12 4,667 segundos 13 >15 segundos 14 3,089 segundos 15 8,667 segundos 16 >15 segundos 17 2,596 segundos 18 5,991 segundos 19 >15 segundos 20 >15 seungos media - Puede apreciarse como los tiempos son muy dispersos y además, sólo 5 de las 20 iteraciones cumplen el límite de tiempo propuesto. Consideramos que esta solución es mala. Resultados aceptables: 25 %. Inaceptable. Por otro lado, para tableros de 4x4 nunca conseguimos ver una solución, incluso dejando el código ejecutándose durante tiempos de más de media hora. 2.2 Modificación del método de Backtracking Esto nos llevó a intentar realizar una modificación del método de backtracking propuesto. En esencia, la modificación propuesta consistía en: En lugar de desarrollar todos los caminos posibles en cada nodo, proporcionados por cada movimiento, desarrollar sólo el camino no explorado que nos acercara más a la solución deseada. Para ello creamos una métrica de cercanía a la solución sumando el valor absoluto de la diferencia de posiciones de cada casilla en su posición actual, con esa misma casilla en la posición en la que debería estar (distancia de Manhattan). Es decir, por ejemplo, el “1” colocado en (3,3), sabiendo que debe estar en (1,2) proporciona una métrica de |3-1| + |3-2| = 3. Con esta nueva aproximación en teoría, al reducir los nodos del árbol de búsqueda a uno sólo en lugar de 4 en cada paso, deberíamos tardar menos en converger a la solución. Sin embargo, esto no resultó ser así del todo, debido a que en muchas ocasiones, para llegar a la solución debemos tomar caminos que nos alejen de la misma, y que con este nuevo algoritmo eran explorados más tarde que otros caminos. Se trata de algo muy similar a si hicieramos g(n) = 0 en un algoritmo de tipo A* Los resultados obtenidos, para un tablero de 3x3, fueron: Número de Prueba Tiempo consumido 1 >15 segundos 2 >15 segundos 3 2,297 segundos 4 >15 segundos 5 0,016 segundos 6 >15 segundos El resultado obtenido es que, si bien ahora hay 9 de las 20 iteraciones que cumplen los objetivos propuestos, los tiempos siguen siendo demasiado dispersos, y el resultado sigue siendo insuficiente. Se ha mejorado, pero la solución aún no es buena; y menos aún para tableros de dimensión superior. Cabe destacar que en una iteración se obtiene un tiempo de 0,016 segundos, suponemos que esta iteración el camino proporcionado por nuestra métrica coincidió con el camino real que buscábamos. En general, como puede comprobarse en la tabla, esto no sucede. A la vista de los resultados decidimos cambiar de algoritmo e intentar otro de tipo A* que es más óptimo para este tipo de problemas. Resultados aceptables: 45%. Inaceptable. De la misma manera que con el método de backtracking puro, nunca conseguimos ver solucionado un tablero de 4x4. 3. IMPLEMENTACIÓN CON ALGORITMO A* El algoritmo A* utiliza una estimación basada tanto en la heurística del nodo actual como del camino recorrido en llegar hasta él, de esta manera tiene en cuenta el camino que se ha recorrido y cambiará de ruta cada vez que encuentre un nodo más prometedor. Su principal desventaja es su tamaño en memoria, pero para un problema de escasa complejidad como este, la memoria requerida no se dispara. Si tuviéramos que realizar este mismo algoritmo para un tablero de 4x4 ó 5x5, ya comienzan a surgir los problemas. cerrada.add(tabl); Nuestra implementación se basa en utilizar dos vectores para ir almacenando los tableros ya recorridos, según se vio en pseudocódigo anteriormente, lista abierta y cerrada. obtenerNodos(tabl); tabl = getMenor(); La heurísticas utilizadas son 2: if(comparaTableros(tabl.getTablero(),TableroFinal)){ Una es la misma que para el último algoritmo de backtracking, pero añadiéndole el coste de llegar hasta el tablero que actualmente se está considerando. Es decir, y para que quede más claro: creamos una métrica de cercanía a la solución sumando el valor absoluto de la diferencia de posiciones de cada casilla en su posición actual, con esa misma casilla en la posición en la que debería estar. Es decir, por ejemplo, el “1” colocado en (3,3), sabiendo que debe estar en (1,2) proporciona una métrica de |3-1| + |3-2| = 3. t2=System.currentTimeMillis(); encontrado=true; } } if(encontrado){ System.out.println("Encontrado!!!!!"); La otra es la de obtener el número de casillas mal colocadas en el tablero. En el ejemplo del tablero siguiente la heurística nos daría el valor: 8. Puesto que no hay ningún valor que esté correctamente colocado en su casilla correspondiente. Vector listaMovimientos = new Vector(); Tablero anterior = tabl; while (anterior != null) { System.out.println("\nPaso siguiente"); mostrarTablero(anterior.getTablero()); listaMovimientos.insertElementAt(anterior, 0); anterior = anterior.getAnterior(); } for (int i = 0; i < listaMovimientos.size(); i++) { Tablero taux = (Tablero) listaMovimientos.get(i); ventanaprincipal.actualizaTablero(taux.getTablero(), listaMovimientos.size() - i - Figura 7 Tablero 8Puzzle 1); La implimentación usa 3 clases: la primera, la clase tablero, maneja los tableros en sí mismos, cacula las heurísticas,etc. La segunda, la clase puzzle es la que gestiona los algoritmos de búsqueda. Y por último la clase VentanaPrincipal proporciona una sencilla interfaz gráfica. Los códigos de dichas clases pueden consultarse en los ficheros adjuntos, y no consideramos necesario incluirlos aquí, ya que el algoritmo de búsqueda es muy similar al pseudocódigo presentado al comienzo de la memoria. Solamente incluiremos el código del algoritmo de búsqueda, para que puede apreciarse la similitud con el pseudocódigo del algoritmo, y cómo el problema del puzzle8 es un problema bastante idóneo para resolver con este algoritmo sin ninguna modificación adicional: public long aestrella(){ try { Thread.sleep(300); } catch (Exception e) {} } }else{ System.out.println("No Encontrado!!!!!"); } return (t2-t1); } //Fin Algoritmo boolean encontrado = false; Tablero tabl = getMenor(); 3.1 Resultado obtenidos mostrarTablero(tabl.getTablero()); long t1=System.currentTimeMillis(); long t2=0; if(comparaTableros(tabl.getTablero(),TableroFinal)){ encontrado=true; } while(!encontrado && tabl!=null){ Con pocas ejecuciones del algoritmo encontramos que la velocidad de convergencia es mayor que antes (cuando usábamos backtracking). Tras varias ejecuciones comprobamos que el algoritmo ya no se “atranca” (tardando tiempos muy altos) si el tablero es solucionable, hallando la solución en tiempos razonables en todos los casos. En la siguiente tabla resumen observamos los resultados: Número de iteración Tiempo requerido 1 2,406 segundos 2 14,092 segundos 3 8,407 segundos 4 13,172 segundos 5 >15 segundos (22) 6 13,656 segundos 7 0,859 segundos 8 0,641 segundos 9 2,078 segundos 10 0,297 segundos 11 0,734 segundos 12 0,973 segundos 13 0,047 segundos 14 0,093 segundos 15 10,057 segundos 16 3,958 segundos 17 2,375 segundos 18 > 15 segundos (20) 19 >15 segundos (17) 20 0,766 segundos 21 2,635 segundos 22 4,875 segundos 23 5,048 segundos 24 0,643 segundos 25 0,063 segundos 26 12,471 segundos 27 3,423 segundos 28 11,262 segundos media 5,02 segundos Como vemos, los resultados son mucho mejores, tenemos un tiempo medio de unos 5 segundos (sin considerar los tiempos que superan los 15 segundos) y sólo 3 valores que superan los 15 segundos (además, de estos 3 valores solamente uno supera también los 20 segundos), en cambio hay 10 valores por debajo de un segundo. Resultados aceptables: 89,3 %. Aceptable. Para puzzles de dimensión superior sólo conseguimos la solución una vez y con un tiempo del orden de 17 minutos. Esto creemos que se debe a que el árbol a explorar aumenta de dimensión muy rápidamente, ya que, en la mayor parte de los movimientos habrá 4 posibilidades (en el de 3x3 sólo la casilla central tenía 4 movimientos posibles) y además el número de movimientos necesarios aumenta drásticamente, aumentando con ello la complejidad de manera exponencial. 4. CONCLUSIONES La realización de esta práctica pone de manifiesto la enorme carga computacional que supone la resolución de juegos mediante algoritmos de inteligencia artificial, y como la elección de los heurísticos es fundamental. En nuestro caso, usando backtracking con la modificación de desarrollar el camino cuyo siguiente tablero era más cercano a la solución, el problema convergía mucho peor que el usando A*, que considera también el camino acumulado. En definitiva, el problema del 8puzzle no se resuelve de manera eficiente usando backtracking, debido a sus características, la elección de una algoritmo en función de las características del problema es también fundamental a la hora de obtener resultados aceptables. Para resolver tableros de dimensión mayor: 4x4, 5x5 o incluso superior, debería buscarse otro algoritmo, porque ninguno de estos proporciona resultados, o bien intentar optimizar la heurística del A* aún más, ya que teóricamente puede conseguirse que el crecimiento sea lineal con la complejidad del problema. 5. REFERENCIAS [1]http://es.wikipedia.org/wiki/Algoritmo_de_b%C3%BAsqueda_ A* [2] Apuntes de la asignatura IRC [3]http://es.wikipedia.org/wiki/Complejidad_computacional [4]http://www.dc.fi.udc.es/.aios/~barreiro/iadocen/puzzle898/introalgoritmos.html