24. Componentes fuertemente conexas

Una componente fuertemente conexa reúne vértices de un grafo dirigido que pueden alcanzarse mutuamente. Dentro de ella siempre existe un camino de ida y otro de regreso.

24.1 Introducción

En un grafo no dirigido, pertenecer a la misma componente conexa significa estar unidos por algún camino. En un grafo dirigido la situación es más exigente: llegar desde A hasta B no garantiza que podamos regresar desde B hasta A.

Las componentes fuertemente conexas permiten dividir un grafo dirigido en regiones de alcanzabilidad mutua. También se las conoce como SCC, por Strongly Connected Components.

24.2 Definición

Una componente fuertemente conexa es un conjunto maximal de vértices C en el que cada par u, v cumple:

Existe un camino dirigido u ⇝ v y también un camino dirigido v ⇝ u.

La palabra maximal significa que no es posible agregar otro vértice al conjunto sin perder la propiedad. Aunque un solo vértice siempre se alcanza a sí mismo mediante un camino de longitud cero, si puede agruparse con otros debe pertenecer a la componente mayor.

24.3 Ejemplo básico

Si tenemos A → B, B → C y C → A, los tres vértices forman una componente fuerte: podemos movernos entre cualquier par siguiendo la orientación de las aristas.

Si además existe C → D pero no hay ningún camino desde D de regreso a A, D no pertenece a esa componente.

A → B → C → A   forma una SCC
C → D   no incorpora D si falta un camino de retorno

24.4 Componentes fuertes y débiles

Dos conceptos distintos pueden aplicarse a grafos dirigidos:

ConectividadCondición
FuerteHay caminos dirigidos de ida y vuelta entre todos los vértices
DébilEl conjunto queda conectado si ignoramos la orientación de las aristas

Toda componente fuerte está contenida en una componente débil, pero una componente débil puede contener varias componentes fuertes.

24.5 Propiedades principales

  • Cada vértice pertenece exactamente a una componente fuertemente conexa.
  • Las componentes forman una partición del conjunto de vértices.
  • Una SCC puede contener un único vértice.
  • Todo vértice de una SCC puede alcanzar a todos los demás de la misma componente.
  • Si agrupamos cada componente en un solo nodo, el grafo resultante no contiene ciclos.

24.6 Simulación interactiva

El grafo comienza con tres componentes fuertes conectadas en una sola dirección. Agrega la arista de retorno G → A y observa cómo todas se fusionan, porque aparece un camino de regreso a través de la cadena completa.

Componentes detectadas

Grafo condensado

Interpretación

El grafo contiene 3 componentes fuertes.
Vértices7
Aristas9
Componentes fuertes3
Fuertemente conexoNo

Los colores representan las componentes calculadas por Tarjan. Las aristas blancas están dentro de una componente; las amarillas conectan componentes diferentes.

24.7 Idea ingenua

Una solución directa consiste en iniciar una búsqueda desde cada vértice y construir una matriz de alcanzabilidad. Dos vértices pertenecen a la misma SCC si cada uno alcanza al otro.

Este enfoque es correcto, pero repetir DFS o BFS desde todos los vértices puede costar O(V(V + E)). Kosaraju y Tarjan encuentran todas las componentes en tiempo lineal.

24.8 Grafo transpuesto

El grafo transpuesto Gᵀ se obtiene invirtiendo la dirección de todas las aristas.

u → v en G   se convierte en   v → u en Gᵀ

Invertir todas las aristas no modifica las componentes fuertemente conexas: si dentro de un conjunto existían caminos de ida y vuelta, después de invertirlos continúan existiendo en sentidos intercambiados.

24.9 Algoritmo de Kosaraju

Kosaraju utiliza dos recorridos DFS:

  1. Ejecutar DFS sobre el grafo original y guardar los vértices según su tiempo de finalización.
  2. Construir el grafo transpuesto.
  3. Procesar los vértices en orden decreciente de finalización.
  4. Cada DFS del segundo recorrido produce una componente fuerte.

El primer recorrido identifica por dónde conviene comenzar; el segundo evita atravesar prematuramente hacia otra componente.

24.10 Kosaraju en JavaScript

function kosaraju(grafo) {
  const vertices = Object.keys(grafo);
  const visitados = new Set();
  const orden = [];

  function dfs1(v) {
    visitados.add(v);
    for (const w of grafo[v]) if (!visitados.has(w)) dfs1(w);
    orden.push(v);
  }

  for (const v of vertices) if (!visitados.has(v)) dfs1(v);

  const transpuesto = Object.fromEntries(vertices.map(v => [v, []]));
  for (const v of vertices) {
    for (const w of grafo[v]) transpuesto[w].push(v);
  }

  visitados.clear();
  const componentes = [];

  function dfs2(v, componente) {
    visitados.add(v);
    componente.push(v);
    for (const w of transpuesto[v]) {
      if (!visitados.has(w)) dfs2(w, componente);
    }
  }

  while (orden.length) {
    const v = orden.pop();
    if (!visitados.has(v)) {
      const componente = [];
      dfs2(v, componente);
      componentes.push(componente);
    }
  }
  return componentes;
}

24.11 Algoritmo de Tarjan

Tarjan encuentra las SCC en un único recorrido DFS. A cada vértice le asigna:

  • índice: orden en el que fue descubierto;
  • low-link: menor índice alcanzable desde él sin abandonar la parte activa de DFS;
  • estado en pila: indica si todavía puede integrar la componente actual.

Si al terminar un vértice se cumple low[v] === indice[v], v es la raíz de una componente. Se retiran elementos de la pila hasta incluirlo.

24.12 Tarjan en JavaScript

function tarjan(grafo) {
  let siguienteIndice = 0;
  const indice = {}, low = {}, pila = [], enPila = new Set();
  const componentes = [];

  function visitar(v) {
    indice[v] = low[v] = siguienteIndice++;
    pila.push(v);
    enPila.add(v);

    for (const w of grafo[v]) {
      if (indice[w] === undefined) {
        visitar(w);
        low[v] = Math.min(low[v], low[w]);
      } else if (enPila.has(w)) {
        low[v] = Math.min(low[v], indice[w]);
      }
    }

    if (low[v] === indice[v]) {
      const componente = [];
      let w;
      do {
        w = pila.pop();
        enPila.delete(w);
        componente.push(w);
      } while (w !== v);
      componentes.push(componente);
    }
  }

  for (const v of Object.keys(grafo)) {
    if (indice[v] === undefined) visitar(v);
  }
  return componentes;
}

24.13 Índice y low-link

El índice nunca cambia después del descubrimiento. El valor low-link, en cambio, puede disminuir cuando DFS encuentra una ruta hacia un vértice anterior que todavía permanece en la pila.

indice[v] = momento de descubrimiento
low[v] = menor índice activo alcanzable desde v

Es importante actualizar con low[w] después de visitar un hijo, pero con indice[w] al encontrar una arista hacia un vértice que ya estaba en la pila.

24.14 Grafo condensado

El grafo condensado reemplaza cada componente fuerte por un único vértice. Se agrega una arista entre dos componentes si en el grafo original existe alguna arista que las conecte.

SCC₁ → SCC₂ si existe u → v con u ∈ SCC₁ y v ∈ SCC₂

El resultado siempre es un DAG. Si tuviera un ciclo, las componentes de ese ciclo serían mutuamente alcanzables y deberían haberse agrupado en una sola.

24.15 Kosaraju y Tarjan: comparación

CaracterísticaKosarajuTarjan
Recorridos DFSDosUno
Grafo transpuestoNecesarioNo necesario
Estructuras claveOrden de finalizaciónÍndices, low-link y pila
ImplementaciónSuele ser más intuitivaMás delicada, pero compacta
ComplejidadO(V + E)O(V + E)

24.16 Aplicaciones

  • Agrupar páginas web o usuarios que pueden alcanzarse mutuamente.
  • Detectar grupos de dependencias circulares.
  • Simplificar un grafo dirigido mediante su condensación.
  • Analizar estados mutuamente accesibles de un sistema.
  • Optimizar compiladores y análisis de flujo de control.
  • Estudiar redes de comunicación y sistemas biológicos.

24.17 Errores comunes

  • Ignorar la dirección de las aristas y obtener componentes débiles.
  • Agrupar vértices porque existe camino solo en un sentido.
  • Olvidar vértices aislados, que forman SCC individuales.
  • Procesar Kosaraju en el orden de finalización equivocado.
  • Construir incorrectamente el grafo transpuesto.
  • Actualizar mal el valor low-link de Tarjan.
  • Retirar un vértice de la pila antes de completar su componente.

24.18 Qué debes recordar de este tema

  • En una SCC todos los vértices son alcanzables mutuamente.
  • Las componentes fuertes forman una partición de los vértices.
  • Kosaraju usa dos DFS y el grafo transpuesto.
  • Tarjan usa un DFS, índices, low-link y una pila.
  • Ambos algoritmos trabajan en O(V + E).
  • El grafo condensado de las componentes siempre es un DAG.

24.19 Conclusión

Las componentes fuertemente conexas revelan la estructura interna de un grafo dirigido: separan las regiones con circulación completa de las conexiones que solo avanzan en una dirección. Kosaraju y Tarjan permiten encontrarlas eficientemente incluso en redes grandes.

En el próximo tema estudiaremos la clausura transitiva, que determina qué pares de vértices son alcanzables.