22. Ordenamiento topológico

Un ordenamiento topológico coloca las tareas de un grafo dirigido en una secuencia que respeta todas sus dependencias: cada requisito aparece antes que aquello que depende de él.

22.1 Introducción

Muchos problemas contienen actividades que no pueden realizarse en cualquier orden. Para compilar un programa se necesitan antes ciertos archivos; para cursar una materia pueden exigirse correlativas; para construir un producto algunas piezas deben estar terminadas previamente.

Estas relaciones se representan mediante un grafo dirigido. Una arista u → v indica que u debe aparecer antes que v. El ordenamiento topológico busca una secuencia lineal que respete todas esas restricciones.

22.2 Definición

Un orden topológico de un grafo dirigido G = (V, E) es una secuencia que contiene cada vértice exactamente una vez y cumple la siguiente condición:

Para toda arista u → v, el vértice u aparece antes que v.

Por ejemplo, si existen las dependencias A → C y B → C, tanto A como B deben estar antes que C. Sin embargo, la condición no determina necesariamente cuál debe aparecer primero entre A y B.

22.3 Solo existe en grafos dirigidos acíclicos

Un ordenamiento topológico existe si y solo si el grafo es un DAG, sigla de Directed Acyclic Graph: grafo dirigido acíclico.

G tiene un orden topológico ⇔ G es dirigido y no contiene ciclos dirigidos.

Si A depende de B, B de C y C de A, ninguna tarea puede colocarse primero. El ciclo expresa dependencias incompatibles y hace imposible construir el orden.

22.4 El orden puede no ser único

Cuando dos vértices no están relacionados por una dependencia, pueden intercambiar sus posiciones sin invalidar el resultado.

DependenciasÓrdenes posibles
A → C, B → CA, B, C   o   B, A, C
A → B, B → CÚnicamente A, B, C

Si en cada paso hay un único vértice disponible, el orden topológico es único. Si alguna vez podemos elegir entre dos o más, existen varios órdenes válidos.

22.5 Grado de entrada

El grado de entrada de un vértice es la cantidad de aristas que llegan a él. En un grafo de dependencias representa cuántos requisitos directos siguen pendientes.

indegree(v) = cantidad de aristas u → v
indegree(v) = 0 ⇒ v no tiene requisitos pendientes

Todo DAG posee al menos un vértice con grado de entrada cero. Si no existiera ninguno, al retroceder indefinidamente por las dependencias terminaríamos encontrando un ciclo.

22.6 Simulación interactiva: construye un orden topológico

Elige cualquiera de los vértices disponibles, es decir, aquellos cuyo grado de entrada actual es cero. Al seleccionar uno se eliminan sus aristas salientes y pueden habilitarse nuevas tareas.

Vértices disponibles

Orden construido

Acción

Selecciona un vértice disponible.
Hay 2 vértices disponibles.
Procesados0 / 7
Disponibles2
Aristas pendientes8
ResultadoEn proceso

Los nodos verdes tienen grado de entrada cero y pueden elegirse. Los celestes ya fueron procesados. El botón para agregar un ciclo permite observar cómo Kahn detecta que el orden es imposible.

22.7 Algoritmo de Kahn

El algoritmo de Kahn elimina repetidamente vértices con grado de entrada cero:

  1. Calcular el grado de entrada de todos los vértices.
  2. Guardar en una cola los vértices cuyo grado de entrada es cero.
  3. Retirar uno, agregarlo al orden y eliminar conceptualmente sus aristas salientes.
  4. Si un vecino alcanza grado cero, incorporarlo a la cola.
  5. Repetir hasta vaciar la cola.

La cola puede reemplazarse por una pila o una cola de prioridad. Cambiar la estructura modifica cuál de los posibles órdenes se obtiene, pero no la validez del algoritmo.

22.8 Implementación de Kahn en JavaScript

function ordenTopologico(grafo) {
  const vertices = Object.keys(grafo);
  const gradoEntrada = Object.fromEntries(
    vertices.map(v => [v, 0])
  );

  for (const origen of vertices) {
    for (const destino of grafo[origen]) {
      gradoEntrada[destino]++;
    }
  }

  const cola = vertices.filter(v => gradoEntrada[v] === 0);
  const orden = [];
  let frente = 0;

  while (frente < cola.length) {
    const actual = cola[frente++];
    orden.push(actual);

    for (const vecino of grafo[actual]) {
      gradoEntrada[vecino]--;
      if (gradoEntrada[vecino] === 0) cola.push(vecino);
    }
  }

  return orden.length === vertices.length ? orden : null;
}

La función devuelve null cuando no logra procesar todos los vértices, lo que indica la presencia de al menos un ciclo dirigido.

22.9 Detección de ciclos con Kahn

Si la cola queda vacía antes de procesar todos los vértices, los nodos restantes conservan dependencias entre ellos. No puede existir un orden topológico completo.

procesados = |V| ⇒ existe orden topológico
procesados < |V| ⇒ existe un ciclo dirigido

Kahn confirma que hay un ciclo, aunque por sí solo no devuelve necesariamente los vértices exactos que lo forman.

22.10 Ordenamiento topológico mediante DFS

También se puede obtener un orden topológico con DFS. Un vértice se agrega al resultado después de terminar de explorar todos sus descendientes. Al final se invierte la lista de finalización.

Explorar descendientes → finalizar vértice → agregarlo
Invertir el resultado al terminar

Para detectar ciclos se utilizan tres estados: no visitado, en proceso y terminado. Encontrar una arista hacia un vértice en proceso revela un ciclo dirigido.

22.11 Implementación con DFS

function ordenTopologicoDFS(grafo) {
  const estado = {}; // 0: nuevo, 1: en proceso, 2: terminado
  const resultado = [];

  function visitar(actual) {
    if (estado[actual] === 1) return false; // ciclo
    if (estado[actual] === 2) return true;

    estado[actual] = 1;
    for (const vecino of grafo[actual]) {
      if (!visitar(vecino)) return false;
    }
    estado[actual] = 2;
    resultado.push(actual);
    return true;
  }

  for (const vertice of Object.keys(grafo)) {
    if (!estado[vertice] && !visitar(vertice)) return null;
  }
  return resultado.reverse();
}

22.12 Validar un orden propuesto

Para comprobar una secuencia se registra la posición de cada vértice. Después se verifica que el origen de cada arista aparezca antes que su destino.

function esOrdenValido(orden, aristas) {
  const posicion = new Map(
    orden.map((vertice, indice) => [vertice, indice])
  );

  return aristas.every(([origen, destino]) =>
    posicion.has(origen) &&
    posicion.has(destino) &&
    posicion.get(origen) < posicion.get(destino)
  );
}

Además debe comprobarse que la secuencia incluya exactamente una vez todos los vértices del grafo.

22.13 Kahn y DFS: comparación

CaracterísticaKahnDFS
Idea centralEliminar grados de entrada ceroOrdenar por finalización
EstructuraCola y contador de gradosPila de recursión y estados
Detección de cicloQuedan vértices sin procesarArista hacia un nodo en proceso
Elección lexicográficaNatural con cola de prioridadDepende del orden de exploración

Ambos métodos tienen la misma complejidad asintótica. Kahn suele resultar más intuitivo para planificar tareas disponibles; DFS se integra bien con otros análisis basados en profundidad.

22.14 Complejidad

Con listas de adyacencia, tanto Kahn como DFS visitan cada vértice y examinan cada arista una cantidad constante de veces.

Tiempo: O(V + E)
Espacio adicional: O(V)

Calcular repetidamente los grados desde cero produciría trabajo innecesario. Kahn es eficiente porque actualiza únicamente los vecinos del vértice recién procesado.

22.15 Aplicaciones

ProblemaInterpretación del orden
CompilaciónConstruir módulos después de sus dependencias
Plan de estudiosCursar materias después de sus correlativas
Gestión de proyectosProgramar tareas respetando requisitos previos
Instalación de paquetesInstalar bibliotecas antes de los programas que las usan
Hojas de cálculoRecalcular celdas en un orden válido
Procesamiento de datosEjecutar etapas de una canalización según sus entradas

El ordenamiento topológico determina una secuencia válida, pero no resuelve por sí solo cuestiones de duración, recursos limitados o ejecución paralela.

22.16 Errores comunes

  • Intentar ordenar topológicamente un grafo no dirigido.
  • No comprobar si existe un ciclo.
  • Suponer que el orden topológico siempre es único.
  • Interpretar u → v en la dirección contraria a la dependencia definida.
  • Olvidar vértices aislados, que también deben aparecer en el resultado.
  • Modificar el grado de entrada original y reutilizarlo sin recalcularlo.
  • Confundir un orden válido con el camino más corto o con una planificación óptima.

22.17 Qué debes recordar de este tema

  • Un orden topológico coloca cada requisito antes que sus dependientes.
  • Solo los grafos dirigidos acíclicos admiten este orden.
  • Un mismo DAG puede tener varios órdenes válidos.
  • Kahn procesa vértices con grado de entrada cero.
  • DFS ordena los vértices según sus tiempos de finalización.
  • No procesar todos los vértices indica la existencia de un ciclo.
  • La complejidad de ambos métodos es O(V + E).

22.18 Conclusión

El ordenamiento topológico transforma dependencias parciales en una secuencia ejecutable. Los algoritmos de Kahn y DFS permiten construirla eficientemente y, al mismo tiempo, descubrir cuándo un ciclo vuelve incompatibles las restricciones.

En el próximo tema profundizaremos en la detección de ciclos para grafos dirigidos y no dirigidos.