Grafo#7618

Grafo (Editorial)

El orden topológico lexicográficamente menor se construye con el algoritmo de Kahn con una cola de prioridad: en cada paso, entre todos los vértices disponibles (vértices en los que no entra ninguna arista proveniente de vértices aún no escritos), se elige el vértice con el número más pequeño.

Una arista añadida u \to v hace que el vértice no esté disponible hasta que se escriba el vértice . Así, añadiendo aristas podemos "esconder" vértices pequeños y obligar de este modo al algoritmo a elegir vértices con números mayores. Cada vértice escondido cuesta una arista del presupuesto .

La idea voraz. Construimos la respuesta posición a posición. La comparación lexicográfica se decide en la primera posición en que las secuencias difieren, por lo que en cada posición debemos colocar el vértice con el número mayor posible — ninguna ganancia en posiciones posteriores puede compensar una pérdida en la posición actual.

Dividimos los vértices que pueden escribirse en el paso actual en dos conjuntos:

El conjunto se guarda en una cola de prioridad con el elemento más pequeño en la cima (un min-heap), y el conjunto , en una cola de prioridad con el elemento más grande en la cima (un max-heap).

Cuándo conviene aplazar un vértice. Sea el vértice más pequeño de . Si no lo tocamos, en la posición actual se colocará . Aplazar (gastando una arista) tiene sentido en dos casos:

En todos los demás casos aplazar no aumenta el número en la posición actual, y no hace falta gastar una arista. Aplazamos los vértices de uno en uno, empezando por el más pequeño, mientras convenga y mientras no se agote el presupuesto .

Qué vértice escribir. Después de los aplazamientos:

Después, como es habitual en el algoritmo de Kahn, disminuimos los grados de entrada de los vecinos del vértice escrito; los vértices cuyo grado de entrada pasa a ser cero se añaden a .

Por qué la respuesta es correcta. Todo vértice aplazado tarde o temprano se escribe desde , y en ese momento se añade exactamente una arista. Por lo tanto, el número de aristas añadidas es igual al número de aplazamientos y no supera . Todas las aristas añadidas van de un vértice escrito antes a un vértice escrito después, así que el orden hallado es un orden topológico del nuevo grafo y no aparecen ciclos.

Complejidad. Cada vértice entra en como máximo una vez y en como máximo una vez, y cada arista del grafo se examina una vez. La complejidad total es O((n + m) \log n).

Algoritmo de resolución del problema

Usamos el algoritmo de Kahn y dos colas de prioridad:

Declaramos los siguientes arrays:

Paso 1. Inicialización

  1. Calcular el grado de entrada de cada vértice.

  2. Añadir a todos los vértices con grado de entrada cero.

  3. Inicializar y la lista como vacíos.

Paso 2. Construcción de la respuesta

Mientras no se hayan procesado los vértices:

1. Aplazar un vértice si conviene.

Mientras el número de aristas añadidas sea menor que , el heap no esté vacío y se cumpla al menos una de las siguientes condiciones:

Extraer el vértice más pequeño de e insertarlo en . Este vértice se procesará más tarde.

2. Elegir el siguiente vértice.

Añadir al final de .

3. Actualizar el grafo.

Para cada arista saliente v \to u:

  1. Disminuir indeg[u] en uno.

  2. Si indeg[u] pasa a ser cero, añadir a .

Repetir estos pasos hasta que todos los vértices se hayan añadido a .

Paso 3. Salida

  1. Imprimir la secuencia .

  2. Imprimir el número de aristas añadidas.

  3. Imprimir las propias aristas añadidas.

Ejemplo

Consideremos el ejemplo del enunciado: n = 5, k = 2, aristas 1 \to 4, 4 \to 2, 1 \to 3. Grados de entrada iniciales: los vértices y tienen grado de entrada , los vértices tienen grado de entrada . Los conjuntos iniciales son A = \{1, 5\}, B = \{\,\}.

Paso

Aplazados

Se toma

Arista

Orden

(dos vértices en )

de

—

—

de

5 \to 1

(dos vértices en : y )

de

—

— (k = 0)

de

—

5, 1, 4, 2

—

de

2 \to 3

5, 1, 4, 2, 3

Veamos los pasos con más detalle.

Obtenemos el orden 5, 1, 4, 2, 3 y las aristas añadidas 5 \to 1 y 2 \to 3. El grafo original (a la izquierda) y el grafo con las aristas añadidas (a la derecha, las aristas añadidas se muestran en rojo):

Sin aristas añadidas, el orden lexicográficamente menor es 1, 3, 4, 2, 5: el vértice no está conectado con nada, pero por su número grande se elige el último. Tras añadir dos aristas, el vértice pasa a ser el primero, y el orden 5, 1, 4, 2, 3 es lexicográficamente mayor.

Consideremos un ejemplo más, en el que al principio hay cuatro vértices disponibles. Sean n = 8, m = 6, k = 4, y sean las aristas del grafo 6 \to 3, 4 \to 8, 3 \to 5, 1 \to 7, 2 \to 7, 5 \to 7.

En los vértices 1, 2, 4, 6 no entra ninguna arista, así que al principio A = \{1, 2, 4, 6\}, B = \{\,\}. Sin aristas añadidas, el orden lexicográficamente menor es 1, 2, 4, 6, 3, 5, 7, 8.

El grafo original (a la izquierda) y el grafo tras añadir aristas (a la derecha, las aristas añadidas se muestran en rojo)

Paso

antes

antes

Aplazados

Se toma

Arista

\{1, 2, 4, 6\}

de

—

\{4, 2, 1\}

3~(4 > 3)

de

6 \to 4

\{3, 2, 1\}

—

de

—

\{3, 2, 1\}

—

de

8 \to 3

—

de

—

—

de

5 \to 2

—

de

2 \to 1

—

de

—

Veamos los pasos con más detalle.

La respuesta:

6 4 8 3 5 2 1 7
4
6 4
8 3
5 2
2 1

Este ejemplo muestra claramente todos los casos del algoritmo. En el paso se aplazan tres vértices de una vez — mientras contenga al menos dos vértices. En el paso se aplica la segunda condición: contiene solo el vértice , pero contiene el vértice mayor , así que también se aplaza. Los vértices aplazados salen de en orden decreciente (4, 3, 2, 1), y cada uno de ellos recibe exactamente una arista desde el vértice escrito antes que él. Por tanto, se añaden exactamente tantas aristas como aplazamientos hubo — cuatro.

Implementación del algoritmo

Leemos los datos de entrada. Construimos el grafo. Las listas de adyacencia se guardan en , y los grados de entrada, en .

scanf("%d %d %d", &n, &m, &k);
vector<vector<int>> g(n + 1);
vector<int> indeg(n + 1, 0);
for (int i = 0; i < m; i++)
{
  scanf("%d %d", &u, &v);
  g[u].push_back(v);
  indeg[v]++;
}

Declaramos dos colas de prioridad: — los vértices disponibles, con el elemento más pequeño en la cima (un min-heap); — los vértices aplazados, con el elemento más grande en la cima (un max-heap).

priority_queue<int, vector<int>, greater<int>> A; // disponibles, min-heap
priority_queue<int> B;                            // aplazados, max-heap

Al principio están disponibles todos los vértices con grado de entrada cero.

for (int v = 1; v <= n; v++)
  if (indeg[v] == 0) A.push(v);

El array acumula la respuesta — el orden topológico buscado, y el array guarda las aristas añadidas.

vector<int> order;
order.reserve(n);
vector<pair<int, int>> added;

Cada iteración del bucle añade un vértice a la respuesta, hasta que se hayan escrito los vértices.

while ((int)order.size() < n)
{

Aplazamos vértices. Mientras queden aristas en el presupuesto y el conjunto no esté vacío, consideramos el menor vértice disponible . Conviene aplazarlo si contiene al menos un vértice más o si contiene un vértice aplazado con un número mayor. El vértice aplazado se traslada de a , y el presupuesto disminuye en . En caso contrario, los aplazamientos se detienen.

  // aplazamos vertices mientras convenga y queden aristas
  while (k > 0 && !A.empty())
  {
    int x = A.top();
    if (A.size() >= 2 || (!B.empty() && B.top() > x))
    {
      A.pop();
      B.push(x);
      k--;
    }
    else break;
  }

Elegimos el vértice que se escribirá en la posición actual. Si no está vacío, es el menor vértice disponible. En caso contrario, tomamos el mayor vértice aplazado y añadimos una arista desde el último vértice escrito order.back() hasta .

  int v;
  if (!A.empty())
  {
    v = A.top();
    A.pop();
  }
  else
  {
    v = B.top();
    B.pop();
    added.push_back(make_pair(order.back(), v));
  }

Añadimos a la respuesta y, como en el algoritmo de Kahn, disminuimos los grados de entrada de sus vecinos. Los vértices cuyo grado de entrada pasa a ser cero quedan disponibles.

  order.push_back(v);
  for (int to : g[v])
    if (--indeg[to] == 0) A.push(to);
}

Imprimimos el orden topológico hallado, el número de aristas añadidas y las propias aristas.

for (int i = 0; i < n; i++)
  printf("%d ", order[i]);
printf("\n%d\n", added.size());
for (auto &e : added)
  printf("%d %d\n", e.first, e.second);