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