Algoritmo de Búsqueda en Anchura (BFS) en Grafos
Introducción
La búsqueda en anchura (Breadth‑First Search, BFS) es uno de los algoritmos fundamentales para recorrer o explorar grafos. Se utiliza cuando se necesita encontrar la ruta más corta en grafos no ponderados, detectar componentes conectados o generar niveles jerárquicos (por ejemplo, en árboles de decisión).
En este artículo profundizaremos en su funcionamiento, veremos una implementación completa en Python 3.11+, compararemos sus características con la búsqueda en profundidad (DFS) y ofreceremos buenas prácticas para su uso en entornos de producción.
Fundamentos teóricos de BFS
- Estructura de datos principal: una cola FIFO que garantiza que los nodos se procesen en el orden de su descubrimiento.
- Orden de visita: nivel por nivel, es decir, todos los vértices a distancia d del origen se visitan antes que los de distancia d+1.
- Complejidad temporal:
O(|V| + |E|), donde|V|es el número de vértices y|E|el número de aristas. - Complejidad espacial:
O(|V|)para almacenar el conjunto de vértices visitados y la cola.
Implementación paso a paso en Python
A continuación se muestra una versión idiomática que utiliza collections.deque y tipado estático con typing para mayor claridad.
from __future__ import annotations
import collections
from typing import Dict, List, Set, Tuple
# Tipo alias para mayor legibilidad
Graph = Dict[int, List[int]]
def bfs(graph: Graph, start: int) -> Tuple[List[int], Dict[int, int]]:
"""Recorre graph desde start usando BFS.
Devuelve una lista con el orden de visita y un diccionario que
mapea cada nodo a su distancia (número de aristas) desde start.
"""
visited: Set[int] = set()
distance: Dict[int, int] = {start: 0}
order: List[int] = []
queue: collections.deque[int] = collections.deque([start])
visited.add(start)
while queue:
vertex = queue.popleft()
order.append(vertex)
for neighbour in graph.get(vertex, []):
if neighbour not in visited:
visited.add(neighbour)
distance[neighbour] = distance[vertex] + 1
queue.append(neighbour)
return order, distance
# Ejemplo de uso ----------------------------------------------------------
if __name__ == "__main__":
sample_graph: Graph = {
1: [2, 3],
2: [4, 5],
3: [6],
4: [],
5: [6],
6: []
}
order, dist = bfs(sample_graph, 1)
print("Orden de visita:", order)
print("Distancias desde 1:", dist)
Salida esperada:
Orden de visita: [1, 2, 3, 4, 5, 6]
Distancias desde 1: {1: 0, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2}
Comparativa rápida: BFS vs DFS
BFS (Breadth‑First Search)
- Explora por niveles.
- Ideal para caminos más cortos en grafos no ponderados.
- Uso de cola FIFO.
- Mayor consumo de memoria en grafos amplios.
DFS (Depth‑First Search)
- Explora profundizando en cada rama.
- Útil para topological sort, detección de ciclos.
- Uso de pila (recursiva o explícita).
- Menor consumo de memoria, pero no garantiza el camino más corto.
Casos de uso en el mundo real
- Redes sociales: cálculo de grados de separación entre usuarios.
- Sistemas de recomendación: exploración de grafos de productos para encontrar ítems a distancia mínima.
- Juegos de tablero: generación de movimientos posibles en el número mínimo de turnos.
- Algoritmos de routing: protocolos como OSPF utilizan BFS para construir árboles de menor coste en topologías no ponderadas.
Mejores prácticas y optimizaciones
- Usar
dequeen lugar de listas: la operaciónpopleft()es O(1). - Evitar la recursión: BFS no se adapta a recursión; usar estructuras iterativas previene desbordamiento de pila.
- Limitar la profundidad: en grafos muy extensos, aplicar un depth limit para evitar recorridos infinitos en grafos cíclicos.
- Persistencia de resultados: almacenar distancias en una base de datos (ej. Redis) cuando se reutilizan consultas frecuentes.
- Paralelismo: en grafos masivos, dividir la cola entre varios workers (ej. con
multiprocessingoRay) manteniendo un conjunto de nodos ya visitados thread‑safe.
Resolución de problemas comunes
| Problema | Posible causa | Solución |
|---|---|---|
| Algoritmo no termina | Grafos con ciclos y falta de marcaje de nodos visitados | Asegúrate de mantener un set de vértices visitados antes de encolar. |
| Memoria excesiva | Cola crece demasiado por grafos densos | Implementa un frontier pruning o usa BFS bidireccional cuando buscas un camino entre dos nodos. |
| Distancias incorrectas | Actualización de la distancia fuera de orden | Incrementa la distancia en base al nodo sacado de la cola, no al vecino. |
Compatibilidad, rendimiento y escalabilidad
El algoritmo BFS es independiente del lenguaje, pero su rendimiento varía según la implementación de la cola y la estructura de grafo:
- Python:
dequees la opción más rápida; evita listas conpop(0). - C / C++:
std::queueostd::dequeofrecen O(1) amortizado. - Java:
ArrayDequesupera aLinkedListen operaciones de inserción y extracción.
Para grafos con cientos de millones de nodos, considera:
- Almacenar el grafo en una base de datos de grafos (Neo4j, Amazon Neptune) y delegar la expansión de vecinos al motor.
- Utilizar BFS distribuido (ej. Pregel, Apache Giraph).
Conclusión
La búsqueda en anchura es una herramienta esencial para cualquier ingeniero de datos, científico de datos o desarrollador de backend que trabaje con grafos. Su simplicidad, garantizada optimalidad en caminos más cortos y facilidad de paralelización la convierten en la elección predeterminada en numerosos escenarios productivos.
Dominar su implementación en Python y comprender sus trade‑offs frente a DFS permitirá diseñar soluciones robustas y escalables.
Algoritmo de Búsqueda en Anchura (BFS) en Grafos: Conceptos, Implementación en Python y Mejores Prácticas