38. Introducción a la teoría de grafos

La teoría de grafos modela objetos y las conexiones entre ellos. Con vértices y aristas podemos representar redes, rutas, dependencias, relaciones sociales, enlaces web y estructuras de datos.

38.1 Introducción

Muchos problemas no consisten en procesar elementos aislados, sino en estudiar cómo se conectan. Una ciudad se relaciona con otras por rutas, una tarea depende de otras tareas y una cuenta sigue a otras cuentas.

Un grafo es una abstracción que conserva precisamente esa información de conexión. La posición o el dibujo de sus puntos no es lo esencial: importa qué vértices están relacionados por aristas.

38.2 Definición de grafo

Un grafo no dirigido se expresa como G = (V, E), donde V es un conjunto de vértices y E es un conjunto de aristas. Cada arista une dos vértices.

V = {A, B, C, D}.
E = {{A, B}, {A, C}, {B, D}}.

Hay una conexión entre A y B, entre A y C y entre B y D.

En un grafo no dirigido, {A, B} y {B, A} representan la misma arista. El par no tiene dirección; solo expresa que A y B son adyacentes.

38.3 Vértices y aristas

Los vértices, también llamados nodos, representan las entidades del problema. Las aristas representan la relación o conexión definida por el modelo.

Red social: vértices = perfiles; aristas = amistad.
Mapa: vértices = ciudades; aristas = rutas.
Dependencias: vértices = tareas; aristas = prerequisitos.
Web: vértices = páginas; aristas = enlaces.

El significado de una arista debe declararse. La misma colección de entidades puede producir grafos distintos según se modele amistad, seguimiento, distancia, comunicación o dependencia.

38.4 Orden y tamaño

El orden de un grafo es la cantidad de vértices, |V|. El tamaño es la cantidad de aristas, |E|.

Para V = {A, B, C, D} y E = {{A, B}, {A, C}, {B, D}}:

Orden = |V| = 4.
Tamaño = |E| = 3.

Estas cantidades permiten comparar grafos y estimar el costo de sus algoritmos. En programación es común usar V para el número de vértices y E para el número de aristas.

38.5 Adyacencia e incidencia

Dos vértices son adyacentes si una arista los une. Una arista es incidente con los vértices que conecta.

En la arista {A, B}:
A y B son adyacentes.
La arista es incidente con A y con B.

A y D no son adyacentes en el ejemplo anterior.

La adyacencia es una relación simétrica en grafos no dirigidos. En grafos dirigidos se reemplaza por relaciones de entrada y salida, que no tienen por qué ser recíprocas.

38.6 Grafos simples, lazos y aristas paralelas

Un grafo simple no tiene lazos ni aristas paralelas. Un lazo conecta un vértice consigo mismo; aristas paralelas son varias aristas que unen el mismo par de vértices.

Lazo: {A, A}.
Aristas paralelas: dos conexiones diferentes entre A y B.

Un grafo simple tiene a lo sumo una arista entre dos vértices distintos y ningún lazo.

Si se permiten aristas paralelas se habla de multigrafo. Puede ser apropiado para modelar, por ejemplo, varias rutas o vuelos diferentes entre las mismas ciudades.

38.7 Grado de un vértice

El grado de un vértice v, escrito deg(v), es la cantidad de aristas incidentes en v. En un grafo simple sin lazos coincide con la cantidad de vecinos de v.

En E = {{A, B}, {A, C}, {B, D}}:
deg(A) = 2.
deg(B) = 2.
deg(C) = 1.
deg(D) = 1.

Un vértice de grado 0 se llama aislado. Un vértice de grado 1 se llama hoja o vértice pendiente en muchos contextos.

38.8 Lema del apretón de manos

En todo grafo no dirigido finito, la suma de los grados de los vértices es igual al doble de la cantidad de aristas:

Σ deg(v) = 2|E|.

Ejemplo: 2 + 2 + 1 + 1 = 6.
También: 2|E| = 2·3 = 6.

Cada arista contribuye uno al grado de cada uno de sus dos extremos, por eso se cuenta dos veces. Una consecuencia es que la cantidad de vértices de grado impar siempre es par.

38.9 Cantidad máxima de aristas

Un grafo simple no dirigido de n vértices tiene como máximo una arista por cada par no ordenado de vértices. Por lo tanto, el máximo es:

n(n - 1) / 2 = C(n, 2).

Con n = 5: 5·4/2 = 10 aristas como máximo.

El grafo que contiene todas esas aristas se llama grafo completo y se denota Kn. En él, cada vértice tiene grado n - 1.

38.10 Grafos dirigidos

En un grafo dirigido o digrafo, las aristas tienen orientación y se representan como pares ordenados (u, v). La arista indica una conexión que va de u hacia v.

V = {Ana, Beto, Carla}.
E = {(Ana, Beto), (Beto, Carla)}.

Ana sigue a Beto.
Beto sigue a Carla.
No se deduce que Beto siga a Ana.

Los enlaces web, los seguidores de una red social y las dependencias de tareas se modelan a menudo con grafos dirigidos porque la relación no es necesariamente simétrica.

38.11 Grado de entrada y salida

En un digrafo, el grado de salida de v es el número de aristas que salen de v; el grado de entrada es el número de aristas que llegan a v.

Para E = {(A, B), (A, C), (C, A)}:

A: salida 2, entrada 1.
B: salida 0, entrada 1.
C: salida 1, entrada 1.

La suma de todos los grados de salida es igual al número de aristas, y lo mismo ocurre con la suma de grados de entrada. Cada arista aporta una salida y una entrada.

38.12 Grafos ponderados

Un grafo ponderado asigna un peso a cada arista. El peso puede representar distancia, tiempo, costo, capacidad, prioridad o cualquier medida definida por el problema.

Ruta A—B: 12 km.
Ruta A—C: 7 km.
Ruta C—D: 5 km.

El peso no cambia qué vértices están conectados; agrega información sobre la conexión.

Los algoritmos de caminos mínimos y redes de costo usan pesos. Debe especificarse si un peso mayor es mejor, peor o simplemente una magnitud neutra para interpretar correctamente el resultado.

38.13 Lista de adyacencia

Una lista de adyacencia guarda, para cada vértice, sus vecinos. Es una representación eficiente para grafos dispersos, es decir, con muchas menos aristas que el máximo posible.

A: B, C.
B: A, D.
C: A.
D: B.

La suma de las longitudes de las listas es 2|E| en un grafo no dirigido.

Recorrer los vecinos de un vértice es directo. La memoria utilizada es O(V + E), por lo que esta estructura es frecuente en implementaciones de algoritmos sobre grafos.

38.14 Matriz de adyacencia

Una matriz de adyacencia usa una tabla V × V. La celda (i, j) vale 1 si existe una arista de i a j, y 0 en caso contrario; en grafos ponderados puede contener el peso.

ABCD
A0110
B1001
C1000
D0100

La matriz de un grafo no dirigido es simétrica respecto de la diagonal. Consultar si existe una arista es O(1), pero guardar la matriz ocupa O(V²), incluso cuando hay pocas conexiones.

38.15 Comparación de representaciones

OperaciónLista de adyacenciaMatriz de adyacencia
MemoriaO(V + E)O(V²)
Consultar si u-v existedepende del grado de uO(1)
Recorrer vecinos de uO(grado(u))O(V)
Mejor escenariografo dispersografo denso o muchas consultas de arista

La elección depende del algoritmo y de la densidad del grafo. No hay una representación universalmente mejor.

38.16 Implementar un grafo no dirigido

class GrafoNoDirigido {
  constructor() {
    this.adyacentes = new Map();
  }

  agregarVertice(vertice) {
    if (!this.adyacentes.has(vertice)) this.adyacentes.set(vertice, new Set());
  }

  agregarArista(origen, destino) {
    if (origen === destino) throw new Error("no se permiten lazos en este grafo simple");
    this.agregarVertice(origen);
    this.agregarVertice(destino);
    this.adyacentes.get(origen).add(destino);
    this.adyacentes.get(destino).add(origen);
  }

  vecinos(vertice) {
    return [...(this.adyacentes.get(vertice) ?? [])];
  }
}

const grafo = new GrafoNoDirigido();
grafo.agregarArista("A", "B");
grafo.agregarArista("A", "C");
console.log(grafo.vecinos("A")); // ["B", "C"]

Los Set evitan aristas paralelas y la inserción en ambos sentidos mantiene la simetría. Si el dominio necesita lazos o múltiples conexiones, debe usarse otra representación.

38.17 Implementar un grafo dirigido

class GrafoDirigido {
  constructor() {
    this.salientes = new Map();
  }

  agregarArista(origen, destino) {
    if (!this.salientes.has(origen)) this.salientes.set(origen, new Set());
    if (!this.salientes.has(destino)) this.salientes.set(destino, new Set());
    this.salientes.get(origen).add(destino);
  }

  vecinosSalientes(vertice) {
    return [...(this.salientes.get(vertice) ?? [])];
  }
}

const red = new GrafoDirigido();
red.agregarArista("Ana", "Beto");
red.agregarArista("Beto", "Carla");
console.log(red.vecinosSalientes("Ana")); // ["Beto"]

Agregar (A, B) no agrega automáticamente (B, A). Para responder consultas sobre vecinos entrantes de forma eficiente conviene mantener también una estructura de adyacencia inversa.

38.18 Subgrafos y grafos inducidos

Un subgrafo usa un subconjunto de vértices y aristas de un grafo original. Un subgrafo inducido por un conjunto de vértices conserva todas las aristas del grafo original cuyos extremos pertenecen a ese conjunto.

G tiene vértices {A, B, C, D} y aristas {AB, AC, BD}.

El subgrafo inducido por {A, B, C} contiene:
vértices {A, B, C} y aristas {AB, AC}.

Los subgrafos permiten enfocarse en una parte de una red: usuarios de una comunidad, servidores de una región o módulos de un proyecto.

38.19 Grafos bipartitos

Un grafo es bipartito si sus vértices pueden separarse en dos conjuntos disjuntos U y W, y todas las aristas van de un conjunto al otro. No hay aristas dentro del mismo grupo.

U: estudiantes.
W: materias.
Arista estudiante—materia: está inscripto.

Otra aplicación: usuarios—productos, con arista «compró» o «calificó».

Los grafos bipartitos modelan relaciones entre dos tipos de entidades. Más adelante aparecen en problemas de asignación y emparejamiento.

38.20 Qué no indica el dibujo

Un dibujo de un grafo puede cambiar la posición de los vértices, la longitud de las aristas o el cruce visual de líneas sin cambiar el grafo. Dos aristas que se cruzan en el papel no crean un vértice nuevo salvo que se declare explícitamente.

La identidad de un grafo depende de V y E.

La geometría del dibujo solo ayuda a visualizar.
Un cruce de líneas no es una conexión por defecto.

Esta distinción evita errores al interpretar mapas esquemáticos, diagramas de redes y diagramas de Hasse.

38.21 Aplicaciones en programación

  • Rutas y navegación entre ubicaciones.
  • Dependencias de paquetes, tareas y compilación.
  • Redes sociales, recomendaciones y análisis de comunidades.
  • Enlaces entre páginas, servicios y microservicios.
  • Planificación, asignación de recursos y análisis de flujos.

La calidad del resultado depende de que los vértices, las aristas, las direcciones y los pesos representen adecuadamente el problema real.

38.22 Errores frecuentes

  • Confundir una arista no dirigida con dos aristas dirigidas.
  • Olvidar agregar ambos sentidos en una lista de adyacencia no dirigida.
  • Usar una matriz O(V²) para un grafo enorme y muy disperso sin necesidad.
  • Confundir el grado con la cantidad total de vértices.
  • Interpretar un cruce visual de aristas como una conexión.
  • No definir qué representa una arista ni cómo se interpretan sus pesos.

38.23 Qué debes recordar y conclusión

  • Un grafo G = (V, E) modela vértices y conexiones.
  • El orden es |V| y el tamaño es |E|.
  • Los grafos dirigidos usan aristas orientadas y distinguen grados de entrada y salida.
  • El lema del apretón de manos establece que la suma de grados es 2|E| en grafos no dirigidos.
  • Las listas de adyacencia favorecen grafos dispersos; las matrices favorecen consultas rápidas de arista.
  • Los grafos permiten modelar redes, dependencias y relaciones complejas de software.

La teoría de grafos ofrece un lenguaje compacto para describir conexiones. En el próximo tema estudiaremos árboles, una familia especial de grafos que organiza datos de manera jerárquica.