Búsqueda por anchura#
Introducción#
En esta entrada hablaremos de búsquedas en gráficas. Nos enfocaremos en la búsqueda por anchura. A grandes rasgos, nos permite comenzar en un vértice
Descripción de búsqueda por anchura#
Cuando hablamos de una búsqueda en una gráfica, nos referimos a un algoritmo que pase por todos sus vértices y todas sus aristas. En este contexto, usualmente trabajaremos con una gráfica dada por sus listas de adyacencia. Recordemos que esto quiere decir que para cada vértice conocemos a sus vecinos. Los algoritmos de búsqueda que mencionaremos explotarán esto: comenzarán con un vértice e irán explorando nuevos vértices que sean vecinos a los ya descubiertos. La diferencia entre distintos algoritmos es que deciden ir a vértices distintos en momentos distintos.
En este contexto, mientras estamos haciendo una búsqueda, conviene pensar a cada vértice en alguno (y sólo uno) de los siguientes estados:
Por encontrar. Todavía no hemos visto al vértice en nuestro recorrido.
Encontrado sin procesar. Ya vimos el vértice en algún momento de nuestro recorrido, pero aún no hemos visto a todos sus vecinos.
Procesado. Ya vimos al vértice y a todos sus vecinos.
La búsqueda por anchura es una manera de hacer la exploración. Si tomamos una gráfica
Se define una lista
de vértices por procesar, un conjunto de vértices encontrados y un conjunto de vértices procesados.Se agrega
a la lista y al conjunto .Mientras haya al menos un elemento
en , debemos procesarlo, es decir, explorar sus vecinos. Para ello, cada vecino de sin encontrar ni procesar se agrega al conjunto y al final de la lista (para procesarlo más adelante). Como ya apuntamos los vecinos de que falta visitar, pasamos de a .
La descripción anterior también nos permite encontrar todas las aristas. Cuando consideremos un vecino
Si
Antes de agregar un elemento a
Implementación de búsqueda por anchura#
Veamos algunos ejemplos de búsqueda por anchura. Para ello, primero daremos una implementación en Python, con apoyo de la librería networkx
. Como argumentos a la función, tenemos:
G
, la gráfica a explorar.v0
, el vértice donde comienza la búsqueda por anchura.
import networkx as nx
def BFS(G,v0):
encontrados={v0}
procesados=set()
en_proceso=[v0]
while en_proceso:
v=en_proceso[0]
print(v) #Aquí decidimos que hacer al comenzar a procesar un vértice
for w in G.neighbors(v):
if (w not in procesados) and (w not in encontrados):
encontrados.add(w)
en_proceso.append(w)
en_proceso.remove(v)
procesados.add(v)
Veamos qué hace el código anterior. Trabajemos con la siguiente gráfica.
G = nx.Graph()
G.add_edges_from([(0,1), (0,2), (0,3), (1,4), (7,4), (10,2), (2,3), (2,4), (3,4), (6,7), (5,7), (6,8), (8,9), (6,10)])
nx.draw_kamada_kawai(G,with_labels=True, node_color='#22bbbb',node_size=500)

Al correr la búsqueda por anchura que comience en el vértice
BFS(G,3)
3
0
2
4
1
10
7
6
5
8
9
En efecto, vamos recorriendo los vértices comenzando en
def BFS_aristas(G,v0):
encontrados={v0}
procesados=set()
en_proceso=[v0]
while en_proceso:
v=en_proceso[0]
for w in G.neighbors(v):
print((v,w)) ## Aquí decidimos qué hacer con la arista recién encontrada.
if (w not in procesados) and (w not in encontrados):
encontrados.add(w)
en_proceso.append(w)
en_proceso.remove(v)
procesados.add(v)
BFS_aristas(G,3)
(3, 0)
(3, 2)
(3, 4)
(0, 1)
(0, 2)
(0, 3)
(2, 0)
(2, 10)
(2, 3)
(2, 4)
(4, 1)
(4, 7)
(4, 2)
(4, 3)
(1, 0)
(1, 4)
(10, 2)
(10, 6)
(7, 4)
(7, 6)
(7, 5)
(6, 7)
(6, 8)
(6, 10)
(5, 7)
(8, 6)
(8, 9)
(9, 8)
Se muestran las aristas en el orden que las encuentra la búsqueda por anchura. Cada arista queda enlistada dos veces, una cada vez que cada uno de sus extremos la descubre. Usualmente dejamos la implementación así pues:
Es sencillo que sólo se enliste una vez cada arista rompiendo la simetría, por ejemplo, agregando un condicional
if u<v
.En gráficas no dirigidas sí es importante revisar ambas direcciones de
a y de a pues puede que en una de ellas tengamos flecha y en la otra no.
Algunas propiedades teóricas de búsqueda por anchura#
Tomemos una gráfica conexa
Como la búsqueda por anchura visita todos los vértices, la gráfica así construida es conexa. Como ágregamos aristas sólo la primera vez que descubrimos un vértice, entonces no tiene ciclos. Así, a esta gráfica le llamamos el árbol por anchura. Es una subgráfica de
Ejemplo. Consideremos la gráfica definida en el siguiente código.
G = nx.Graph()
G.add_edges_from([(0,1), (0,2), (0,3), (1,4), (7,4), (10,2), (2,3), (2,4), (3,4), (6,7), (5,7), (6,8), (8,9), (6,10)])
nx.draw_kamada_kawai(G,with_labels=True, node_color='#22bbbb',node_size=500)
KKL=nx.kamada_kawai_layout(G)

El siguiente código hace una búsqueda por anchura que comienza en padres
que nos dice cuál vértice fue descubrierto por cuál. Al final esto es lo que la función regresa.
def BFS_padres(G,v0):
encontrados={v0}
procesados=set()
en_proceso=[v0]
padres={v0:None}
while en_proceso:
v=en_proceso[0]
for w in G.neighbors(v):
if (w not in procesados) and (w not in encontrados):
encontrados.add(w)
en_proceso.append(w)
padres[w]=v
en_proceso.remove(v)
procesados.add(v)
return(padres)
dic_padres=BFS_padres(G,2)
print(dic_padres)
{2: None, 0: 2, 10: 2, 3: 2, 4: 2, 1: 0, 6: 10, 7: 4, 8: 6, 5: 7, 9: 8}
Para entender mejor este árbol de padres, vamos a dibujarlo como una gráfica dirigida, en donde pondremos una flecha de un vértice
D=nx.DiGraph()
for j in dic_padres:
if dic_padres[j]!=None:
D.add_edge(j,dic_padres[j])
nx.draw(D,pos=KKL,with_labels=True, node_color='#22bbbb',node_size=500)

Lo que nos está diciendo este dibujo es, por ejemplo, que
Veamos algunas propiedades de la búsqueda por anchura y del árbol por anchura. En las siguientes proposiciones
Proposición. Sean
Demostración. Haremos la demostración por inducción sobre la distancia de
Supongamos que la afirmación es cierta para cuando
Sea
Por hipótesis inductiva,
Podemos ser todavía más precisos.
Proposición. Si
Demostración. Un camino de longitud
Veamos que no puede ser menor a
Así, la distancia de
Tarea moral#
Los siguientes problemas te ayudarán a practicar lo visto en esta entrada. Para resolverlos, necesitarás usar herramientas matemáticas, computacionales o ambas.
Dibuja una gráfica con el siguiente conjunto de aristas. Luego, realiza manualmente una búsqueda por anchura que comience en el vértice
:Sean
y el número de vértices y aristas, respectivamente, de una gráfica. Encuentra gráficas en donde la búsqueda por anchura tome pasos. Encuentra gráficas en donde la búsqueda por anchura tome pasos.Sea
el arbol de anchura de una gráfica con vértice inicial . El nivel de un vértice es la cantidad de veces que hay que aplicarle la funciónpadre
para llegar a . Supongamos que y son vértices adyacentes en . Muestra que .Cada vértice de una gráfica se ha pintado rojo o azul. Haz un algoritmo de búsqueda por profundidad que cuente la cantidad de aristas que hay con un extremo rojo y uno azul.
Usando búsqueda por anchura, diseña un algoritmo que decida si una gráfica es conexa o no.