Práctica 3

Anuncio
Práctica 3
Búsqueda
Vamos a continuar viendo algoritmos de búsqueda en grafos. En esta práctica nos centraremos
en algoritmos de búsqueda informada.
1 Heurístico
Ya hemos visto en teoría que este tipo de algoritmos necesitan lo que se conoce como función
heurística. Esta función debe ser una estimación del coste que supone ir de un nodo cualquiera
al nodo objetivo.
Pensando en el problema de las ciudades, una buena función heurística sería la distancia que hay
en línea recta desde cada ciudad a la ciudad objetivo (ese ha sido precisamente el heurístico
utilizado).
Trabajo: En la práctica anterior adaptaste el fichero CiudadesCompleto.java para resolver el
problema de las ciudades en Rumanía. En aquel momento pusiste como heurístico 0 para todas
las ciudades. Modifica, ahora, el valor del heurístico para adaptarlo a lo comentado en el párrafo
anterior.
2 A* y primero el mejor
Podemos ahora llamar a los algoritmos de búsqueda informada implementados en la librería
busqueda.jar (actualmente, esta librería tiene implementados los algoritmos A* y primero el
mejor). Para ello debemos añadir el siguiente código:
System.out.println("--------------------------------------------");
inicio = System.currentTimeMillis();
sol = PrimeroElMejor.Resuelve(a,"ciudades_primero_el_mejor.xml");
fin = System.currentTimeMillis();
System.out.println("Una solución por Primero el Mejor: "+sol);
System.out.println("Y ha tardado: "+ ((fin-inicio)/1000.0) +" segundos");
System.out.println("--------------------------------------------");
inicio = System.currentTimeMillis();
sol = AEstrella.Resuelve(a,"ciudades_aestrella.xml");
fin = System.currentTimeMillis();
System.out.println("Una solución por A*: "+sol);
System.out.println("Y ha tardado: "+ ((fin-inicio)/1000.0) +" segundos");
System.out.println("Numero de pasos para la solución: "+sol.size());
Este código ejecuta ambos algoritmos ofreciendo, al finalizar, el camino propuesto por cada uno
y el tiempo que han empleado para su cálculo. Ahora ya podemos compilar y ejecutar:
javac –classpath busqueda.jar CiudadesCompleto.java
java –classpath busqueda.jar;. CiudadesCompleto
Trabajo: Utiliza estos dos algoritmos para resolver los dos problemas de las ciudades
planteados ¿Obtienen mejores caminos? ¿Son más rápidos? Visualiza las trazas. Puedes Hacer
lo mismo con el fichero CiudadesCompleto.java original (ya tenía un heurístico para el
problema original)
3 NPuzzle
Vamos a trabajar ahora con el famoso 8-puzzle. Ya sabéis que el 8-puzzle es un juego en el que
hay 9 casillas y 8 fichas. El objetivo del juego es pasar de un estado inicial de las fichas a uno
final mediante sucesivos movimientos de las diferentes fichas hacia la casilla vacía.
Imaginemos los siguientes estados inicial y final. ¿Qué movimientos son necesarios para llegar
alcanzar el objetivo?
Estado Inicial
1
3
6
5
2
8
4
7
Estado Final
1
2
3
4
5
6
7
8
Para solucionar este problema podemos utilizar los algoritmos de búsqueda incluidos en la
librería busqueda.jar. Para ello necesitamos definir la clase NPuzzle. Esta clase podemos
descargarla del siguiente enlace:
http://www.aic.uniovi.es/ssii/P3/NPuzzle.java
3.1 Código
Para codificar este problema necesitamos cuatro campos:
private int[][] puzzle;
static private int[][]obje;
private int fi;
private int co;
puzzle indica la situación del puzzle en el estado actual, obje la situación de las piezas en el
objetivo y los campos fi y co se utilizan para almacenar la posición en la que no hay pieza.
Se necesitan dos constructores: uno para instanciar el problema y otro para ir generando los
nodos sucesores:
public NPuzzle(int[][] matriz,
int[][] obj){
puzzle = matriz;
obje
= obj;
nombre = new String("N-Puzzle");
prof=0;
for (int i=0; i < matriz.length; ++i){
for (int j=0; j < matriz[i].length; ++j){
if (matriz[i][j]==0){
fi = i;
co = j;
}
}
}
private NPuzzle
(int[][] matriz,
int p, int f, int c){
puzzle = matriz;
prof
= p;
fi
= f;
co
= c;
}
}
Para saber si un nodo es objetivo, lo único que hay que hacer es ver si puzzle y obje tienen
las piezas dispuestas de la misma manera:
public boolean EsObjetivo(){
if (obje.length!=puzzle.length) return false;
for (int i=0; i < obje.length; ++i)
for (int j=0; j < obje[i].length; ++j)
if (obje[i][j] != puzzle[i][j]) return false;
return true;
}
Para generar nodos sucesores, debemos crear una LinkedList de InfNodos. Un InfNodo
contiene el nodo sucesor, el coste para ir del nodo actual al sucesor y un texto descriptivo de la
acción. En el NPuzzle debemos controlar la posición sin ficha para saber qué sucesores tiene el
nodo actual:
public LinkedList<InfNodo> Sucesores(){
LinkedList<InfNodo> succ = new LinkedList<InfNodo>();
if((fi+1)<puzzle.length){
int[][] copia = new int[puzzle.length][puzzle[0].length];
for (int i=0; i < puzzle.length; ++i)
for (int j=0; j < puzzle[i].length; ++j)
copia[i][j]=puzzle[i][j];
copia[fi][co] = copia[fi+1][co];
copia[fi+1][co] = 0;
NPuzzle t = new NPuzzle(copia,prof+1,fi+1,co);
succ.add(new InfNodo(t,1,new String("Mover "
+ copia[fi][co] +" hacia arriba")));
}
if((co+1)<puzzle[0].length){
int[][] copia = new int[puzzle.length][puzzle[0].length];
for (int i=0; i < puzzle.length; ++i)
for (int j=0; j < puzzle[i].length; ++j)
copia[i][j]=puzzle[i][j];
copia[fi][co] = copia[fi][co+1];
copia[fi][co+1] = 0;
NPuzzle t = new NPuzzle(copia,prof+1,fi,co+1);
succ.add(new InfNodo(t,1,new String("Mover "
+ copia[fi][co] + " hacia izquierda")));
}
if((fi-1)>=0){
int[][] copia = new int[puzzle.length][puzzle[0].length];
for (int i=0; i < puzzle.length; ++i)
for (int j=0; j < puzzle[i].length; ++j)
copia[i][j]=puzzle[i][j];
copia[fi][co] = copia[fi-1][co];
copia[fi-1][co] = 0;
NPuzzle t = new NPuzzle(copia,prof+1,fi-1,co);
succ.add(new InfNodo(t,1,new String("Mover "
+ copia[fi][co] + " hacia abajo")));
}
if((co-1)>=0){
int[][] copia = new int[puzzle.length][puzzle[0].length];
for (int i=0; i < puzzle.length; ++i)
for (int j=0; j < puzzle[i].length; ++j)
copia[i][j]=puzzle[i][j];
copia[fi][co] = copia[fi][co-1];
copia[fi][co-1] = 0;
NPuzzle t = new NPuzzle(copia,prof+1,fi,co-1);
succ.add(new InfNodo(t,1,new String("Mover "
+ copia[fi][co] + " hacia derecha")));
}
return succ;
}
El heurístico utilizado para este problema es la suma de los movimientos de todas las piezas
para alcanzar sus posiciones objetivo, conocida como la distancia Manhattan:
public double Heuristico(){
// Algoritmo que calcula la suma de las distancias manhattan
// hasta la solucion
double suma=0;
for (int i=0; i < puzzle.length; ++i){
for (int j=0; j < puzzle[i].length; ++j){
if(obje[i][j]!=puzzle[i][j]){
//Buscamos la posición correcta para la ficha en puzzle[i][j]
int i2=0, j2=0;
while(i2<obje.length && (obje[i2][j2]!=puzzle[i][j])){
j2++;
if (j2 >= obje[i2].length) {i2++; j2=0;}
}
//En [i2][j2] está la posición correcta
suma+=Math.abs(i2-i)+Math.abs(j2-j);
}
}
}
return suma;
}
Cuando se crea una nueva clase en java que herede de java.lang.Object es necesario redefinir
la función equals y la función hashCode. Esta última es necesaria para optimizar las
ordenaciones de objetos. Debemos implementarla de manera que retorne un mismo entero para
todos los nodos que sean iguales. En este caso usamos la siguiente codificación:
Nodo
1
3
6
136 * 1
5
2
8
+ 528 * 2
4
7
470 * 3
2462
public int hashCode(){
int suma = 0, num;
for (int i=0; i < puzzle.length; ++i){
num = 0;
for (int j=0; j < puzzle[i].length; ++j)
num += puzzle[i][j] * Math.pow(10,puzzle[i].length-(j+1));
suma+=num*(i+1);
}
return suma;
}
Redefinimos también la función equals:
public boolean equals( Object c ){
if ( c instanceof NPuzzle ){
NPuzzle n = (NPuzzle)c;
if (n.puzzle.length!=puzzle.length) return false;
for (int i=0; i < puzzle.length; ++i)
for (int j=0; j < puzzle[i].length; ++j)
if (n.puzzle[i][j]!=puzzle[i][j]) return false;
return true;
}
return false;
}
Implementamos, también, la función Información. En este caso, la información que mostramos
de cada nodo es la situación de las piezas en el nodo actual y la situación de las piezas que se
quiere alcanzar, es decir, el nodo objetivo.
public String Informacion(){
String retorno = new String();
for (int i=0; i < puzzle.length; ++i){
for (int j=0; j < puzzle[i].length; ++j)
retorno= retorno + new String(puzzle[i][j]+" ");
retorno = retorno + new String(" ");
for (int j=0; j < obje[i].length; ++j)
retorno= retorno + new String(obje[i][j]+" ");
if(i!=(puzzle.length-1))
retorno= retorno + System.getProperty("line.separator");
}
return retorno;
}
Y con esto ya tenemos definido todo lo necesario para solucionar el problema del 8-puzzle.
Ahora ya podemos compilar y ejecutar:
javac –classpath busqueda.jar NPuzzle.java
java –classpath busqueda.jar;. NPuzzle
Trabajo: Observa el tiempo que tarda en finalizar cada algoritmo ¿Tardan menos los de
búsqueda informada? ¿Obtienen todos los algoritmos la misma solución? Visualiza las
diferentes trazas para comprender el funcionamiento de los algoritmos.
Cambia los estados inicial y final.
3.2 Cambiando la función heurística y la dimensión
Hay, al menos, dos heurísticos tradicionales para el problema del NPuzzle. Uno es la suma de
los movimientos de todas las piezas para alcanzar sus posiciones objetivo (distancia Manhattan)
y el otro cuenta el número de piezas descolocadas respecto a su posición en el objetivo.
Trabajo: Implementa el segundo heurístico y compara los tiempos de los algoritmos
informados con ambos heurísticos. ¿Qué heurístico ofrece mejores soluciones?
Hasta ahora hemos trabajado en el problema del NPuzzle tratando de solucionar únicamente el
problema del 8-puzzle. Sin embargo, si nos fijamos en la implementación, parece que no hay
limitación en cuanto a la dimensión del puzzle. ¿Te sientes capaz de adaptarlo para que resuelva
el problema del 15-puzzle?
Estado Inicial
1
3
4
2
6
8
9
10 7
11
13 14 15 12
5
1
5
9
13
Estado Final
2
3
4
6
7
8
10 11 12
14 15
Trabajo: ¡Inténtalo! Los cambios que hay que hacer son mínimos. Únicamente tienes que
modificar el programa principal
4 Otros Problemas
Este problema consiste en colocar n reinas en un tablero de dimensión n por n sin que se
ataquen entre ellas.
¿Cómo plantearías la solución del problema de las NReinas? Piensa en ello.
Descargar