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.