Recorrido en profundidad (DFS)
Recorre un grafo yendo lo más lejos posible por cada camino antes de volver atrás. Con recursividad o una pila, sirve para contar componentes, detectar ciclos y explorar laberintos.
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 profundidad (DFS)
Escribe las aristas de un grafo y el nodo de salida: DFS baja por un camino hasta el fondo y solo entonces vuelve atrás, con la pila de llamadas.
- la llamada en curso
Paso 1
Primera llamada, dfs(A): se marca A como visitado (es el 1º) y se apunta en el orden.
1static void dfs(Map<String, List<String>> g, String u, Set<String> vistos, List<String> orden) {
2 vistos.add(u); // u = A, orden = A
3 orden.add(u);
4 for (String v : g.get(u))
5 if (!vistos.contains(v)) dfs(g, v, vistos, orden);
6}Variables
- u
- A
- orden
- A
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
El recorrido en profundidad (DFS, *depth-first search*) hace lo contrario que BFS: desde el nodo actual se va a un vecino sin visitar, y desde ese a otro, y así hasta que no queda por dónde seguir. Solo entonces vuelve atrás al último nodo que tenía vecinos pendientes y sigue por ahí. Es lo que hace alguien que explora un laberinto sin soltar la mano de la pared.
Lo más natural es escribirlo con recursividad: dfs(u) marca u y llama a dfs(v) por cada vecino v sin visitar. La pila de llamadas del lenguaje guarda el camino por el que se ha bajado y volver de una llamada es la vuelta atrás. También se puede escribir con una pila explícita, lo que evita el StackOverflowError en grafos con caminos de decenas de miles de nodos.
DFS no encuentra caminos más cortos, pero su forma de recorrer da mucha información. Cada vez que se arranca un DFS desde un nodo aún sin visitar se descubre una componente conexa entera. Y en un grafo dirigido, si se llega a un nodo que todavía está en la pila de llamadas (empezado pero sin terminar), hay un ciclo: se ha vuelto a un antepasado.
Para esto último se usan tres colores: blanco (sin visitar), gris (en curso) y negro (terminado). Una arista hacia un gris es un ciclo; hacia un negro, no. Y el orden en que terminan los nodos es justo el orden en que hay que construir unas dependencias: lo que no usa nada termina primero. Así detectan Maven, Gradle o npm las dependencias circulares.
Cuándo usarlo
- Contar o etiquetar componentes conexas: islas en un mapa, grupos de amigos, zonas de una imagen (el «bote de pintura»).
- Detectar ciclos: dependencias circulares entre módulos, interbloqueos entre procesos.
- Ordenar dependencias (orden topológico) y otros análisis de grafos dirigidos.
- Explorar todas las posibilidades: es el esqueleto del backtracking (laberintos, sudokus, combinaciones).
Cuándo no
- Para el camino más corto: el primer camino que encuentra DFS puede ser larguísimo; usa BFS o Dijkstra.
- Recursivo sobre caminos muy profundos (una cadena de un millón de nodos): desborda la pila; usa la versión con pila explícita.
- Si interesa lo cercano primero (amigos de amigos, radio de alcance): BFS.
Paso a paso
- Visitar. Marca el nodo actual como visitado y haz con él lo que toque: apuntarlo, contarlo, compararlo.
- Bajar. Por cada vecino sin visitar, llama a dfs con él: se baja un nivel antes de mirar el resto de vecinos.
- Volver atrás. Cuando no quedan vecinos sin visitar, la llamada termina y se sigue en el nodo anterior por donde se había quedado.
- Repetir para lo que falte. Para recorrer todo el grafo (no solo lo alcanzable), lanza un DFS desde cada nodo que siga sin visitar: cada lanzamiento es una componente.
El código
DFS recursivo y con pila explícita
Los dos dan el mismo orden: con la pila hay que meter los vecinos al revés, para que el primero quede en la cima, y marcar cada nodo al sacarlo.
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) {
7 grafo.computeIfAbsent(a, k -> new ArrayList<>()).add(b);
8 grafo.computeIfAbsent(b, k -> new ArrayList<>()).add(a);
9 }
10
11 static void dfs(String u, Set<String> vistos, List<String> orden) {
12 vistos.add(u);
13 orden.add(u);
14 for (String v : grafo.get(u))
15 if (!vistos.contains(v)) dfs(v, vistos, orden); // baja ya; el resto de vecinos, a la vuelta
16 }
17
18 static List<String> dfsConPila(String origen) {
19 Set<String> vistos = new HashSet<>();
20 List<String> orden = new ArrayList<>();
21 Deque<String> pila = new ArrayDeque<>();
22 pila.push(origen);
23 while (!pila.isEmpty()) {
24 String u = pila.pop();
25 if (!vistos.add(u)) continue; // ya se visitó por otro camino
26 orden.add(u);
27 List<String> vecinos = grafo.get(u);
28 for (int i = vecinos.size() - 1; i >= 0; i--) // al revés: el primero queda en la cima
29 if (!vistos.contains(vecinos.get(i))) pila.push(vecinos.get(i));
30 }
31 return orden;
32 }
33
34 public static void main(String[] args) {
35 String[][] aristas = {{"A", "B"}, {"A", "C"}, {"B", "D"}, {"C", "D"}, {"C", "E"}, {"D", "F"}, {"E", "F"}, {"F", "G"}};
36 for (String[] e : aristas) arista(e[0], e[1]);
37 List<String> orden = new ArrayList<>();
38 dfs("A", new HashSet<>(), orden);
39 System.out.println("Recursivo: " + String.join(" ", orden));
40 System.out.println("Con pila: " + String.join(" ", dfsConPila("A")));
41 }
42}grafo = {}
def arista(a, b):
grafo.setdefault(a, []).append(b)
grafo.setdefault(b, []).append(a)
def dfs(u, vistos, orden):
vistos.add(u)
orden.append(u)
for v in grafo[u]:
if v not in vistos:
dfs(v, vistos, orden) # baja ya; el resto de vecinos, a la vuelta
def dfs_con_pila(origen):
vistos, orden, pila = set(), [], [origen]
while pila:
u = pila.pop()
if u in vistos:
continue # ya se visitó por otro camino
vistos.add(u)
orden.append(u)
for v in reversed(grafo[u]): # al revés: el primero queda en la cima
if v not in vistos:
pila.append(v)
return orden
for a, b in [("A", "B"), ("A", "C"), ("B", "D"), ("C", "D"), ("C", "E"), ("D", "F"), ("E", "F"), ("F", "G")]:
arista(a, b)
orden = []
dfs("A", set(), orden)
print("Recursivo: " + " ".join(orden))
print("Con pila: " + " ".join(dfs_con_pila("A")))const grafo = new Map();
function arista(a, b) {
if (!grafo.has(a)) grafo.set(a, []);
if (!grafo.has(b)) grafo.set(b, []);
grafo.get(a).push(b);
grafo.get(b).push(a);
}
function dfs(u, vistos, orden) {
vistos.add(u);
orden.push(u);
for (const v of grafo.get(u))
if (!vistos.has(v)) dfs(v, vistos, orden); // baja ya; el resto de vecinos, a la vuelta
}
function dfsConPila(origen) {
const vistos = new Set(), orden = [], pila = [origen];
while (pila.length > 0) {
const u = pila.pop();
if (vistos.has(u)) continue; // ya se visitó por otro camino
vistos.add(u);
orden.push(u);
const vecinos = grafo.get(u);
for (let i = vecinos.length - 1; i >= 0; i--) // al revés: el primero queda en la cima
if (!vistos.has(vecinos[i])) pila.push(vecinos[i]);
}
return orden;
}
for (const [a, b] of [["A", "B"], ["A", "C"], ["B", "D"], ["C", "D"], ["C", "E"], ["D", "F"], ["E", "F"], ["F", "G"]]) arista(a, b);
const orden = [];
dfs("A", new Set(), orden);
console.log("Recursivo: " + orden.join(" "));
console.log("Con pila: " + dfsConPila("A").join(" "));using System;
using System.Collections.Generic;
class Program {
static readonly Dictionary<string, List<string>> grafo = new();
static void Arista(string a, string b) {
if (!grafo.ContainsKey(a)) grafo[a] = new List<string>();
if (!grafo.ContainsKey(b)) grafo[b] = new List<string>();
grafo[a].Add(b);
grafo[b].Add(a);
}
static void Dfs(string u, HashSet<string> vistos, List<string> orden) {
vistos.Add(u);
orden.Add(u);
foreach (string v in grafo[u])
if (!vistos.Contains(v)) Dfs(v, vistos, orden); // baja ya; el resto de vecinos, a la vuelta
}
static List<string> DfsConPila(string origen) {
var vistos = new HashSet<string>();
var orden = new List<string>();
var pila = new Stack<string>();
pila.Push(origen);
while (pila.Count > 0) {
string u = pila.Pop();
if (!vistos.Add(u)) continue; // ya se visitó por otro camino
orden.Add(u);
var vecinos = grafo[u];
for (int i = vecinos.Count - 1; i >= 0; i--) // al revés: el primero queda en la cima
if (!vistos.Contains(vecinos[i])) pila.Push(vecinos[i]);
}
return orden;
}
static void Main() {
foreach (var (a, b) in new[] { ("A", "B"), ("A", "C"), ("B", "D"), ("C", "D"), ("C", "E"), ("D", "F"), ("E", "F"), ("F", "G") }) Arista(a, b);
var orden = new List<string>();
Dfs("A", new HashSet<string>(), orden);
Console.WriteLine("Recursivo: " + string.Join(" ", orden));
Console.WriteLine("Con pila: " + string.Join(" ", DfsConPila("A")));
}
}<?php
$grafo = [];
foreach ([["A", "B"], ["A", "C"], ["B", "D"], ["C", "D"], ["C", "E"], ["D", "F"], ["E", "F"], ["F", "G"]] as [$a, $b]) {
$grafo[$a][] = $b;
$grafo[$b][] = $a;
}
function dfs(array $g, string $u, array &$vistos, array &$orden): void {
$vistos[$u] = true;
$orden[] = $u;
foreach ($g[$u] as $v)
if (!isset($vistos[$v])) dfs($g, $v, $vistos, $orden); // baja ya; el resto de vecinos, a la vuelta
}
function dfsConPila(array $g, string $origen): array {
$vistos = [];
$orden = [];
$pila = [$origen];
while ($pila) {
$u = array_pop($pila);
if (isset($vistos[$u])) continue; // ya se visitó por otro camino
$vistos[$u] = true;
$orden[] = $u;
foreach (array_reverse($g[$u]) as $v) // al revés: el primero queda en la cima
if (!isset($vistos[$v])) $pila[] = $v;
}
return $orden;
}
$vistos = [];
$orden = [];
dfs($grafo, "A", $vistos, $orden);
echo "Recursivo: " . implode(" ", $orden) . "\n";
echo "Con pila: " . implode(" ", dfsConPila($grafo, "A")) . "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Recursivo: A B D C E F G Con pila: A B D C E F G
Componentes conexas y ciclos con tres colores
Un DFS por cada nodo sin visitar cuenta las componentes. En un grafo dirigido, una flecha hacia un nodo «en curso» delata un ciclo.
1import java.util.*;
2
3public class Main {
4 static void marcar(Map<String, List<String>> g, String u, Set<String> vistos, List<String> grupo) {
5 vistos.add(u);
6 grupo.add(u);
7 for (String v : g.get(u))
8 if (!vistos.contains(v)) marcar(g, v, vistos, grupo);
9 }
10
11 /** color: 0 sin visitar, 1 en curso (en la pila de llamadas), 2 terminado. */
12 static boolean hayCiclo(Map<String, List<String>> g, String u, Map<String, Integer> color) {
13 color.put(u, 1);
14 for (String v : g.get(u)) {
15 int c = color.getOrDefault(v, 0);
16 if (c == 1) return true; // flecha hacia un nodo en curso: ciclo
17 if (c == 0 && hayCiclo(g, v, color)) return true;
18 }
19 color.put(u, 2);
20 return false;
21 }
22
23 static boolean tieneCiclo(Map<String, List<String>> g) {
24 Map<String, Integer> color = new HashMap<>();
25 for (String u : g.keySet())
26 if (color.getOrDefault(u, 0) == 0 && hayCiclo(g, u, color)) return true;
27 return false;
28 }
29
30 static Map<String, List<String>> grafo(String[] nodos, String[][] aristas, boolean dirigido) {
31 Map<String, List<String>> g = new LinkedHashMap<>();
32 for (String n : nodos) g.put(n, new ArrayList<>());
33 for (String[] e : aristas) {
34 g.get(e[0]).add(e[1]);
35 if (!dirigido) g.get(e[1]).add(e[0]);
36 }
37 return g;
38 }
39
40 public static void main(String[] args) {
41 Map<String, List<String>> red = grafo(new String[]{"A", "B", "C", "D", "E", "F", "G", "H"},
42 new String[][]{{"A", "B"}, {"B", "C"}, {"A", "C"}, {"D", "E"}, {"F", "G"}}, false);
43 Set<String> vistos = new HashSet<>();
44 int n = 0;
45 for (String u : red.keySet()) {
46 if (vistos.contains(u)) continue;
47 List<String> grupo = new ArrayList<>();
48 marcar(red, u, vistos, grupo); // un DFS por cada nodo que siga sin visitar
49 System.out.println("Componente " + (++n) + ": " + String.join(" ", grupo));
50 }
51
52 String[] tareas = {"compilar", "enlazar", "probar", "desplegar"};
53 String[][] cadena = {{"compilar", "enlazar"}, {"enlazar", "probar"}, {"probar", "desplegar"}};
54 String[][] vuelta = {{"compilar", "enlazar"}, {"enlazar", "probar"}, {"probar", "desplegar"}, {"desplegar", "enlazar"}};
55 System.out.println("Tareas en cadena: " + (tieneCiclo(grafo(tareas, cadena, true)) ? "hay un ciclo" : "sin ciclos"));
56 System.out.println("Con desplegar → enlazar: " + (tieneCiclo(grafo(tareas, vuelta, true)) ? "hay un ciclo" : "sin ciclos"));
57 }
58}def marcar(g, u, vistos, grupo):
vistos.add(u)
grupo.append(u)
for v in g[u]:
if v not in vistos:
marcar(g, v, vistos, grupo)
def hay_ciclo(g, u, color):
"""color: 0 sin visitar, 1 en curso (en la pila de llamadas), 2 terminado."""
color[u] = 1
for v in g[u]:
c = color.get(v, 0)
if c == 1:
return True # flecha hacia un nodo en curso: ciclo
if c == 0 and hay_ciclo(g, v, color):
return True
color[u] = 2
return False
def tiene_ciclo(g):
color = {}
for u in g:
if color.get(u, 0) == 0 and hay_ciclo(g, u, color):
return True
return False
def grafo(nodos, aristas, dirigido):
g = {n: [] for n in nodos}
for a, b in aristas:
g[a].append(b)
if not dirigido:
g[b].append(a)
return g
red = grafo("ABCDEFGH", [("A", "B"), ("B", "C"), ("A", "C"), ("D", "E"), ("F", "G")], False)
vistos = set()
n = 0
for u in red:
if u in vistos:
continue
grupo = []
marcar(red, u, vistos, grupo) # un DFS por cada nodo que siga sin visitar
n += 1
print(f"Componente {n}: " + " ".join(grupo))
tareas = ["compilar", "enlazar", "probar", "desplegar"]
cadena = [("compilar", "enlazar"), ("enlazar", "probar"), ("probar", "desplegar")]
vuelta = cadena + [("desplegar", "enlazar")]
print("Tareas en cadena: " + ("hay un ciclo" if tiene_ciclo(grafo(tareas, cadena, True)) else "sin ciclos"))
print("Con desplegar → enlazar: " + ("hay un ciclo" if tiene_ciclo(grafo(tareas, vuelta, True)) else "sin ciclos"))function marcar(g, u, vistos, grupo) {
vistos.add(u);
grupo.push(u);
for (const v of g.get(u)) if (!vistos.has(v)) marcar(g, v, vistos, grupo);
}
/** color: 0 sin visitar, 1 en curso (en la pila de llamadas), 2 terminado. */
function hayCiclo(g, u, color) {
color.set(u, 1);
for (const v of g.get(u)) {
const c = color.get(v) ?? 0;
if (c === 1) return true; // flecha hacia un nodo en curso: ciclo
if (c === 0 && hayCiclo(g, v, color)) return true;
}
color.set(u, 2);
return false;
}
function tieneCiclo(g) {
const color = new Map();
for (const u of g.keys()) if ((color.get(u) ?? 0) === 0 && hayCiclo(g, u, color)) return true;
return false;
}
function grafo(nodos, aristas, dirigido) {
const g = new Map(nodos.map((n) => [n, []]));
for (const [a, b] of aristas) {
g.get(a).push(b);
if (!dirigido) g.get(b).push(a);
}
return g;
}
const red = grafo([..."ABCDEFGH"], [["A", "B"], ["B", "C"], ["A", "C"], ["D", "E"], ["F", "G"]], false);
const vistos = new Set();
let n = 0;
for (const u of red.keys()) {
if (vistos.has(u)) continue;
const grupo = [];
marcar(red, u, vistos, grupo); // un DFS por cada nodo que siga sin visitar
console.log(`Componente ${++n}: ${grupo.join(" ")}`);
}
const tareas = ["compilar", "enlazar", "probar", "desplegar"];
const cadena = [["compilar", "enlazar"], ["enlazar", "probar"], ["probar", "desplegar"]];
const vuelta = [...cadena, ["desplegar", "enlazar"]];
console.log("Tareas en cadena: " + (tieneCiclo(grafo(tareas, cadena, true)) ? "hay un ciclo" : "sin ciclos"));
console.log("Con desplegar → enlazar: " + (tieneCiclo(grafo(tareas, vuelta, true)) ? "hay un ciclo" : "sin ciclos"));using System;
using System.Collections.Generic;
using System.Linq;
class Program {
static void Marcar(Dictionary<string, List<string>> g, string u, HashSet<string> vistos, List<string> grupo) {
vistos.Add(u);
grupo.Add(u);
foreach (string v in g[u])
if (!vistos.Contains(v)) Marcar(g, v, vistos, grupo);
}
// color: 0 sin visitar, 1 en curso (en la pila de llamadas), 2 terminado
static bool HayCiclo(Dictionary<string, List<string>> g, string u, Dictionary<string, int> color) {
color[u] = 1;
foreach (string v in g[u]) {
int c = color.GetValueOrDefault(v);
if (c == 1) return true; // flecha hacia un nodo en curso: ciclo
if (c == 0 && HayCiclo(g, v, color)) return true;
}
color[u] = 2;
return false;
}
static bool TieneCiclo(Dictionary<string, List<string>> g) {
var color = new Dictionary<string, int>();
foreach (string u in g.Keys)
if (color.GetValueOrDefault(u) == 0 && HayCiclo(g, u, color)) return true;
return false;
}
static Dictionary<string, List<string>> Grafo(string[] nodos, (string, string)[] aristas, bool dirigido) {
var g = new Dictionary<string, List<string>>();
foreach (string n in nodos) g[n] = new List<string>();
foreach (var (a, b) in aristas) {
g[a].Add(b);
if (!dirigido) g[b].Add(a);
}
return g;
}
static void Main() {
var red = Grafo(new[] { "A", "B", "C", "D", "E", "F", "G", "H" },
new[] { ("A", "B"), ("B", "C"), ("A", "C"), ("D", "E"), ("F", "G") }, false);
var vistos = new HashSet<string>();
int n = 0;
foreach (string u in red.Keys) {
if (vistos.Contains(u)) continue;
var grupo = new List<string>();
Marcar(red, u, vistos, grupo); // un DFS por cada nodo que siga sin visitar
Console.WriteLine(quot;Componente {++n}: {string.Join(" ", grupo)}");
}
string[] tareas = { "compilar", "enlazar", "probar", "desplegar" };
var cadena = new[] { ("compilar", "enlazar"), ("enlazar", "probar"), ("probar", "desplegar") };
var vuelta = cadena.Append(("desplegar", "enlazar")).ToArray();
Console.WriteLine("Tareas en cadena: " + (TieneCiclo(Grafo(tareas, cadena, true)) ? "hay un ciclo" : "sin ciclos"));
Console.WriteLine("Con desplegar → enlazar: " + (TieneCiclo(Grafo(tareas, vuelta, true)) ? "hay un ciclo" : "sin ciclos"));
}
}<?php
function marcar(array $g, string $u, array &$vistos, array &$grupo): void {
$vistos[$u] = true;
$grupo[] = $u;
foreach ($g[$u] as $v)
if (!isset($vistos[$v])) marcar($g, $v, $vistos, $grupo);
}
/** color: 0 sin visitar, 1 en curso (en la pila de llamadas), 2 terminado. */
function hayCiclo(array $g, string $u, array &$color): bool {
$color[$u] = 1;
foreach ($g[$u] as $v) {
$c = $color[$v] ?? 0;
if ($c === 1) return true; // flecha hacia un nodo en curso: ciclo
if ($c === 0 && hayCiclo($g, $v, $color)) return true;
}
$color[$u] = 2;
return false;
}
function tieneCiclo(array $g): bool {
$color = [];
foreach (array_keys($g) as $u)
if (($color[$u] ?? 0) === 0 && hayCiclo($g, $u, $color)) return true;
return false;
}
function grafo(array $nodos, array $aristas, bool $dirigido): array {
$g = array_fill_keys($nodos, []);
foreach ($aristas as [$a, $b]) {
$g[$a][] = $b;
if (!$dirigido) $g[$b][] = $a;
}
return $g;
}
$red = grafo(str_split("ABCDEFGH"), [["A", "B"], ["B", "C"], ["A", "C"], ["D", "E"], ["F", "G"]], false);
$vistos = [];
$n = 0;
foreach (array_keys($red) as $u) {
if (isset($vistos[$u])) continue;
$grupo = [];
marcar($red, $u, $vistos, $grupo); // un DFS por cada nodo que siga sin visitar
$n++;
echo "Componente $n: " . implode(" ", $grupo) . "\n";
}
$tareas = ["compilar", "enlazar", "probar", "desplegar"];
$cadena = [["compilar", "enlazar"], ["enlazar", "probar"], ["probar", "desplegar"]];
$vuelta = [...$cadena, ["desplegar", "enlazar"]];
echo "Tareas en cadena: " . (tieneCiclo(grafo($tareas, $cadena, true)) ? "hay un ciclo" : "sin ciclos") . "\n";
echo "Con desplegar → enlazar: " . (tieneCiclo(grafo($tareas, $vuelta, true)) ? "hay un ciclo" : "sin ciclos") . "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Componente 1: A B C Componente 2: D E Componente 3: F G Componente 4: H Tareas en cadena: sin ciclos Con desplegar → enlazar: hay un ciclo
Traza: DFS desde A en el grafo del visualizador
| Pila de llamadas | Qué pasa |
|---|---|
| A | entra en A (el 1º visitado) |
| A › B | entra en B (el 2º visitado) |
| A › B › D | entra en D (el 3º visitado) |
| A › B › D › C | entra en C (el 4º visitado) |
| A › B › D › C › E | entra en E (el 5º visitado) |
| A › B › D › C › E › F | entra en F (el 6º visitado) |
| A › B › D › C › E › F › G | entra en G (el 7º visitado) |
| A › B › D › C › E › F › G | G no tiene más vecinos sin visitar: vuelve a F |
| A › B › D › C › E › F | F no tiene más vecinos sin visitar: vuelve a E |
| A › B › D › C › E | E no tiene más vecinos sin visitar: vuelve a C |
| A › B › D › C | C no tiene más vecinos sin visitar: vuelve a D |
| A › B › D | D no tiene más vecinos sin visitar: vuelve a B |
| A › B | B no tiene más vecinos sin visitar: vuelve a A |
| A | A no tiene más vecinos sin visitar: fin |
La pila de llamadas es el camino desde A hasta el nodo actual: crece al bajar y se vacía al volver atrás. D se visita antes que C porque se llega a él bajando por B.
Complejidad
| Versión | Tiempo | Memoria extra |
|---|---|---|
| DFS recursivo | O(V + E) | O(V): la profundidad de la pila de llamadas |
| DFS con pila explícita | O(V + E) | O(E) en el peor caso: un nodo puede estar apilado varias veces |
| Componentes conexas | O(V + E) | O(V) |
| Detección de ciclos (tres colores) | O(V + E) | O(V) |
Como BFS: cada nodo se visita una vez y cada arista se mira una vez por extremo. Lo que cambia es el orden, no el coste.
- Mejor caso: O(n)
- Caso medio: O(n)
- Peor caso: O(n)
O(V + E), como BFS: cada nodo se visita una vez y cada arista se mira desde sus dos extremos. Lo que cambia es el orden, no el coste. Las curvas grises son las demás clases, para comparar.
En la práctica
- Las herramientas de construcción (Maven, Gradle, npm, Make) recorren las dependencias en profundidad y avisan de las circulares.
- El «bote de pintura» de los editores de imagen rellena la zona conectada del mismo color (*flood fill*).
- Los sistemas operativos y las bases de datos buscan ciclos en el grafo de esperas para detectar interbloqueos.
- Los generadores de laberintos clásicos son un DFS que va tirando paredes al azar.
- Los compiladores recorren el árbol sintáctico en profundidad para analizarlo y generar código.
Errores típicos
- Olvidar marcar el nodo antes de bajar a los vecinos: en un grafo con ciclos, recursividad infinita.
- Usar un solo «visitado» para detectar ciclos en un grafo dirigido: hay que distinguir «en curso» de «terminado» (tres colores).
- En un grafo no dirigido, tomar la arista de vuelta al padre como un ciclo: hay que ignorar al nodo del que se viene.
- Recursividad demasiado profunda: con decenas de miles de nodos en fila,
StackOverflowError. - Lanzar DFS solo desde un nodo y creer que se ha recorrido todo el grafo: si no es conexo, faltan componentes.
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. Islas en un mapa
El mapa es una cuadrícula de # (tierra) y . (agua). Una isla es un grupo de casillas de tierra unidas por los lados (en diagonal no cuentan). Escribe cuántas islas hay y sus tamaños de mayor a menor. El main lee el mapa y llama a inundar en cada casilla de tierra sin contar: complétala con un DFS recursivo.
- Entrada: el mapa, una fila por línea, todas del mismo ancho.
- Salida:
5 islas; tamaños de mayor a menor: 3, 2, 2, 2, 1(con una,1 isla; …), oNo hay islas. - Si una fila tiene otro ancho u otros caracteres:
Mapa no válido en la fila 2: «…».
Ejemplo
##..# #...# ..#.. ..... ##.##
5 islas; tamaños de mayor a menor: 3, 2, 2, 2, 1
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 static char[][] mapa; // '#' tierra, '.' agua
5 static int filas, cols;
6
7 /** Si (f, c) es tierra sin contar, la marca y sigue por sus cuatro vecinas; devuelve cuántas casillas ha marcado. */
8 static int inundar(int f, int c) {
9 if (f < 0 || f >= filas || c < 0 || c >= cols || mapa[f][c] != '#') return 0;
10 mapa[f][c] = 'x'; // contada: no se vuelve a entrar
11 return 1 + inundar(f - 1, c) + inundar(f + 1, c) + inundar(f, c - 1) + inundar(f, c + 1);
12 }
13
14 public static void main(String[] args) {
15 Scanner sc = new Scanner(System.in);
16 List<String> lineas = new ArrayList<>();
17 while (sc.hasNextLine()) {
18 String l = sc.nextLine().trim();
19 if (!l.isEmpty()) lineas.add(l);
20 }
21 if (lineas.isEmpty()) {
22 System.out.println("Mapa vacío");
23 return;
24 }
25 filas = lineas.size();
26 cols = lineas.get(0).length();
27 mapa = new char[filas][];
28 for (int f = 0; f < filas; f++) {
29 String l = lineas.get(f);
30 if (l.length() != cols || !l.matches("[#.]+")) {
31 System.out.println("Mapa no válido en la fila " + (f + 1) + ": «" + l + "»");
32 return;
33 }
34 mapa[f] = l.toCharArray();
35 }
36 List<Integer> islas = new ArrayList<>();
37 for (int f = 0; f < filas; f++)
38 for (int c = 0; c < cols; c++)
39 if (mapa[f][c] == '#') islas.add(inundar(f, c));
40 if (islas.isEmpty()) {
41 System.out.println("No hay islas");
42 return;
43 }
44 islas.sort(Comparator.reverseOrder());
45 StringJoiner sj = new StringJoiner(", ");
46 for (int t : islas) sj.add(String.valueOf(t));
47 System.out.println(islas.size() + (islas.size() == 1 ? " isla" : " islas") + "; tamaños de mayor a menor: " + sj);
48 }
49}Cada llamada de main a inundar sobre una casilla # es el arranque de un DFS nuevo: descubre una componente entera (una isla) y la deja marcada, así que el número de arranques es el número de islas.
Marcar en el propio mapa ahorra un boolean[][] de visitados. Con mapas enormes, la recursividad puede desbordar la pila: entonces se usa una pila explícita o BFS.
2. Dependencias circulares
Cada línea dice qué módulos usa un módulo: app: core log. Si las dependencias tienen un ciclo, escribe el primero que encuentre el DFS; si no, el orden de compilación (cada módulo después de todos los que usa). El main lee la entrada (los módulos que solo aparecen a la derecha también existen, sin dependencias) y recorre los módulos en orden alfabético: completa visitar con los tres colores.
- Entrada:
módulo: dependencias separadas por espacios(puede no tener ninguna:log:). Línea sin:o sin nombre:Línea no válida: «…». - Sin ciclos:
Sin ciclos. Orden de compilación: log, core, app, ui: el orden en que TERMINAN los módulos, recorriendo los módulos y las dependencias de cada uno en orden alfabético. - Con ciclo:
Ciclo: b → d → e → b, desde el módulo en el que se cierra (el que ya estaba en el camino) hasta volver a él.
Ejemplo
app: core log core: log log: ui: app
Sin ciclos. Orden de compilación: log, core, app, ui
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 static final int BLANCO = 0, GRIS = 1, NEGRO = 2; // sin visitar, en el camino actual, terminado
5 static final Map<String, TreeSet<String>> usa = new TreeMap<>();
6 static final Map<String, Integer> color = new HashMap<>();
7 static final List<String> camino = new ArrayList<>(); // los módulos del camino actual, en orden
8 static final List<String> orden = new ArrayList<>(); // los terminados: primero lo que no usa nada
9 static final List<String> ciclo = new ArrayList<>();
10
11 /** DFS desde u. Si encuentra un ciclo, lo deja en «ciclo» (empezando y acabando en el mismo módulo) y
12 devuelve true. Si no, al terminar u lo añade a «orden» y devuelve false. */
13 static boolean visitar(String u) {
14 color.put(u, GRIS);
15 camino.add(u);
16 for (String v : usa.get(u)) {
17 int c = color.get(v);
18 if (c == GRIS) { // v está en el camino actual: se ha cerrado un ciclo
19 ciclo.addAll(camino.subList(camino.indexOf(v), camino.size()));
20 ciclo.add(v);
21 return true;
22 }
23 if (c == BLANCO && visitar(v)) return true;
24 }
25 camino.remove(camino.size() - 1);
26 color.put(u, NEGRO);
27 orden.add(u); // todo lo que usa ya está en la lista
28 return false;
29 }
30
31 public static void main(String[] args) {
32 Scanner sc = new Scanner(System.in);
33 while (sc.hasNextLine()) {
34 String linea = sc.nextLine().trim();
35 if (linea.isEmpty()) continue;
36 int dos = linea.indexOf(':');
37 String nombre = dos < 0 ? "" : linea.substring(0, dos).trim();
38 if (!nombre.matches("[\\w.-]+")) {
39 System.out.println("Línea no válida: «" + linea + "»");
40 continue;
41 }
42 usa.computeIfAbsent(nombre, k -> new TreeSet<>());
43 String resto = linea.substring(dos + 1).trim();
44 if (!resto.isEmpty())
45 for (String m : resto.split("\\s+")) {
46 usa.get(nombre).add(m);
47 usa.computeIfAbsent(m, k -> new TreeSet<>());
48 }
49 }
50 for (String m : usa.keySet()) color.put(m, BLANCO);
51 for (String m : usa.keySet()) {
52 if (color.get(m) == BLANCO && visitar(m)) {
53 System.out.println("Ciclo: " + String.join(" → ", ciclo));
54 return;
55 }
56 }
57 System.out.println("Sin ciclos. Orden de compilación: " + String.join(", ", orden));
58 }
59}Los tres colores son la clave: un módulo gris está en el camino actual, así que llegar a él desde abajo significa haber dado la vuelta. Llegar a un negro no: solo es una dependencia compartida.
El orden de terminación es una ordenación topológica al revés de las flechas «usa»: cada módulo termina después que todo lo que usa. Es lo que hacen Maven o Gradle para decidir qué compilar primero.
Test
Test: Recorrido en profundidad (DFS)
0/5 respondidas · 0 aciertosElige 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.¿Qué estructura de datos usa DFS, de forma explícita o con la recursividad?
2.¿Garantiza DFS encontrar el camino con menos aristas?
3.En un grafo dirigido, ¿qué indica una arista hacia un nodo que está «en curso» (gris)?
4.¿Cómo se cuentan las componentes conexas de un grafo no dirigido?
5.¿Qué riesgo tiene el DFS recursivo en un grafo que es una cadena de 100.000 nodos?