Apuntes DAM
Volver al inicio

Recorrido en anchura (BFS)

AlgoritmosGrafosNivel intermedioTambién: BFS, breadth-first search, búsqueda en anchura, recorrido por niveles

Recorre un grafo por capas: primero los vecinos del origen, luego los vecinos de esos… Con una cola, encuentra el camino con menos aristas entre dos nodos.

Visualízalo paso a paso

Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.

Recorrido en anchura (BFS)

Escribe las aristas de un grafo y el nodo de salida: BFS lo recorre por capas, de lo más cercano a lo más lejano, con una cola.

Como «A-B, A-C»: hasta 9 nodos y 16 aristas
  • descubierto, en la cola

Paso 1

Se marca A como visto y entra en la cola. La cola es la clave de BFS: el primero que entra es el primero que sale, así que los nodos salen por orden de distancia a A (d = número de aristas).

1static List<String> bfs(Map<String, List<String>> g, String origen) {
2    List<String> orden = new ArrayList<>();
3    Set<String> vistos = new HashSet<>(List.of(origen));
4    Deque<String> cola = new ArrayDeque<>(List.of(origen));  // cola = [A], orden = —
5    while (!cola.isEmpty()) {
6        String u = cola.poll();
7        orden.add(u);
8        for (String v : g.get(u))
9            if (vistos.add(v)) cola.add(v);
10    }
11    return orden;
12}

Variables

cola
[A]
orden
—

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Un grafo es un conjunto de nodos unidos por aristas: ciudades y carreteras, personas que se siguen, casillas de un tablero. En el código casi siempre se guarda como una lista de adyacencia: para cada nodo, la lista de sus vecinos (Map<String, List<String>>). Si las aristas tienen dirección (A sigue a B, pero B no a A) el grafo es dirigido; si no, cada arista se apunta en los dos sentidos.

El recorrido en anchura (BFS, *breadth-first search*) explora desde un nodo de origen como una mancha de aceite: primero el origen, luego todos sus vecinos, luego los vecinos de los vecinos… Para conseguir ese orden usa una cola: se saca el primero, se miran sus vecinos y los que no se habían visto se meten al final. Como lo que entra antes sale antes, los nodos salen por orden de distancia.

Esa es su gran propiedad: la primera vez que BFS descubre un nodo, lo hace por un camino con el mínimo de aristas. Si al descubrir cada nodo se apunta desde cuál se llegó (su padre), al final basta con seguir los padres hacia atrás para reconstruir el camino más corto. Por eso BFS es el algoritmo del camino mínimo cuando todas las aristas «cuestan» lo mismo: saltos en una red social, movimientos en un laberinto o en un tablero.

El detalle que más errores provoca: un nodo se marca como visto al meterlo en la cola, no al sacarlo. Si se marca al sacarlo, el mismo nodo puede entrar varias veces (una por cada vecino que lo descubra) y el recorrido hace trabajo de más. Y sin el conjunto de vistos, en cuanto el grafo tiene un ciclo, BFS no termina nunca.

Cuándo usarlo

  • El camino más corto en número de pasos: saltos en una red, movimientos en un laberinto, en un tablero o en un puzle (cada estado del puzle es un nodo).
  • Recorrer por niveles: los amigos de tus amigos hasta el grado 3, un árbol planta a planta.
  • Saber qué se alcanza desde un nodo (y a qué distancia) o si dos nodos están conectados.
  • Propagar algo desde un punto: el relleno de una zona, la difusión en una red, un rastreador web.

Cuándo no

  • Si las aristas tienen pesos distintos (kilómetros, minutos, euros): el camino con menos aristas no es el más corto; hace falta Dijkstra.
  • Si el grafo es enorme y muy ancho y solo quieres saber si hay camino: la cola de BFS puede llegar a tener una capa entera; DFS gasta menos memoria.
  • Para detectar ciclos en grafos dirigidos u ordenar dependencias: DFS o la ordenación topológica encajan mejor.

Paso a paso

  1. Preparar. Marca el origen como visto, apunta su distancia (0) y mételo en la cola.
  2. Sacar el primero. Mientras la cola no esté vacía, saca el nodo que lleva más tiempo esperando.
  3. Descubrir vecinos. Por cada vecino que aún no se haya visto: márcalo, su distancia es la del actual + 1, apunta que su padre es el actual y mételo al final de la cola.
  4. Reconstruir el camino. Para ir del origen a un nodo, sigue sus padres hacia atrás hasta el origen y da la vuelta a la lista.

El código

BFS: distancias y camino más corto

El grafo del visualizador: BFS desde A apuntando la distancia y el padre de cada nodo; el camino de A a G se reconstruye hacia atrás.

Java
1import java.util.*;
2
3public class Main {
4    static final Map<String, List<String>> grafo = new LinkedHashMap<>();
5
6    static void arista(String a, String b) {               // no dirigida: se apunta en los dos sentidos
7        grafo.computeIfAbsent(a, k -> new ArrayList<>()).add(b);
8        grafo.computeIfAbsent(b, k -> new ArrayList<>()).add(a);
9    }
10
11    public static void main(String[] args) {
12        String[][] aristas = {{"A", "B"}, {"A", "C"}, {"B", "D"}, {"C", "D"}, {"C", "E"}, {"D", "F"}, {"E", "F"}, {"F", "G"}};
13        for (String[] e : aristas) arista(e[0], e[1]);
14
15        Map<String, Integer> dist = new HashMap<>();
16        Map<String, String> padre = new HashMap<>();
17        Deque<String> cola = new ArrayDeque<>();
18        List<String> orden = new ArrayList<>();
19        dist.put("A", 0);
20        cola.add("A");
21        while (!cola.isEmpty()) {
22            String u = cola.poll();
23            orden.add(u);
24            for (String v : grafo.get(u)) {
25                if (!dist.containsKey(v)) {                   // la primera vez que se ve: por el camino más corto
26                    dist.put(v, dist.get(u) + 1);
27                    padre.put(v, u);
28                    cola.add(v);
29                }
30            }
31        }
32        System.out.println("Orden de visita: " + String.join(" ", orden));
33        for (String n : orden) System.out.println("  dist(" + n + ") = " + dist.get(n));
34
35        LinkedList<String> camino = new LinkedList<>();
36        for (String n = "G"; n != null; n = padre.get(n)) camino.addFirst(n);    // de G hacia atrás
37        System.out.println("Camino más corto de A a G: " + String.join(" → ", camino));
38    }
39}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Orden de visita: A B C D E F G
  dist(A) = 0
  dist(B) = 1
  dist(C) = 1
  dist(D) = 2
  dist(E) = 2
  dist(F) = 3
  dist(G) = 4
Camino más corto de A a G: A → B → D → F → G

Laberinto: BFS en una cuadrícula

Cada casilla libre es un nodo y sus vecinas (arriba, abajo, izquierda, derecha) son sus aristas: no hace falta construir el grafo. El camino encontrado se dibuja con asteriscos.

Java
1import java.util.*;
2
3public class Main {
4    public static void main(String[] args) {
5        String[] plano = {
6            "S.#.......",
7            ".##.####.#",
8            "....#....#",
9            ".##...##.E",
10            "...#.#....",
11        };
12        int filas = plano.length, cols = plano[0].length();
13        char[][] m = new char[filas][];
14        int ini = 0, fin = 0;
15        for (int f = 0; f < filas; f++) {
16            m[f] = plano[f].toCharArray();
17            for (int c = 0; c < cols; c++) {
18                if (m[f][c] == 'S') ini = f * cols + c;       // cada casilla es un nodo: f * cols + c
19                if (m[f][c] == 'E') fin = f * cols + c;
20            }
21        }
22        int[] df = {-1, 1, 0, 0}, dc = {0, 0, -1, 1};      // arriba, abajo, izquierda, derecha
23        int[] previa = new int[filas * cols];
24        Arrays.fill(previa, -2);                              // -2: sin descubrir
25        previa[ini] = -1;
26        Deque<Integer> cola = new ArrayDeque<>(List.of(ini));
27        while (!cola.isEmpty()) {
28            int u = cola.poll();
29            if (u == fin) break;                              // la primera vez que se llega es por el camino más corto
30            for (int k = 0; k < 4; k++) {
31                int f = u / cols + df[k], c = u % cols + dc[k];
32                if (f < 0 || f >= filas || c < 0 || c >= cols || m[f][c] == '#' || previa[f * cols + c] != -2) continue;
33                previa[f * cols + c] = u;
34                cola.add(f * cols + c);
35            }
36        }
37        if (previa[fin] == -2) {
38            System.out.println("No hay salida");
39            return;
40        }
41        int pasos = 1;
42        for (int p = previa[fin]; p != ini; p = previa[p]) {   // de la salida hacia atrás
43            m[p / cols][p % cols] = '*';
44            pasos++;
45        }
46        System.out.println("Camino más corto: " + pasos + " pasos");
47        for (char[] fila : m) System.out.println(new String(fila));
48    }
49}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Camino más corto: 14 pasos
S.#.......
*##.####.#
****#****#
.##***##*E
...#.#....

Traza: BFS desde A en el grafo del visualizador

SaleVecinos nuevos (entran)Cola despuésDistancia
AB, CB Cd(A) = 0
BDC Dd(B) = 1
CED Ed(C) = 1
DFE Fd(D) = 2
E—Fd(E) = 2
FGGd(F) = 3
G—vacíad(G) = 4

Los nodos salen de la cola en orden de distancia: primero el 0, luego los dos a distancia 1, luego los de 2… D se descubre desde B y por eso, cuando C lo mira, ya está visto.

Complejidad

OperaciónLista de adyacenciaMatriz de adyacencia
Recorrer todo (BFS o DFS)O(V + E)O(V²)
Memoria del grafoO(V + E)O(V²)
¿Son vecinos u y v?O(grado de u)O(1)
Memoria extra de BFS (cola y vistos)O(V)O(V)

V es el número de nodos y E el de aristas. Los grafos reales suelen ser dispersos (pocas aristas por nodo): por eso la lista de adyacencia es lo habitual.

0102030405015101520tamaño de la entrada (n)operacionesO(n!)O(2ⁿ)O(n²)O(n log n)O(1)O(log n)O(n)
  • Mejor caso: O(n)
  • Caso medio: O(n)
  • Peor caso: O(n)

En realidad O(V + E): cada nodo entra y sale de la cola una vez y cada arista se mira desde sus dos extremos. Las curvas grises son las demás clases, para comparar.

En la práctica

  • Las redes sociales calculan «amigos de amigos» y los grados de separación con BFS limitados a 2 o 3 niveles.
  • Los juegos mueven personajes por mapas de casillas con BFS (o con A*, su versión con pesos y estimación).
  • Los rastreadores de los buscadores recorren la web por anchura desde unas páginas semilla.
  • El recolector de basura de Java marca los objetos vivos recorriendo el grafo de referencias desde las raíces.
  • En redes, la difusión (*broadcast*) y el descubrimiento de vecinos avanzan por capas.

Errores típicos

  • Marcar como visto al sacar de la cola en vez de al meter: el mismo nodo entra varias veces.
  • Olvidar el conjunto de vistos: con un ciclo, el bucle no termina.
  • Usar una pila en lugar de una cola (pop en vez de poll): eso ya es DFS y pierde la garantía del camino más corto.
  • Usar BFS con aristas de pesos distintos y creer que da el camino de menos kilómetros.
  • En JavaScript, shift() sobre un array es O(n): con colas enormes conviene un índice que avance en lugar de quitar el primero.

Ejercicios

Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.

1. Grados de separación

Cada línea con dos nombres es una amistad (en los dos sentidos). Las líneas que empiezan por ? preguntan cómo llegar de una persona a otra con el mínimo de saltos: escribe el número de saltos y el camino. El main ya lee la entrada y guarda los amigos de cada persona en orden alfabético: completa camino.

  • Amistad: ana luis. Consulta: ? ana pedro.
  • Respuesta: ana → pedro: 2 saltos (ana, sara, pedro); con uno solo, 1 salto; de alguien a sí mismo, 0 saltos (ana).
  • Si no hay camino: ana y eva no están conectados. Si un nombre no aparece en ninguna amistad: No conozco a zoe.
  • Si hay varios caminos igual de cortos, vale el que encuentra BFS recorriendo los amigos en orden alfabético (el código ya los guarda así). Cualquier otra línea: Línea no válida: «…».
☕JavaGrados de separaciónMedio

Ejemplo

Entrada (lo que se escribe por teclado)
ana luis
luis marta
marta pedro
ana sara
sara pedro
? ana pedro
? luis sara
? ana luis
Salida esperada
ana → pedro: 2 saltos (ana, sara, pedro)
luis → sara: 2 saltos (luis, ana, sara)
ana → luis: 1 salto (ana, luis)
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    /** Los amigos de cada persona, en orden alfabético. */
5    static final Map<String, TreeSet<String>> amigos = new TreeMap<>();
6
7    /** El camino más corto (en saltos) de origen a destino, con los dos incluidos; null si no están conectados. */
8    static List<String> camino(String origen, String destino) {
9        Map<String, String> previo = new HashMap<>();       // de quién se descubre cada persona
10        Deque<String> cola = new ArrayDeque<>();
11        previo.put(origen, null);
12        cola.add(origen);
13        while (!cola.isEmpty()) {
14            String u = cola.poll();
15            if (u.equals(destino)) {
16                LinkedList<String> c = new LinkedList<>();
17                for (String n = destino; n != null; n = previo.get(n)) c.addFirst(n);
18                return c;
19            }
20            for (String v : amigos.get(u)) {
21                if (!previo.containsKey(v)) {                 // solo la primera vez: así es el más corto
22                    previo.put(v, u);
23                    cola.add(v);
24                }
25            }
26        }
27        return null;
28    }
29
30    public static void main(String[] args) {
31        Scanner sc = new Scanner(System.in);
32        while (sc.hasNextLine()) {
33            String linea = sc.nextLine().trim();
34            if (linea.isEmpty()) continue;
35            String[] p = linea.split("\\s+");
36            if (p[0].equals("?")) {
37                if (p.length != 3) {
38                    System.out.println("Línea no válida: «" + linea + "»");
39                    continue;
40                }
41                String a = p[1], b = p[2];
42                if (!amigos.containsKey(a)) System.out.println("No conozco a " + a);
43                else if (!amigos.containsKey(b)) System.out.println("No conozco a " + b);
44                else {
45                    List<String> c = camino(a, b);
46                    if (c == null) System.out.println(a + " y " + b + " no están conectados");
47                    else System.out.println(a + " → " + b + ": " + (c.size() - 1) + (c.size() == 2 ? " salto (" : " saltos (") + String.join(", ", c) + ")");
48                }
49            } else if (p.length == 2 && !p[0].equals(p[1])) {
50                amigos.computeIfAbsent(p[0], k -> new TreeSet<>()).add(p[1]);
51                amigos.computeIfAbsent(p[1], k -> new TreeSet<>()).add(p[0]);
52            } else System.out.println("Línea no válida: «" + linea + "»");
53        }
54    }
55}

Es un BFS de manual: la cola garantiza que se descubre a cada persona por el camino con menos saltos, y el mapa de previos sirve a la vez para no repetir y para reconstruir el camino.

Se puede parar en cuanto sale el destino de la cola (o incluso al descubrirlo): todo lo que quedaba por explorar está más lejos.

2. El caballo de ajedrez

En un tablero de n × n hay algunas casillas prohibidas. Para cada consulta, calcula el mínimo de saltos de caballo para ir de una casilla a otra sin pisar ninguna prohibida. No hay que construir el grafo: cada casilla es un nodo y sus vecinas son las (hasta 8) casillas a las que salta el caballo. La lectura y las casillas ya están escritas: completa saltos.

  • Primera línea: n, entre 3 y 26 (si no, Tamaño no válido: «…» y se termina). Segunda línea: las casillas prohibidas separadas por espacios, o - si no hay.
  • Después, una consulta por línea: a1 h8. Las columnas son letras (a, b, c…) y las filas números desde 1.
  • Respuesta: a1 → h8: 6 saltos (1 salto, 0 saltos), o a1 → c3: imposible.
  • Errores: Casilla no válida: «z9», La casilla b3 está prohibida, Consulta no válida: «…».
☕JavaEl caballo de ajedrezDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
8
-
a1 h8
a1 b3
d4 d4
Salida esperada
a1 → h8: 6 saltos
a1 → b3: 1 salto
d4 → d4: 0 saltos
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
⏳
Test oculto #6
0/6 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    static int n;                                           // el tablero es de n × n
5    static boolean[][] prohibida;
6    static final int[] DF = {1, 2, 2, 1, -1, -2, -2, -1};    // los 8 saltos del caballo
7    static final int[] DC = {2, 1, -1, -2, -2, -1, 1, 2};
8
9    /** "c5" → {fila, columna} contando desde 0; null si no es una casilla del tablero. */
10    static int[] casilla(String s) {
11        if (!s.matches("[a-z][1-9][0-9]?")) return null;
12        int c = s.charAt(0) - 'a', f = Integer.parseInt(s.substring(1)) - 1;
13        return f < n && c < n ? new int[]{f, c} : null;
14    }
15
16    /** El mínimo de saltos de caballo de (f0, c0) a (f1, c1) sin pisar casillas prohibidas; -1 si no se puede. */
17    static int saltos(int f0, int c0, int f1, int c1) {
18        int[][] dist = new int[n][n];
19        for (int[] fila : dist) Arrays.fill(fila, -1);
20        Deque<int[]> cola = new ArrayDeque<>();
21        dist[f0][c0] = 0;
22        cola.add(new int[]{f0, c0});
23        while (!cola.isEmpty()) {
24            int[] u = cola.poll();
25            if (u[0] == f1 && u[1] == c1) return dist[f1][c1];
26            for (int k = 0; k < 8; k++) {
27                int f = u[0] + DF[k], c = u[1] + DC[k];
28                if (f < 0 || f >= n || c < 0 || c >= n || prohibida[f][c] || dist[f][c] != -1) continue;
29                dist[f][c] = dist[u[0]][u[1]] + 1;
30                cola.add(new int[]{f, c});
31            }
32        }
33        return -1;
34    }
35
36    public static void main(String[] args) {
37        Scanner sc = new Scanner(System.in);
38        String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
39        if (!primera.matches("\\d{1,2}") || Integer.parseInt(primera) < 3 || Integer.parseInt(primera) > 26) {
40            System.out.println("Tamaño no válido: «" + primera + "»");
41            return;
42        }
43        n = Integer.parseInt(primera);
44        prohibida = new boolean[n][n];
45        String segunda = sc.hasNextLine() ? sc.nextLine().trim() : "-";
46        if (!segunda.equals("-")) {
47            for (String s : segunda.split("\\s+")) {
48                int[] p = casilla(s);
49                if (p == null) System.out.println("Casilla no válida: «" + s + "»");
50                else prohibida[p[0]][p[1]] = true;
51            }
52        }
53        while (sc.hasNextLine()) {
54            String linea = sc.nextLine().trim();
55            if (linea.isEmpty()) continue;
56            String[] q = linea.split("\\s+");
57            if (q.length != 2) {
58                System.out.println("Consulta no válida: «" + linea + "»");
59                continue;
60            }
61            int[] a = casilla(q[0]), b = casilla(q[1]);
62            if (a == null || b == null) System.out.println("Casilla no válida: «" + (a == null ? q[0] : q[1]) + "»");
63            else if (prohibida[a[0]][a[1]] || prohibida[b[0]][b[1]]) System.out.println("La casilla " + (prohibida[a[0]][a[1]] ? q[0] : q[1]) + " está prohibida");
64            else {
65                int s = saltos(a[0], a[1], b[0], b[1]);
66                System.out.println(q[0] + " → " + q[1] + ": " + (s < 0 ? "imposible" : s + (s == 1 ? " salto" : " saltos")));
67            }
68        }
69    }
70}

Es un grafo implícito: los vecinos se calculan con los 8 desplazamientos del caballo en vez de leerlos de una lista. Muchísimos problemas de «mínimo número de movimientos» (puzles, tableros, laberintos) se resuelven así.

Cada consulta hace un BFS de como mucho n² casillas con 8 vecinas cada una: con n = 26 son menos de 6.000 operaciones.

Test

Test: Recorrido en anchura (BFS)

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿Qué estructura usa BFS para decidir qué nodo procesar después?

  2. 2.En un grafo sin pesos, ¿qué garantiza BFS?

  3. 3.¿Cuándo hay que marcar un nodo como visto en BFS?

  4. 4.¿Qué coste tiene BFS con una lista de adyacencia?

  5. 5.Si cada arista tiene una distancia en km distinta, ¿sirve BFS para el camino más corto?

Relacionado