Un grafo es una relación binaria con estructura. La teoría de grafos estudia propiedades de redes: conectividad, caminos, flujos, coloración. Los algoritmos de grafos resuelven problemas del mundo real: navegación GPS, redes sociales, logística, telecomunicaciones.
Un grafo es un par G = (V, E) donde V es un conjunto de vértices y E es una relación binaria sobre V. Los elementos de E son aristas que conectan pares de vértices.
La teoría de grafos formaliza preguntas intuitivas:
Cada pregunta corresponde a una propiedad de la relación binaria E. Este tema explora cómo resolver estos problemas algorítmicamente.
Un grafo no dirigido modela una relación simétrica: si hay arista (u,v), también hay (v,u).
Un grafo dirigido modela una relación asimétrica: (u,v) ≠ (v,u).
sigue = {(Ana, Luis), (Luis, Marta), (Marta, Ana)} forma un ciclo dirigido.
// Grafo como lista de adyacencia con pesos (ponderado)
class GrafoPonderado {
constructor(vertices) {
this.vertices = vertices;
this.adyacencia = new Map();
vertices.forEach(v => this.adyacencia.set(v, []));
}
agregarArista(u, v, peso = 1) {
this.adyacencia.get(u).push({ destino: v, peso });
}
// Obtener vecinos de un vértice
vecinos(v) {
return this.adyacencia.get(v);
}
// Grado de un vértice (número de aristas)
grado(v) {
return this.adyacencia.get(v).length;
}
// Obtener todas las aristas
aristas() {
const aristas = [];
for (const [u, adyacentes] of this.adyacencia) {
for (const { destino: v, peso } of adyacentes) {
aristas.push({ u, v, peso });
}
}
return aristas;
}
}
// Uso
const grafo = new GrafoPonderado(["A", "B", "C", "D"]);
grafo.agregarArista("A", "B", 4);
grafo.agregarArista("A", "C", 2);
grafo.agregarArista("B", "C", 1);
grafo.agregarArista("B", "D", 5);
grafo.agregarArista("C", "D", 8);
console.log("Grado de A:", grafo.grado("A")); // 2
console.log("Aristas:", grafo.aristas());
Un grafo es conexo si existe camino entre todo par de vértices. Las componentes conexas son subgrafos máximos conexos.
// Encontrar componentes conexas con DFS
class GrafoConexo {
constructor(vertices) {
this.vertices = vertices;
this.adyacencia = new Map();
vertices.forEach(v => this.adyacencia.set(v, []));
}
agregarArista(u, v) {
this.adyacencia.get(u).push(v);
this.adyacencia.get(v).push(u);
}
// DFS para marcar vértices visitados
dfs(v, visitados) {
visitados.add(v);
for (const vecino of this.adyacencia.get(v)) {
if (!visitados.has(vecino)) {
this.dfs(vecino, visitados);
}
}
}
// Encontrar componentes conexas
componentesConexas() {
const visitados = new Set();
const componentes = [];
for (const v of this.vertices) {
if (!visitados.has(v)) {
const componente = new Set();
this.dfs_marcar(v, visitados, componente);
componentes.push(Array.from(componente));
}
}
return componentes;
}
dfs_marcar(v, visitados, componente) {
visitados.add(v);
componente.add(v);
for (const vecino of this.adyacencia.get(v)) {
if (!visitados.has(vecino)) {
this.dfs_marcar(vecino, visitados, componente);
}
}
}
}
// Uso
const grf = new GrafoConexo(["A", "B", "C", "D", "E"]);
grf.agregarArista("A", "B");
grf.agregarArista("B", "C");
grf.agregarArista("D", "E");
console.log("Componentes:", grf.componentesConexas());
// [["A", "B", "C"], ["D", "E"]]
Dijkstra encuentra el camino mínimo desde un nodo fuente a todos los demás. Usa una propiedad de optimalidad: el subcamino de un camino óptimo es también óptimo.
// Algoritmo de Dijkstra (O(V² + E) con array, O((V + E) log V) con heap)
class GrafoDijkstra {
constructor(vertices) {
this.vertices = vertices;
this.adyacencia = new Map();
vertices.forEach(v => this.adyacencia.set(v, []));
}
agregarArista(u, v, peso) {
this.adyacencia.get(u).push({ destino: v, peso });
}
dijkstra(fuente) {
const distancias = new Map();
const visitados = new Set();
const anterior = new Map();
// Inicializar distancias
for (const v of this.vertices) {
distancias.set(v, v === fuente ? 0 : Infinity);
anterior.set(v, null);
}
for (let i = 0; i < this.vertices.length; i++) {
// Seleccionar vértice no visitado con mínima distancia
let u = null;
let minDist = Infinity;
for (const v of this.vertices) {
if (!visitados.has(v) && distancias.get(v) < minDist) {
u = v;
minDist = distancias.get(v);
}
}
if (u === null || minDist === Infinity) break;
visitados.add(u);
// Relajación de aristas
for (const { destino: v, peso } of this.adyacencia.get(u)) {
const nuevaDist = distancias.get(u) + peso;
if (nuevaDist < distancias.get(v)) {
distancias.set(v, nuevaDist);
anterior.set(v, u);
}
}
}
return { distancias, anterior };
}
// Reconstruir camino desde fuente a destino
reconstruirCamino(anterior, destino) {
const camino = [];
let actual = destino;
while (actual !== null) {
camino.unshift(actual);
actual = anterior.get(actual);
}
return camino;
}
}
// Uso
const gd = new GrafoDijkstra(["A", "B", "C", "D"]);
gd.agregarArista("A", "B", 4);
gd.agregarArista("A", "C", 2);
gd.agregarArista("B", "C", 1);
gd.agregarArista("B", "D", 5);
gd.agregarArista("C", "D", 8);
const { distancias, anterior } = gd.dijkstra("A");
console.log("Distancias desde A:", distancias);
// Map { A → 0, C → 2, B → 3, D → 8 }
console.log("Camino A→D:", gd.reconstruirCamino(anterior, "D"));
// ["A", "C", "B", "D"]
Un árbol generador es un subgrafo conexo acíclico que conecta todos los vértices. El MST es el árbol generador con peso mínimo.
// Algoritmo de Kruskal para MST (usando Union-Find)
class UnionFind {
constructor(elementos) {
this.padre = new Map();
this.rango = new Map();
elementos.forEach(e => {
this.padre.set(e, e);
this.rango.set(e, 0);
});
}
encontrar(x) {
if (this.padre.get(x) !== x) {
this.padre.set(x, this.encontrar(this.padre.get(x))); // Compresión de ruta
}
return this.padre.get(x);
}
unir(x, y) {
const px = this.encontrar(x);
const py = this.encontrar(y);
if (px === py) return false; // Ya están en el mismo conjunto
// Unión por rango
if (this.rango.get(px) < this.rango.get(py)) {
this.padre.set(px, py);
} else if (this.rango.get(px) > this.rango.get(py)) {
this.padre.set(py, px);
} else {
this.padre.set(py, px);
this.rango.set(px, this.rango.get(px) + 1);
}
return true;
}
}
class GrafoMST {
constructor(vertices) {
this.vertices = vertices;
this.aristas = [];
}
agregarArista(u, v, peso) {
this.aristas.push({ u, v, peso });
}
kruskal() {
// Ordenar aristas por peso
this.aristas.sort((a, b) => a.peso - b.peso);
const uf = new UnionFind(this.vertices);
const mst = [];
let pesoTotal = 0;
for (const { u, v, peso } of this.aristas) {
if (uf.unir(u, v)) {
mst.push({ u, v, peso });
pesoTotal += peso;
}
}
return { mst, pesoTotal };
}
}
// Uso
const gm = new GrafoMST(["A", "B", "C", "D"]);
gm.agregarArista("A", "B", 4);
gm.agregarArista("A", "C", 2);
gm.agregarArista("B", "C", 1);
gm.agregarArista("B", "D", 5);
gm.agregarArista("C", "D", 8);
const { mst, pesoTotal } = gm.kruskal();
console.log("MST:", mst);
// [{ u: "B", v: "C", peso: 1 }, { u: "A", v: "C", peso: 2 }, { u: "A", v: "B", peso: 4 }]
console.log("Peso total:", pesoTotal); // 7
Un grafo acíclico (DAG) no contiene ciclos. Un grafo es bipartido si sus vértices se pueden dividir en dos conjuntos sin aristas dentro de cada conjunto.
// Detectar si un grafo es bipartido (2-coloreable)
class GrafoBipartido {
constructor(vertices) {
this.vertices = vertices;
this.adyacencia = new Map();
vertices.forEach(v => this.adyacencia.set(v, []));
}
agregarArista(u, v) {
this.adyacencia.get(u).push(v);
this.adyacencia.get(v).push(u);
}
esBipartido() {
const color = new Map();
for (const v of this.vertices) {
if (color.get(v) === undefined) {
// BFS para colorear
const cola = [v];
color.set(v, 0);
while (cola.length > 0) {
const u = cola.shift();
for (const vecino of this.adyacencia.get(u)) {
if (color.get(vecino) === undefined) {
color.set(vecino, 1 - color.get(u));
cola.push(vecino);
} else if (color.get(vecino) === color.get(u)) {
return false; // Vecino tiene el mismo color
}
}
}
}
}
return true;
}
// Coloración greedy (no óptima en general)
coloracionGreedy() {
const color = new Map();
const coloresDisponibles = new Set();
for (const v of this.vertices) {
const coloresUsados = new Set();
for (const vecino of this.adyacencia.get(v)) {
if (color.get(vecino) !== undefined) {
coloresUsados.add(color.get(vecino));
}
}
let c = 0;
while (coloresUsados.has(c)) c++;
color.set(v, c);
}
return color;
}
}
// Uso
const gb = new GrafoBipartido(["A", "B", "C", "D"]);
gb.agregarArista("A", "B");
gb.agregarArista("A", "C");
gb.agregarArista("B", "D");
gb.agregarArista("C", "D");
console.log("¿Es bipartido?", gb.esBipartido()); // true
console.log("Coloración:", gb.coloracionGreedy());
En un grafo con capacidades en aristas, el flujo máximo es la máxima cantidad que puede fluir de una fuente a un sumidero. El teorema min-corte max-flujo relaciona esto con el corte de mínima capacidad.
// Algoritmo de Ford-Fulkerson (simplificado con BFS = Edmonds-Karp)
class GrafoFlujo {
constructor(vertices) {
this.vertices = vertices;
this.grafo = new Map();
vertices.forEach(v => this.grafo.set(v, new Map()));
}
agregarArista(u, v, capacidad) {
if (!this.grafo.get(u).has(v)) {
this.grafo.get(u).set(v, 0);
}
if (!this.grafo.get(v).has(u)) {
this.grafo.get(v).set(u, 0);
}
this.grafo.get(u).set(v, capacidad);
}
bfsEncontrarCamino(fuente, sumidero) {
const visitados = new Set([fuente]);
const cola = [[fuente, [fuente]]];
while (cola.length > 0) {
const [u, camino] = cola.shift();
if (u === sumidero) return camino;
for (const v of this.grafo.get(u).keys()) {
if (!visitados.has(v) && this.grafo.get(u).get(v) > 0) {
visitados.add(v);
cola.push([v, [...camino, v]]);
}
}
}
return null;
}
flujoMaximo(fuente, sumidero) {
let flujoTotal = 0;
while (true) {
const camino = this.bfsEncontrarCamino(fuente, sumidero);
if (!camino) break;
// Encontrar la capacidad mínima del camino
let flujoMinimo = Infinity;
for (let i = 0; i < camino.length - 1; i++) {
const u = camino[i];
const v = camino[i + 1];
flujoMinimo = Math.min(flujoMinimo, this.grafo.get(u).get(v));
}
// Actualizar capacidades
for (let i = 0; i < camino.length - 1; i++) {
const u = camino[i];
const v = camino[i + 1];
this.grafo.get(u).set(v, this.grafo.get(u).get(v) - flujoMinimo);
this.grafo.get(v).set(u, this.grafo.get(v).get(u) + flujoMinimo);
}
flujoTotal += flujoMinimo;
}
return flujoTotal;
}
}
// Uso
const gf = new GrafoFlujo(["A", "B", "C", "D"]);
gf.agregarArista("A", "B", 10);
gf.agregarArista("A", "C", 10);
gf.agregarArista("B", "D", 4);
gf.agregarArista("C", "B", 2);
gf.agregarArista("C", "D", 8);
console.log("Flujo máximo A→D:", gf.flujoMaximo("A", "D")); // 12
Los algoritmos de grafos resuelven problemas reales en:
// Aplicación: Red social - distancia geodésica
class RedSocial {
constructor(usuarios) {
this.usuarios = usuarios;
this.adyacencia = new Map();
usuarios.forEach(u => this.adyacencia.set(u, []));
}
agregarAmistad(u, v) {
this.adyacencia.get(u).push(v);
this.adyacencia.get(v).push(u);
}
// Encontrar distancia mínima entre dos usuarios (BFS)
distancia(u, v) {
if (u === v) return 0;
const visitados = new Set([u]);
const cola = [[u, 0]];
while (cola.length > 0) {
const [nodo, dist] = cola.shift();
for (const vecino of this.adyacencia.get(nodo)) {
if (vecino === v) return dist + 1;
if (!visitados.has(vecino)) {
visitados.add(vecino);
cola.push([vecino, dist + 1]);
}
}
}
return Infinity; // No conexo
}
// Grado de separación (six degrees)
usuariosADistancia(usuario, k) {
const visitados = new Map();
const cola = [[usuario, 0]];
visitados.set(usuario, 0);
while (cola.length > 0) {
const [u, dist] = cola.shift();
for (const vecino of this.adyacencia.get(u)) {
if (!visitados.has(vecino)) {
visitados.set(vecino, dist + 1);
if (dist + 1 <= k) {
cola.push([vecino, dist + 1]);
}
}
}
}
const resultado = [];
for (const [usuario, distancia] of visitados) {
if (distancia === k) resultado.push(usuario);
}
return resultado;
}
}
// Uso
const red = new RedSocial(["Ana", "Luis", "Marta", "Carlos", "Diana"]);
red.agregarAmistad("Ana", "Luis");
red.agregarAmistad("Luis", "Marta");
red.agregarAmistad("Marta", "Carlos");
red.agregarAmistad("Carlos", "Diana");
console.log("Distancia Ana-Diana:", red.distancia("Ana", "Diana")); // 4
console.log("Amigos a distancia 2 de Ana:", red.usuariosADistancia("Ana", 2));
// ["Marta"]
La teoría de grafos es una de las aplicaciones más impactantes de las relaciones discretas. Los grafos permiten modelar sistemas complejos del mundo real: redes, dependencias, relaciones.
Cada algoritmo de grafos que hemos estudiado—desde DFS/BFS hasta Dijkstra, Kruskal y Ford-Fulkerson—explota una propiedad específica de la relación binaria subyacente:
Lo más importante es que ninguno de estos algoritmos es "magia". Cada uno es una consecuencia lógica de las propiedades de las relaciones discretas.
Cuando entiendas que un grafo es simplemente una relación binaria con estructura, y que los algoritmos de grafos explotan esas estructuras, habrás adquirido una comprensión profunda de por qué funcionan. Eso te permitirá:
La teoría de grafos y las relaciones discretas son el lenguaje universal de la computación moderna. Dominarlas es dominar la esencia de cómo resolvemos problemas complejos con elegancia algorítmica.