Está en la página 1de 32

Búsqueda en profundidad

general

La gira del caballo es un caso especial de una búsqueda en profundidad donde el objetivo
es crear el árbol de búsqueda en profundidad más profundo, sin ramas. La búsqueda en
profundidad más general es realmente más fácil. Su objetivo es buscar lo más
profundamente posible, conectando tantos nodos en el grafo como sea posible y
ramificando donde sea necesario.
Incluso es posible que una búsqueda en profundidad cree más de un árbol. Cuando el
algoritmo de búsqueda en profundidad crea un grupo de árboles llamamos a esto
un bosque de profundidad. Al igual que con la búsqueda en anchura, nuestra búsqueda
en profundidad hace uso de los enlaces a los predecesores para construir el árbol. Además,
la búsqueda en profundidad hará uso de dos variables de instancia adicionales en la
clase Vertice. Las nuevas variables de instancia son los tiempos de descubrimiento y de
finalización. El tiempo de descubrimiento rastrea el número de pasos en el algoritmo antes
de que un vértice sea encontrado por primera vez. El tiempo de finalización es el número
de pasos en el algoritmo antes de que un vértice se pinte de negro. Como veremos después
de examinar el algoritmo, los tiempos de descubrimiento y de finalización de los nodos
proporcionan algunas propiedades interesantes que podemos usar en algoritmos
posteriores.
El código para nuestra búsqueda en profundidad se muestra en el Programa 5. Puesto que
las dos funciones bep y su auxiliar visitabep usan una variable para realizar un
seguimiento del tiempo entre llamadas a visitabep, hemos elegido implementar el código
como métodos de una clase que hereda de la clase Grafo. Esta implementación extiende
la clase Grafo agregando una variable de instancia tiempoy los dos
métodos bep y visitabep. Mirando la línea 11, usted notará que el método bep itera sobre
todos los vértices del grafo llamando a visitabep sobre los nodos que sean blancos. La
razón por la que iteramos sobre todos los nodos, en lugar de simplemente buscar desde
un nodo de partida elegido, es asegurarnos de que se consideren todos los nodos en el
grafo y que no haya vértices que se queden fuera del bosque de profundidad. Puede
parecer inusual ver la instrucción for unVertice in self, pero recuerde que en este
caso self es una instancia de la clase grafoBEP, y que iterar sobre todos los vértices en una
instancia de un grafo es algo natural que hacer.
Programa 5
1
from pythonds.graphs import Grafo
2
class grafoBEP(Grafo):
3
4 def __init__(self):

5 super().__init__()

6 self.tiempo = 0

8 def bep(self):

9 for unVertice in self:

10 unVertice.asignarColor('blanco')

11 unVertice.asignarPredecesor(-1)

12 for unVertice in self:

13 if unVertice.obtenerColor() == 'blanco':

14 self.visitabep(unVertice)

15

16 def visitabep(self,verticeInicio):

17 verticeInicio.asignarColor('gris')

18 self.tiempo += 1

19 verticeInicio.asignarDescubrimiento(self.tiempo)

20 for siguienteVertice in verticeInicio.obtenerConexiones():

21 if siguienteVertice.obtenerColor() == 'blanco':

22 siguienteVertice.asignarPredecesor(verticeInicio)

23 self.visitabep(siguienteVertice)

24 verticeInicio.asignarColor('negro')

25 self.tiempo += 1

verticeInicio.asignarFinalizacion(self.tiempo)

Aunque nuestra implementación de bea sólo estaba interesada en considerar nodos para
los que había una ruta que llevaba de regreso al inicio, es posible crear un bosque de
anchura que represente la ruta más corta entre todas las parejas de nodos en el grafo.
Dejamos esto como ejercicio. En nuestros próximos dos algoritmos vamos a ver por qué
es importante el seguimiento del árbol de profundidad.
El método visitabep comienza con un solo vértice llamado verticeInicio y explora todos los
vértices blancos vecinos lo más profundamente posible. Si usted examina atentamente el
código de visitabep y lo compara con la búsqueda en anchura, lo que debería notar es que
el algoritmo visitabep es casi idéntico a bea, excepto que en la última línea del
ciclo for interno, visitabep se llama a sí misma recursivamente para continuar la búsqueda
a un nivel más profundo, mientras que bea añade el nodo a una cola para su exploración
posterior. Es interesante observar que donde bea usa una cola, visitabepusa una pila. Usted
no verá una pila en el código, pero está implícita en la llamada recursiva a visitabep.
La siguiente secuencia de figuras ilustra en acción el algoritmo de búsqueda en
profundidad para un grafo pequeño. En estas figuras, las líneas punteadas indican aristas
que están comprobadas, aunque el nodo en el otro extremo de la arista ya se ha añadido
al árbol de profundidad. En el código esta prueba se realiza comprobando que el color del
otro nodo no sea blanco.
La búsqueda comienza en el vértice A del grafo (Figura 14). Puesto que todos los vértices
son blancos al comienzo de la búsqueda, el algoritmo visita el vértice A. El primer paso
al visitar un vértice es pintarlo de gris, lo que indica que se está explorando el vértice y al
tiempo de descubrimiento se le asigna 1. Dado que el vértice A tiene dos vértices
adyacentes (B, D), cada uno de ellos requiere ser visitado también. Tomaremos la
decisión arbitraria de que visitaremos los vértices adyacentes en orden alfabético.
El vértice B se visita a continuación (Figura 15), por lo que se pinta de gris y se asigna 2
a su tiempo de descubrimiento. El vértice B también es adyacente a otros dos nodos (C,
D), así que seguiremos en orden alfabético y visitaremos a continuación el nodo C.
Visitar el vértice C (Figura 16) nos lleva al final de una rama del árbol. Después de pintar
el nodo de gris y asignarle 3 a su tiempo de descubrimiento, el algoritmo también
determina que no hay vértices adyacentes a C. Esto significa que hemos terminado de
explorar el nodo C y por lo tanto podemos pintar el vértice de negro y asignarle 4 al
tiempo final. Usted puede ver el estado de nuestra búsqueda en este punto en la Figura
17.
Dado que el vértice C era el final de una rama, ahora regresamos al vértice B y seguimos
explorando los nodos adyacentes a B. El único vértice adicional que se debe explorar
desde B es D, por lo que ahora podemos visitar D (Figura 18) y continuar nuestra
búsqueda desde el vértice D. El vértice D nos conduce rápidamente al vértice E (Figura
19). El vértice E tiene dos vértices adyacentes, B y F. Normalmente exploraríamos estos
vértices adyacentes en orden alfabético, pero como B ya está pintado de gris, el algoritmo
reconoce que no debería visitar B, ya que hacerlo pondría al algoritmo en un ciclo. Así,
la exploración continúa con el siguiente vértice de la lista, a saber F (Figura 20).
El vértice F tiene sólo un vértice adyacente, C, pero como C está pintado de negro, no
hay nada más que explorar, y el algoritmo ha llegado al final de otra rama. De aquí en
adelante, verá usted desde la Figura 21hasta la Figura 25 que el algoritmo regresa al
primer nodo, asignando los tiempos de finalización y pintando los vértices de color negro.
Figura 14: Construcción del árbol de búsqueda en profundidad-10

Figura 15: Construcción del árbol de búsqueda en profundidad-11

Figura 16: Construcción del árbol de búsqueda en profundidad-12

Figura 17: Construcción del árbol de búsqueda en profundidad-13

Figura 18: Construcción del árbol de búsqueda en profundidad-14


Figura 19: Construcción del árbol de búsqueda en profundidad-15

Figura 20: Construcción del árbol de búsqueda en profundidad-16

Figura 21: Construcción del árbol de búsqueda en profundidad-17

Figura 22: Construcción del árbol de búsqueda en profundidad-18

Figura 23: Construcción del árbol de búsqueda en profundidad-19


Figura 24: Construcción del árbol de búsqueda en profundidad-20

Figura 25: Construcción del árbol de búsqueda en profundidad-21


Los tiempos de inicio y finalización de cada nodo muestran una propiedad
denominada propiedad de paréntesis. Esta propiedad significa que todos los hijos de un
nodo en particular en el árbol de profundidad tienen un tiempo de descubrimiento
posterior y un tiempo de finalización anterior que aquellos de su padre. La Figura
26 muestra el árbol construido por el algoritmo de búsqueda en profundidad.

Figura 26: TEl árbol resultante de la búsqueda en profundidad


Problema del 8 puzzle y Búsqueda No Informada
[código]

El tradicional juego del 8-puzzle consiste, en dado un tablero con 9 casillas, las cuales
van enumeradas del 1 al 8 más una casilla vacía. Dicha casilla vacia, es la que, con
movimientos horizontales, verticales, hacia la izquierda o derecha, debe ser desplazada e
intercambiada con alguno de sus vecinos, de manera que, dada una configuración inicial
se llegue a una configuración final (meta). Este problema, al tratar de ser resuelto
computacionalmente representa un problema al que debemos de tratar con sumo cuidado.

Aunque las reglas del juego sean sencillas de realizar (y evidentemente de programar)
conlleva una complejidad mayor al momento de obtener la solución, es por esta razón que
resulta un ejemplo clásico y muy didáctico para poner en práctica algoritmos de búsqueda
que encuentren la solución eficiente a una configuración de 8-puzzle.

Búsqueda No Informada
Para la solución del problema del 8-puzzle presentamos 3 soluciones haciendo uso de
algoritmos de busqueda no informada:
1. Búsqueda primero en amplitud: expande el nodo más interno sin visitar,
colocando sus nodos borde en una estructura FIFO.
2. Búsqueda primero en profundidad limitada: expande el nodo más
profundo que aún no había sido expandido, colocando sus nodos borde en una
estructura LIFO, expandiendose como máximo hasta una profundidad límite.
3. Búsqueda en profundidad iterativa: combina la búqueda primero en
profundidad con la búsqueda primero en amplitud.
Desde aquí pueden descargar un documento de referencia. Es importante aclarar que
este tipo de búsqueda no aseguran que siempre, dada una configuración inicial se
encontrará la solución, esto debido a lo que se conoce como error de paridad.

Para entender que es error de paridad, primero debemos saber que es una inversión.
Una inversión se presenta cuando un número mayor está "delante" de un número menor
al colocar los números en una sola fila.

Es decir, si tuviesemos un estado: "123485760", donde 0 representa a la casilla vacía,


tendríamos que 8 se encuentra delante de 5, 7 y 6, lo que da un numero de 3
inversiones; y también el 7 esta delante de 6, lo que suma un total de 4 inversiones. El
número total de inversiones para este estado fue 4, es decir un número par, lo que
significa que el juego tiene solución. Si el número total de inversiones hubiese resultado
impar, el juego con dicho estado inicial, no tendría solución.

Implementación
Se ha implementado los 3 algoritmos de búsqueda no informada. La implementación de
los algoritmos de búsqueda primero en amplitud, primero en profundidad limitada y
en profundidad iterativa se ha realizado usando C++ sobre el compilador MingW 5.1.4.

Para la implementación de estos algoritmos se hace uso de Tablas Hash. El código


fuente en C++ de las tablas Hash usadas se encuentran en los post tablas hash
[código] y tablas hash 2 [código].

El codigo fuente y ejecutable para la solución del problema del 8 puzzle se puede
descargar desde aqui.

Experimentos
Se realizaron experimentos acerca de la complejidad de los algoritmos implementados,
las características del computador sobre el que se realizaron los experimentos son:
procesador Pentium IV con memoria RAM 512 MB sobre el sistema operativo
Windows XP usando el lenguaje de programación C++ con el compilador MinGW
5.1.4.

Los criterios tomados en cuenta para los experimentos fueron:

Generación automática de estados


El estado inicial para un determinado juego de puzzle es generado de manera aleatoria.

Tamaño de la Tabla Hash


El tamaño de la tabla hash quedo determinada en base a la propiedad del juego. El juego
tiene 9 casillas, por lo tanto se podran realizar 9! = 362 880 permutaciones diferentes.
De la misma manera se medira la efectividad de la funcion hash la cual se determina
mediante la fórmula

efectividad de función hash = # de accesos directos/ # de accesos totales * 100

Número de Nodos generados


Es la cantidad total de nodos que se han ido generando durante el proceso de busqueda.
El número de nodos generados es un indicador que nos permite medir la complejidad de
tiempo para un determinado tipo de búsqueda.

Número de Nodos en memoria


Es la cantidad total de nodos que despúes de ser generados han sido guardados en algun
TAD (Tipo Abstracto de Datos), la cual, según sea el caso, puede tratarse de una pila o
cola. El número de nodos en memoria es un indicador que nos permite medir la
complejidad de espacio para un determinado tipo de búsqueda.

Profundidad del árbol de búsqueda


Existen dos medidas de la profundidad: la profundidad máxima del espacio de estados,
la cual nos indica el nivel máximo hasta el cual se expande el árbol; y la profundidad de
solución de menor costo, la cual nos indica la profundidad en la cual se encontró la
solución.
ALGORITMO DE BÚSQUEDA A*
(PATHFINDING A*) – XNA

Teoría
El algoritmo A* es usado para encontrar la ruta más cercana para ir de un lugar
a otro (llamados nodos), es el más usado debido a que es sencillo y rápido.

La representación es la siguiente:
f(n) = g(n) + h(n)
g(n) es la distancia total que se toma de ir de la posición inicial a la posición
actual
h(n) es la distancia estimada desde la posición inicial a la posición de destino
de final, en este caso se usa una función heurística para calcular el valor
estimado
f(n) es la suma de g(n) y h(n), y es el valor calculado más corto.
En palabras humanas lo que se necesita es:
– Un nodo o punto inicial
– Un nodo final que representa el final del algoritmo
– Un método para identificar que nodos son traspasables y cuales son
sólidos
– Un método para determinar el costo directo (g) de moverse entre los
nodos
– Un método para determinar el costo indirecto (h)
– Una lista de nodos abiertos, en esta lista se guardaran todos los nodos
que se han identificado como posibles movimientos, pero aún no han sido
evaluados
– Una lista de nodos cerrados, donde se guardaran todos los nodos
evaluados y descartados, aunque no es necesario una lista, basta con un
estado que indique que el nodo se encuentra cerrado
– Una forma de identificar que nodo procede a otro, para poder retornar la
cadena de los nodos
Vamos a continuar con los tutoriales del mapa de tiles, para poder satisfacer la
forma de identificar los nodos sólidos y los que son traspasables y cada nodo
será representado por un tile.
Si vamos a permitir que los actores del juego puedan moverse diagonalmente,
debemos darle un costo directo más bajo al movimiento en diagonal que al
movimiento en línea recta de dos nodos, por ejemplo:

En la imagen anterior, la línea amarilla representa el movimiento de ir del nodo


azul claro al nodo azul oscuro, pero sin hacer ningún movimiento diagonal, si el
costo de moverse de un nodo a otro es de 10, el resultado de la línea amarilla
es de 20, en cambio el movimiento de la línea roja, que es diagonal debe ser
de 15 o 14, que sería el resultado del teorema de Pitágoras.
Ahora, para el valor indirecto se usa la heurística que es un cálculo aproximado
de ir de un nodo actual al nodo final o destino, en la mayoría de casos usan
como heurística la distancia de Manhattan o la distancia Euclidiana , en nuestro
caso vamos a implementar varias para ver la diferencia de ellas.

Cómo funciona?
El algoritmo es el siguiente:
1. Si el nodo inicial es igual al nodo final, se retorna el nodo inicial como solución
2. Si no, se adiciona el nodo inicial a la lista abierta
3. Mientras la lista abierta no esté vacía, se recorre cada nodo que haya en la lista abierta y
se toma el que tenga el costo total más bajo
4. Si el nodo obtenido es igual al nodo final, se retornan todos los nodos sucesores al nodo
encontrado
5. Si no , se toma el nodo y se elimina de la lista abierta para guardarse en la lista cerrada
y se buscan todos los nodos adyacentes al nodo obtenido y se adicionan a la lista
abierta a menos que el nodo se encuentre en la lista cerrada o que el nodo sea sólido
6. Si el nodo adyacente ya se encuentra en la lista abierta se verifica que el costo sea
menor, si es menor se cambian los valores de costo, sino se ignora
7. Se vuelve al paso 3 y se repite hasta que el punto 4 sea verdadero o que la lista abierta
quede vacía

Siempre he dicho que una imagen vale más que mil palabras por eso vamos a
tomar como ejemplo la siguiente imagen

El nodo inicial es el nodo (2,4) o el que esta de color azul, el nodo final es el
nodo (3,2) o nodo rojo, los nodos de color verde son nodos sólidos y no pueden
ser traspasados.
Al empezar el algoritmo tendremos el nodo azul como opción y lo agregamos a
la lista abierta, luego como no es el nodo final, obtenemos sus nodos
adyacentes y luego lo dejaremos en la lista cerrada, calculamos los valores de
los nodos de la lista abierta (a menos que ya los hayamos calculado) y
tomamos el de menor valor:

La heurística que se ha usado es la distancia de Manhattan:


H = Math.Abs(nodoActual.X – nodoFinal.X) + Math.Abs(nodoActual.Y –
nodoFinal.Y)
Como ejemplo tomamos el cuadrado B (1,4) hasta el nodo final (3,2)
H = (1 – 3) + (4 – 2)
H= 2+2
H=4
Lo que hicimos fue contar los cuadrados que hay para llegar del nodo actual al
nodo final.
Ahora, el nodo que tiene el menor valor es C, por lo tanto buscamos sus nodos
adyacentes, dando como resultado el nodo E y F porque los demás son sólidos
y el nodo (4,5) aunque es alcanzable diagonalmente, es inalcanzable porque
uno de los nodos cerca es sólido.
Se añade el nodo C a la lista cerrada y los demás nodos a la lista abierta, pero
como ya se encuentran, se verifica que el valor calculado no sea menor, los
costos nuevos son de los nodos son:

Como el costo no es menor que el que tienen en la lista abierta, se descartan y


se vuelve a tomar el menor valor de la lista abierta, lo que da un empate entre
B y E, pero si tomamos el nodo E, volveríamos a descartar los demás nodos,
volviendo a tomar el valor B porque el E es enviado a la lista cerrada.
Si se sigue con el algoritmo va a dar lo siguiente:

El algoritmo en el código:
Ahora que se ha “explicado” de qué se trata el algoritmo, vamos a crear las
clases y métodos necesarios para implementar el algoritmo.
Agregamos una clase al proyecto SceneManager ( Estoy continuando con el
proyecto de Motor de Tiles), la clase es llamada nodo y contiene la información
del nodo:
1 public class Nodo

2 {

3 #region declaraciones

public Nodo NodoPadre;


4
public Nodo NodoFinal;
5
private Vector2 posicion;
6
public float costoTotal;
7
public float costoG;
8 public bool Cerrado = false;
9 #endregion

10

11 #region propiedades

12 public Vector2 Posicion

{
13
get
14
{
15
return posicion;
16
}
17
set
18 {
19 posicion = new Vector2((float)MathHelper.Clamp(value.X, 0f, (float)Camara2D.an

20 (float)MathHelper.Clamp(value.Y, 0f, (float)Camara2D.anchoTile * Camara2D.numY

21 }

}
22

23
public Int32 GrillaX
24
{
25
get { return (Int32)posicion.X; }
26
}
27

28
public Int32 GrillaY
29 {
30 get { return (Int32)posicion.Y; }

31 }
32 #endregion

33

34 public Nodo(Nodo nodoPadre, Nodo nodoFinal, Vector2 posicion, float costo)

{
35
NodoPadre = nodoPadre;
36
NodoFinal = nodoFinal;
37
Posicion = posicion;
38
costoG = costo;
39 if (nodoFinal != null)
40 {

41 costoTotal = costoG + Calcularcosto();

42 }

43 }

44
public float Calcularcosto()
45
{
46
return Math.Abs(GrillaX - NodoFinal.GrillaX) + Math.Abs(GrillaY - NodoFinal.Gri
47
}
48

49
public Boolean esIgual(Nodo nodo)
50 {
51 return (Posicion == nodo.Posicion);

52 }

53 }

54

55

56

57

58

La variable NodoPadre sirve para saber que nodo lo precede, y así poder
después formar una cadena con el camino más corto, la variable NodoFinal
indica el nodo destino al cual hay que llegar, esta variable sirve para calcular el
costo indirecto o la heurística.
La variable Posicion representa las coordenadas del nodo en el mapa, las
propiedades GrillaX y GrillaY permiten acceder más rápido a las coordenadas.
Cada vez que se crea un nuevo nodo, se calcula su costo total, con el costo
directo (g) y su costo indirecto (h), la condición de que el nodo final sea
diferente a nulo, es para cuando se crea el nodo final, el método esIgual es útil
para comparar dos nodos, verificando sus posiciones.
Ahora, agregamos otra clase que servirá para encontrar la ruta, la llamaremos
gestorBusqueda:
public class gestorBusqueda
1
{
2
private const Int32 costoIrDerecho = 10;
3
private const Int32 costoIrDiagonal = 15;
4
private List<Nodo> listaAbierta = new List<Nodo>();
5 private List<Vector2> listaCerrada = new List<Vector2>();
6 public TileEngine motor;

8 public gestorBusqueda(TileEngine motor)

9 {

this.motor = motor;
10
}
11

12
/// <summary>
13
/// Adiciona un Nodo a la lista abierta, ordenadamente
14
/// </summary>
15
/// <param name="nodo"></param>
16 private void adicionarNodoAListaAbierta(Nodo nodo)
17 {

18 Int32 indice = 0;

19 float costo = nodo.costoTotal;

while ((listaAbierta.Count() > indice) &&


20
(costo < listaAbierta[indice].costoTotal))
21
{
22
indice++;
23
}
24
listaAbierta.Insert(indice, nodo);
25 }

26

27 public List<Vector2> encontrarCamino(Vector2 posTileInicial, Vector2 posTileFin

{
28
if (motor == null)
29
{
30
return null;
31
}
32 Tile tileInicial = motor.Mapa.tileMapLayers[0].obtenerTile((int)posTileInicial
33 Tile tileFinal = motor.Mapa.tileMapLayers[0].obtenerTile((int)posTileFinal.X,

34

35 if (tileInicial.Colision || tileFinal.Colision)

36 {

37 return null;

}
38

39
listaAbierta.Clear();
40
listaCerrada.Clear();
41

42
Nodo nodoInicial;
43
Nodo nodoFinal;
44

45 nodoFinal = new Nodo(null, null, posTileFinal, 0);


46 nodoInicial = new Nodo(null, nodoFinal, posTileInicial, 0);

47

48 // se adiciona el nodo inicial

49 adicionarNodoAListaAbierta(nodoInicial);

50 while (listaAbierta.Count > 0)

{
51
Nodo nodoActual = listaAbierta[listaAbierta.Count - 1];
52
// si es el nodo Final
53
if (nodoActual.esIgual(nodoFinal))
54
{
55
56 List<Vector2> mejorCamino = new List<Vector2>();

57 while (nodoActual != null)

{
58
mejorCamino.Insert(0, nodoActual.Posicion);
59
nodoActual = nodoActual.NodoPadre;
60
}
61
return mejorCamino;
62 }
63 listaAbierta.Remove(nodoActual);
64

65 foreach (Nodo posibleNodo in encontrarNodosAdyacentes(nodoActual, nodoFinal))

66 {

67 // si el nodo no se encuentra en la lista cerrada

if (!listaCerrada.Contains(posibleNodo.Posicion))
68
{
69
// si ya se encuentra en la lista abierta
70
if (listaAbierta.Contains(posibleNodo))
71
{
72 if (posibleNodo.costoG >= posibleNodo.costoTotal)
73 {

74 continue;

75 }

76 }

adicionarNodoAListaAbierta(posibleNodo);
77
}
78
}
79
// se cierra el nodo actual
80
listaCerrada.Add(nodoActual.Posicion);
81 }
82 return null;

83 }

84

85 private List<Nodo> encontrarNodosAdyacentes(Nodo nodoActual, Nodo nodoFinal)

86 {
87 List<Nodo> nodosAdyacentes = new List<Nodo>();

88 Int32 X = nodoActual.GrillaX;

Int32 Y = nodoActual.GrillaY;
89
Boolean arribaIzquierda = true;
90
Boolean arribaDerecha = true;
91
Boolean abajoIzquierda = true;
92
Boolean abajoDerecha = true;
93

94 //Izquierda
95 if ((X > 0) && (!motor.Mapa.tileMapLayers[0].obtenerTile(X - 1, Y).Colision))

96 {

97 nodosAdyacentes.Add(new Nodo(nodoActual, nodoFinal, new Vector2(X - 1, Y), cost

98 }

else
99
{
100
arribaIzquierda = false;
101
abajoIzquierda = false;
102
}
103

104 //Derecha
105 if ((X < motor.Mapa.NumXTiles - 1) && (!motor.Mapa.tileMapLayers[0].obtenerTile

106 {

107 nodosAdyacentes.Add(new Nodo(nodoActual, nodoFinal,

108 new Vector2(X + 1, Y), costoIrDerecho + nodoActual.costoG));

}
109
else
110
{
111
arribaDerecha = false;
112
abajoDerecha = false;
113 }
114

115 //Arriba

116 if ((Y > 0) && (!motor.Mapa.tileMapLayers[0].obtenerTile(X, Y - 1).Colision))

117 {
118 nodosAdyacentes.Add(new Nodo(nodoActual, nodoFinal,

119 new Vector2(X, Y - 1), costoIrDerecho + nodoActual.costoG));

}
120
else
121
{
122
arribaIzquierda = false;
123
arribaDerecha = false;
124 }
125

126 // Abajo

127 if ((Y < motor.Mapa.NumYTiles - 1) && (!motor.Mapa.tileMapLayers[0].obtenerTile

128 {

129 nodosAdyacentes.Add(new Nodo(nodoActual, nodoFinal,

new Vector2(X, Y + 1), costoIrDerecho + nodoActual.costoG));


130
}
131
else
132
{
133
abajoIzquierda = false;
134 abajoDerecha = false;
135 }

136

137 // Dirección Diagonal

138 if ((arribaIzquierda) && (!motor.Mapa.tileMapLayers[0].obtenerTile(X - 1, Y - 1

139 {

nodosAdyacentes.Add(new Nodo(nodoActual, nodoFinal,


140
new Vector2(X - 1, Y - 1), costoIrDiagonal + nodoActual.costoG));
141
}
142

143
if ((arribaDerecha) && (!motor.Mapa.tileMapLayers[0].obtenerTile(X + 1, Y - 1).
144
{
145 nodosAdyacentes.Add(new Nodo(nodoActual, nodoFinal,
146 new Vector2(X + 1, Y - 1), costoIrDiagonal + nodoActual.costoG));

147 }

148
149 if ((abajoIzquierda) && (!motor.Mapa.tileMapLayers[0].obtenerTile(X - 1, Y + 1)

150 {

nodosAdyacentes.Add(new Nodo(nodoActual, nodoFinal,


151
new Vector2(X - 1, Y + 1), costoIrDiagonal + nodoActual.costoG));
152
}
153

154
if ((abajoDerecha) && (!motor.Mapa.tileMapLayers[0].obtenerTile(X + 1, Y + 1).C
155
{
156 nodosAdyacentes.Add(new Nodo(nodoActual, nodoFinal,
157 new Vector2(X + 1, Y + 1), costoIrDiagonal + nodoActual.costoG));

158 }

159

160 return nodosAdyacentes;

161 }

}
162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

Primero declaramos dos constantes, que indicarán el valor directo de moverse


linealmente y moverse diagonalmente, también se tiene la lista abierta, la lista
cerrada que guarda la posición y no el nodo, debido a que con la lista no
podemos usar el método Contains con la clase Nodo, también se tiene una
referencia a la clase TileEngine para poder identificar los nodos traspasables y
los que no.
En el constructor de la clase enviamos la referencia a la clase TileEngine.
Tenemos un método que adiciona un nodo a la lista abierta, pero
ordenadamente (adicionarNodoAListaAbierta).
El método encontrarCamino recibe como parámetro dos vectores, uno con la
posición inicial y otro con la posición final y devuelve una lista de las posiciones
o el camino más corto, apenas empieza se verifica que la referencia al motor
no esté nula, si lo es se retorna nulo.
Los vectores recibidos en el método, se convierte en Tiles y luego se verifican
que ninguno de los dos sea sólido, si lo son se retorna nulo.
Si no son sólidos, entonces se crean los Nodos inicial y final, luego se
implementa el algoritmo, el método encontrarNodosAdyacentes busca todos los
nodos cercanos al nodo actual y se verifican los posibles movimientos, también
que el nodo no se sobrepase de los límites del mapa.
Ahora para verificar el algoritmo, haremos unos pequeños cambios al código
– Adicionamos una propiedad a la clase Tile que indicará el color del tinte:
1
public Color Color { get; set; }
2 public Tile(Int32 tipo)
3 {

4 Tipo = tipo;

5 Colision = false;

Color = Color.White;
6
}
7

En la clase game1 del proyecto ColisionMapa, declaramos una variable para el


gestor de búsqueda:
1 gestorBusqueda GestorBusqueda;

En el método LoadContent, justo después de inicializar la variable mapa,


inicializamos la variable:
1 GestorBusqueda = new gestorBusqueda(mapa);

En el método Initialize mostramos el Mouse:


1 IsMouseVisible = true;
Y en el método Draw, después del Spritebatch.Begin() creamos un código
temporal que tomará el Tile donde se haga clic como posición inicial del
algoritmo y la posición del tanque como posición final, si el método
encontrarCamino devuelve la lista con los nodos del camino, pintaremos cada
Tile para indicar el camino:
1

2 // Código Temporal
MouseState ms = Mouse.GetState();
3
if ((ms.X > 0) && (ms.Y > 0) &&
4
(ms.X < Camara2D.TamanoPantalla.X) && (ms.Y < Camara2D.TamanoPantalla.Y))
5
{
6
Vector2 mouseLoc = Camara2D.ScreenToWorld(new Vector2(ms.X, ms.Y));
7 if (ms.LeftButton == ButtonState.Pressed)
8 {

9 mouseLoc = mapa.Mapa.GetSquareAtPixel(mouseLoc);

10 posTile = mapa.Mapa.GetSquareAtPixel(new Vector2(tanque.Posicion.X + (distancia.X


+ distancia);
11

12
List<Vector2> camino = GestorBusqueda.encontrarCamino(mouseLoc, posTile);
13 if (camino != null)
14 {

15 foreach (Vector2 nodo in camino)

16 {

17 Tile tile = mapa.Mapa.tileMapLayers[0].obtenerTile((int)nodo.X, (int)nodo.Y);

tile.Color = new Color(128, 0, 0, 80);


18
}
19
}
20
}
21
}
22// fin codigo temporal

23

Y para finalizar, en la clase TileMap antes de dibujarlo:


1 if (debug)

2 {

3 if (tile.Colision)
4 {

5 tile.Color = Color.Red;

}
6
else
7
{
8
tile.Color = Color.White;
9
}
10 }
11 else
12 {

13 if (tile.Color == Color.Transparent)

14 {

tile.Color = Color.White;
15
}
16
}
17

18
spriteBatch.Draw(layer.sheet.Textura, Vector2.Zero, sourceRect, tile.Color,
19
0, position, scale, SpriteEffects.None, 0.0f);
20

21

Y al ejecutar el proyecto podemos ver lo siguiente:


He modificado el código, para mostrar el tiempo que transcurre desde que se
llama el método de encontrar el camino hasta que retorna un resultado,
además del total de nodos abiertos y cerrados, y la opción de cambiar la
heurística:

Nuestro primer algoritmo


heurístico
October 31, 2012Kiko Correoso

algoritmosbúsqueda en escaladaheurísticahill-climbingoptimizaciónpython

Normalmente, cuando se trata de optimizar 'algo' se pueden usar


diferentes aproximaciones.
 La primera sería el uso de algoritmos de búsqueda exhaustivos como la fuerza bruta. Estos
algoritmos nos dan la solución exacta pero si el espacio de búsqueda es muy amplio el
tiempo de cálculo necesario puede ser inabordable (explosión combinatoria).
 Otra aproximación, que es la que veremos hoy, sería el uso de algoritmos heurísticos. La
palabra heurística viene del griego y vendría a significar algo así como 'relativo a la
búsqueda' (recordad, eureka significa 'lo he encontrado' y tiene el mismo origen
etimológico). Estos algoritmos sacrifican la exactitud de la solución en favor del tiempo de
respuesta, es decir, intentamos obtener soluciones lo suficientemente buenas con un tiempo
de respuesta corto o aceptable.
Vamos a proponer un problema sencillo y lo vamos a resolver de
forma aproximada usando un algoritmo heurístico de 'búsqueda en
escalada' o 'hill-climbing'.

Vamos a ello, la resolución del problema es obvia en este caso, pero


solo se trata de que veáis el funcionamiento de todo esto. Tenemos
un conjunto de n números que se hallan dentro de un subconjunto de
los números reales. ¿Cuál sería el conjunto de estos n números que
me permiten minimizar lo siguiente?

sumNk=1k2
Para simplificar permitiremos que los n números se puedan repetir
por lo que la respuesta obvia es que el conjunto de los números sea
[0, 0, 0, ..., 0] por lo que la solución exacta sería 0. Si pensamos por
un momento que la solución no es obvia, la búsqueda exhaustiva
nunca sería óptima puesto que estamos buscando entre los números
reales. Por tanto, ¿como resolveríamos esto con una búsqueda
heurística? Primero de todo pondremos un poco de código, lo
explicamos y luego haremos uso del mismo para ver los resultados
que obtenemos:

import numpy as np

import matplotlib.pyplot as plt

plt.ion()

class Busqueda:

u"""

Clase (fea) para explicar el funcionamiento de

un algoritmo heurístico

"""
def __init__(self, ngeneraciones, ngenes, lim_sup, lim_inf):

self.ngeneraciones = ngeneraciones

self.ngenes = ngenes

self.lim_sup = lim_sup

self.lim_inf = lim_inf

def padre(self):

self.individuo0 = np.random.rand(self.ngenes) *

(self.lim_sup - self.lim_inf) + self.lim_inf

return self.individuo0

def fitness(self, individuo):

funcion = individuo * individuo

return np.sum(funcion)

def hijo(self, individuo):

nindividuo = individuo + np.random.normal(0,1, self.ngenes)

nindividuo[nindividuo < self.lim_inf] = self.lim_inf

nindividuo[nindividuo > self.lim_sup] = self.lim_sup

return nindividuo

def calculos(self):

fit_acum = []

individuo = self.individuo0

poblacion = self.individuo0

fit_acum = np.append(fit_acum, self.fitness(individuo))

for generacion in xrange(self.ngeneraciones - 1):

nindividuo = self.hijo(individuo)

if self.fitness(nindividuo) <= fit_acum[-1]:

individuo = nindividuo

fit_acum = np.append(fit_acum, self.fitness(individuo))

poblacion = np.append(poblacion, individuo)

return poblacion.reshape(self.ngeneraciones, self.ngenes), fit_acum


def representa_proceso(self, poblacion, fitness_acumulado):

plt.subplot(311)

plt.xlim(self.lim_inf, self.lim_sup)

plt.ylim(0., self.lim_sup ** 2.)

for gen in range(self.ngenes):

plt.text(poblacion[0, gen],

self.fitness(poblacion[0, gen]),

gen+1)

plt.subplot(312)

plt.xlim(self.lim_inf, self.lim_sup)

plt.ylim(0., self.lim_sup ** 2.)

for generacion in range(self.ngeneraciones):

for gen in range(self.ngenes):

plt.text(poblacion[generacion, gen],

self.fitness(poblacion[generacion, gen]),

gen+1)

plt.subplot(313)

plt.xlim(0, self.ngeneraciones)

plt.ylim(0., np.max(fitness_acumulado))

plt.plot(fitness_acumulado)

Vamos a guardar el anterior código en un fichero que se


llame heuristico.py y vamos a abrir ipython en el mismo lugar donde
se encuentra el fichero heuristico.py.
Hemos creado una clase llamada Busqueda que hemos de inicializar
con una serie de parámetros, el primero sería el número de iteraciones
que vamos a usar en nuestra búsqueda, el segundo sería el número de
miembros que vamos a usar, n, y el tercer y cuarto serían el límite
superior e inferior del intervalo de números reales donde vamos a
restringir la búsqueda. Dentro de ipython hacemos lo siguiente:
In [1]: from heuristico import Busqueda

In [2]: busqueda = Busqueda(1000, 3, 100, -100)


Vamos a hacer una simulación que contará con 1000 generaciones (o
pasos, o iteraciones, como más os guste), la solución tendrá en cuenta
un conjunto de 3 números que estarán restringidos entre [-100, 100].

Lo siguiente será crear la primera solución (la solución padre de la


que derivarán todas las soluciones hijas siguientes) que no es más que
una solución aleatoria para empezar la búsqueda. Lo hacemos de la
siguiente forma:

In [3]: busqueda.padre()

Out[3]: array([-56.14070247, 96.07815303, 31.11068139])


Veis que el primer miembro ha tomado un valor en torno a -56, el
segundo un valor en torno a 96 y el tercero un valor en torno a 31.

Ahora vamos a ver la chicha del algoritmo. El cálculo completo se


realiza en el método calculos, que a su vez hace uso de los
métodos fitness e hijo. El método fitness es el que calcula la solución
del problema (en este caso se trata de minimizar el valor). El
método hijo es el que crea una posible solución nueva cercana a la
solución actual (solución padre). En el método calculos hacemos las
iteraciones (1000 en este ejemplo) para comparar al padre con el hijo
y ver si tiene mejor fitness y en caso de que sea así actualizamos al
padre con el hijo, es decir, le pasamos los valores del hijo al padre.
In [4]: pob, fit_acum = busqueda.calculos()

Hemos calculado la población final, pob, el conjunto de 3 elementos


que nos minimizan el resultado, y hemos obtenido el valor de la
función fitness en cada iteración, fit_acum.
Por último, vamos a hacer una representación de los resultados de la
siguiente manera:

In [5]: busqueda.representa_proceso(pob, fit_acum)

Que nos mostraría la siguiente ventana:


Donde el primer panel (arriba), nos muestra la posición de la primera
solución aleatoria que hemos generado con el método padre. Como
hemos visto anteriormente, el primer miembro ha tomado un valor en
torno a -56, el segundo un valor en torno a 96 y el tercero un valor en
torno a 31. En el segundo panel (en medio) vemos como va
evolucionando la solución, cada uno de los miembros de la solución
va descendiendo poco a poco hacia la solución final (cercana a cero).
Por último, en el tercer panel (abajo) vemos el valor de nuestra
solución y como se va a cercando a una solución cercana a 0, de
hecho, vemos que a partir de las 300 iteraciones ya estaríamos cerca
de 0. Aunque después de las 1000 iteraciones no hemos alcanzado la
solución exacta, 0, puesto que estos algoritmos dan valores
aproximados.
Veamos un segundo caso:
In [1]: from heuristico import Busqueda # Solo necesario si habéis cerrado ipython

In [2]: busqueda = Busqueda(1000, 20, 200, 0)

In [3]: busqueda.padre()

Out[3]:

array([ 71.65433435, 13.38201722, 57.30044929, 74.69591418,

197.39440847, 42.3185271 , 69.08520774, 7.40961601,

79.87846973, 161.14232418, 135.47599187, 183.17622057,

30.69728341, 21.85297791, 157.85448115, 139.02125074,

98.8888481 , 49.02073966, 46.10618954, 116.69194813])

In [4]: pob, fit_acum = busqueda.calculos()

In [5]: busqueda.representa_proceso(pob, fit_acum)

Que nos daría el siguiente resultado:


En este caso vemos hemos usado un conjunto de 20 números y
observamos que quizá serían necesarias más iteraciones para llegar a
un número más aproximado.

Normalmente, el algoritmo se para cuando se han superado las


iteraciones o cuando la solución no ha mejorado en un número
determinado de pasos (esto segundo no lo he implementado por lo
que lo tenéis como ejercicio).

Saludos.

P.D.: Como siempre, se agradece cualquier corrección, crítica


constructiva, propuesta de mejora, debate,..., en los comentarios.

También podría gustarte