Atlasingeniería

Estructuras de datosTablas hash y grafosTema 3

BFS y DFS

Los dos recorridos de un grafo se diferencian en una sola decisión: qué vértice se procesa primero. De ahí salen el camino más corto en aristas y la detección de ciclos.

Para este tema conviene tener claro:Representación de grafosPilas y colas

Recorrer un grafo es visitar todo lo alcanzable desde un punto sin repetir ni perderse. Hay dos formas de hacerlo, y la única diferencia entre ellas es de qué estructura se saca el próximo vértice. Ese detalle cambia por completo lo que cada recorrido puede responder.

El mismo esqueleto para los dos

El esquema es común: una estructura con los vértices pendientes y una marca de visitados. Se saca uno, se procesa y se agregan sus vecinos no visitados.

Si la estructura es una cola, el recorrido es en anchura (BFS): sale primero el que llegó primero, y el grafo se explora por capas. Si es una pila, es en profundidad (DFS): sale el último que entró, y el recorrido se hunde por una rama hasta agotarla. Con listas de adyacencia, ambos cuestan O(V+E)O(V+E).

Antes de seguir, predecí

BFS sobre la red de seguidores de una cuenta muy popular. ¿Qué se llena primero?

La trampa está en cuándo se marca

Hay una trampa en el orden de las marcas. Marcar al sacar de la cola deja entrar el mismo vértice varias veces, una por cada vecino que lo empuja, y la cola se infla.

La versión correcta marca al encolar: un vértice entra a lo sumo una vez y la cola nunca supera VV elementos. Esa marca también es el lugar natural para guardar de quién vino cada vértice, que es lo que después permite reconstruir el camino.

BFS: por capas, y por qué da el camino más corto

BFS visita primero todos los vértices a distancia 1, después los de distancia 2, y así. Esa disciplina por capas le da su propiedad central: la primera vez que se alcanza un vértice es por un camino con la mínima cantidad de aristas.

De ahí sale el camino más corto en grafos sin pesos, y también la distancia mínima a varios destinos en una sola pasada. Con pesos distintos deja de valer: ahí hace falta Dijkstra.

Arrancamos en A. Lo marcamos al encolarlo, no al sacarlo.

1 / 5
BFS desde A. Los vértices se pintan en el orden en que salen de la cola, y ese orden respeta distancias crecientes: por eso la primera vez que se llega a un vértice es por el camino más corto en cantidad de aristas.

DFS: hasta el fondo y volver

DFS avanza mientras haya por dónde y retrocede cuando se queda sin vecinos nuevos. Se escribe recursivo casi solo, con la pila de llamadas haciendo de pila explícita, aunque en grafos profundos conviene la versión iterativa para no desbordarla.

Lo útil de DFS son los tiempos: cuándo entra a un vértice y cuándo termina de explorarlo. Con esos dos números salen el orden topológico —los vértices por tiempo de finalización decreciente— y las componentes fuertemente conexas.

Detectar ciclos, que depende de si es dirigido

Detectar ciclos es la aplicación clásica, y depende de si el grafo es dirigido. En uno no dirigido, encontrar un vecino ya visitado que no sea el padre inmediato indica ciclo.

En uno dirigido no alcanza: hay que distinguir un vértice ya terminado de uno que sigue abierto en la rama actual. Sólo una arista hacia un vértice todavía en exploración cierra un ciclo. Es exactamente lo que detecta un sistema de compilación cuando avisa que hay dependencias circulares.

Cuál elegir según la pregunta

La elección sigue la pregunta. Si es “qué tan lejos” o “el camino más corto en pasos”, BFS. Si es “existe un camino”, “hay ciclo”, “en qué orden se puede hacer esto”, DFS.

También difieren en memoria: BFS guarda una capa entera, que en un grafo ancho puede ser enorme; DFS guarda una rama, que en un grafo profundo puede ser igual de cara. En grafos desconectados, cualquiera de los dos hay que arrancarlo desde cada vértice no visitado para cubrir todas las componentes.

Los dos, escritos

El esqueleto es literalmente el mismo y cambia una línea: de qué punta se saca el próximo.

type Graph = ReadonlyMap<string, readonly string[]>;

const breadthFirst = (graph: Graph, start: string): string[] => {
  const visited = new Set<string>([start]);
  const pending: string[] = [start];
  const order: string[] = [];

  while (pending.length > 0) {
    const current = pending.shift()!; // del frente: FIFO
    order.push(current);

    for (const neighbour of graph.get(current) ?? []) {
      if (visited.has(neighbour)) continue;
      visited.add(neighbour); // marcar al encolar, no al desencolar
      pending.push(neighbour);
    }
  }

  return order;
};

Cambiar shift por pop convierte eso en DFS, y con eso cambian las propiedades del recorrido. La versión recursiva de DFS es más común porque la pila la pone el lenguaje:

const depthFirst = (graph: Graph, start: string, visited = new Set<string>()): string[] => {
  if (visited.has(start)) return [];
  visited.add(start);

  return [start, ...(graph.get(start) ?? []).flatMap((n) => depthFirst(graph, n, visited))];
};

Cuál de los dos, y para qué

BFSDFS
Estructuracolapila, o la recursión
Orden de visitapor distancia crecientepor camino, hasta el fondo
Camino más corto sin pesossí, garantizadono
Memoria en el peor casoel nivel más anchoel camino más largo
Detectar ciclosse puedenatural, con tres estados
Orden topológicocon grados de entradanatural, al terminar cada nodo
Componentes conexassirve igualsirve igual
La fila de la memoria es la que decide en grafos grandes, y va en contra de la intuición: en un árbol muy ancho, BFS puede necesitar muchísima más memoria que DFS.
Más a fondo · nivel seniorCuando las aristas dejan de costar lo mismo

La garantía de BFS depende de que todas las aristas valgan 1: explora en distancias crecientes porque la cantidad de aristas es la distancia. En cuanto los pesos cambian, deja de valer y hay que reemplazar la cola.

Con pesos positivos, la cola se cambia por una cola de prioridad y BFS se convierte en Dijkstra: en vez de sacar el que entró antes, saca el más cercano. Con pesos que pueden ser negativos ni eso alcanza y hace falta Bellman-Ford. Y hay un caso intermedio lindo: si los pesos son sólo 0 y 1, alcanza con una cola de dos puntas —las aristas de peso 0 se agregan al frente y las de peso 1 al final— y se mantiene el costo lineal de BFS sin necesidad de un heap.

Los cuatro algoritmos son el mismo esqueleto con distinta política de extracción, y verlos así es mucho más útil que memorizarlos por separado.

Cierre

Cola o pila: esa es toda la diferencia. La cola da capas y caminos mínimos en aristas; la pila da profundidad, tiempos de entrada y salida, orden topológico y ciclos. Los dos recorren el grafo completo en O(V+E)O(V+E) marcando al encolar.

Autoevaluación

¿Lo entendiste?

¿Cuál es la única diferencia estructural entre BFS y DFS?
¿Cuándo hay que marcar un vértice como visitado en BFS?
BFS da el camino más corto. ¿Con qué condición?
¿Por qué detectar ciclos en un grafo dirigido no es lo mismo que en uno no dirigido?