Grafo#7618

Grafo (Editoriale)

L'ordinamento topologico lessicograficamente minimo si costruisce con l'algoritmo di Kahn con una coda con priorità: a ogni passo, tra tutti i vertici disponibili (vertici in cui non entra alcun arco proveniente da vertici non ancora scritti), si sceglie il vertice con il numero più piccolo.

Un arco aggiunto u \to v rende il vertice non disponibile finché non viene scritto il vertice . Quindi, aggiungendo archi, possiamo "nascondere" i vertici piccoli e costringere così l'algoritmo a scegliere vertici con numeri più grandi. Ogni vertice nascosto costa un arco del budget .

L'idea greedy. Costruiamo la risposta posizione per posizione. Il confronto lessicografico è deciso dalla prima posizione in cui le sequenze differiscono, quindi in ogni posizione dobbiamo mettere il vertice con il numero più grande possibile — nessun guadagno nelle posizioni successive può compensare una perdita in quella corrente.

Dividiamo i vertici che possono essere scritti al passo corrente in due insiemi:

L'insieme è memorizzato in una coda con priorità con l'elemento più piccolo in cima (un min-heap), e l'insieme in una coda con priorità con l'elemento più grande in cima (un max-heap).

Quando conviene rinviare un vertice. Sia il vertice più piccolo di . Se lo lasciamo stare, nella posizione corrente verrà messo . Rinviare (spendendo un arco) ha senso in due casi:

In tutti gli altri casi il rinvio non aumenta il numero nella posizione corrente, e non c'è bisogno di spendere un arco. Rinviamo i vertici uno alla volta, a partire dal più piccolo, finché conviene e finché il budget non è esaurito.

Quale vertice scrivere. Dopo i rinvii:

Poi, come al solito nell'algoritmo di Kahn, diminuiamo i gradi entranti dei vicini del vertice scritto; i vertici il cui grado entrante diventa zero vengono aggiunti ad .

Perché la risposta è corretta. Ogni vertice rinviato prima o poi viene scritto da , e in quel momento viene aggiunto esattamente un arco. Quindi il numero di archi aggiunti è uguale al numero di rinvii e non supera . Tutti gli archi aggiunti vanno da un vertice scritto prima a un vertice scritto dopo, perciò l'ordine trovato è un ordinamento topologico del nuovo grafo e non si formano cicli.

Complessità. Ogni vertice entra in al più una volta e in al più una volta, e ogni arco del grafo viene esaminato una volta. La complessità totale è O((n + m) \log n).

Algoritmo di risoluzione del problema

Usiamo l'algoritmo di Kahn e due code con priorità:

Dichiariamo i seguenti array:

Passo 1. Inizializzazione

  1. Calcolare il grado entrante di ogni vertice.

  2. Aggiungere ad tutti i vertici con grado entrante zero.

  3. Inizializzare e la lista come vuoti.

Passo 2. Costruzione della risposta

Finché non sono stati elaborati tutti gli vertici:

1. Rinviare un vertice se conviene.

Finché il numero di archi aggiunti è minore di , lo heap non è vuoto e vale almeno una delle seguenti condizioni:

Estrarre il vertice più piccolo da e inserirlo in . Questo vertice verrà elaborato più tardi.

2. Scegliere il vertice successivo.

Aggiungere in fondo ad .

3. Aggiornare il grafo.

Per ogni arco uscente v \to u:

  1. Diminuire indeg[u] di uno.

  2. Se indeg[u] diventa zero, aggiungere ad .

Ripetere questi passi finché tutti i vertici non sono stati aggiunti ad .

Passo 3. Output

  1. Stampare la sequenza .

  2. Stampare il numero di archi aggiunti.

  3. Stampare gli archi aggiunti.

Esempio

Consideriamo l'esempio del testo: n = 5, k = 2, archi 1 \to 4, 4 \to 2, 1 \to 3. Gradi entranti iniziali: i vertici e hanno grado entrante , i vertici hanno grado entrante . Gli insiemi iniziali sono A = \{1, 5\}, B = \{\,\}.

Passo

Rinviati

Preso

Arco

Ordine

(due vertici in )

da

—

—

da

5 \to 1

(due vertici in : e )

da

—

— (k = 0)

da

—

5, 1, 4, 2

—

da

2 \to 3

5, 1, 4, 2, 3

Esaminiamo i passi più nel dettaglio.

Otteniamo l'ordine 5, 1, 4, 2, 3 e gli archi aggiunti 5 \to 1 e 2 \to 3. Il grafo originale (a sinistra) e il grafo con gli archi aggiunti (a destra, gli archi aggiunti sono in rosso):

Senza archi aggiunti l'ordinamento lessicograficamente minimo è 1, 3, 4, 2, 5: il vertice non è collegato a nulla, ma a causa del suo numero grande viene scelto per ultimo. Dopo l'aggiunta di due archi, il vertice viene per primo, e l'ordine 5, 1, 4, 2, 3 è lessicograficamente maggiore.

Consideriamo un altro esempio, in cui all'inizio sono disponibili quattro vertici. Siano n = 8, m = 6, k = 4, e siano gli archi del grafo 6 \to 3, 4 \to 8, 3 \to 5, 1 \to 7, 2 \to 7, 5 \to 7.

Nei vertici 1, 2, 4, 6 non entra alcun arco, quindi all'inizio A = \{1, 2, 4, 6\}, B = \{\,\}. Senza archi aggiunti l'ordinamento lessicograficamente minimo è 1, 2, 4, 6, 3, 5, 7, 8.

Il grafo originale (a sinistra) e il grafo dopo l'aggiunta degli archi (a destra, gli archi aggiunti sono in rosso)

Passo

prima

prima

Rinviati

Preso

Arco

\{1, 2, 4, 6\}

da

—

\{4, 2, 1\}

3~(4 > 3)

da

6 \to 4

\{3, 2, 1\}

—

da

—

\{3, 2, 1\}

—

da

8 \to 3

—

da

—

—

da

5 \to 2

—

da

2 \to 1

—

da

—

Esaminiamo i passi più nel dettaglio.

La risposta:

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

Questo esempio mostra chiaramente tutti i casi dell'algoritmo. Al passo vengono rinviati tre vertici in una volta — finché contiene almeno due vertici. Al passo si applica la seconda condizione: contiene solo il vertice , ma contiene il vertice più grande , quindi anche viene rinviato. I vertici rinviati escono da in ordine decrescente (4, 3, 2, 1), e ciascuno di essi riceve esattamente un arco dal vertice scritto prima di esso. Quindi vengono aggiunti esattamente tanti archi quanti sono stati i rinvii — quattro.

Implementazione dell'algoritmo

Leggiamo i dati di input. Costruiamo il grafo. Le liste di adiacenza sono memorizzate in , i gradi entranti in .

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]++;
}

Dichiariamo due code con priorità: — i vertici disponibili, con l'elemento più piccolo in cima (un min-heap); — i vertici rinviati, con l'elemento più grande in cima (un max-heap).

priority_queue<int, vector<int>, greater<int>> A; // disponibili, min-heap
priority_queue<int> B;                            // rinviati, max-heap

Inizialmente sono disponibili tutti i vertici con grado entrante zero.

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

L'array accumula la risposta — l'ordinamento topologico richiesto, e l'array memorizza gli archi aggiunti.

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

Ogni iterazione del ciclo aggiunge un vertice alla risposta, finché non sono stati scritti tutti gli vertici.

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

Rinviamo i vertici. Finché nel budget restano archi e l'insieme non è vuoto, consideriamo il più piccolo vertice disponibile . Conviene rinviarlo se contiene almeno un altro vertice oppure se contiene un vertice rinviato con numero più grande. Un vertice rinviato viene spostato da a , e il budget diminuisce di . Altrimenti i rinvii si interrompono.

  // rinviamo i vertici finché conviene e restano archi
  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;
  }

Scegliamo il vertice da scrivere nella posizione corrente. Se non è vuoto, è il più piccolo vertice disponibile. Altrimenti prendiamo il più grande vertice rinviato e aggiungiamo un arco dall'ultimo vertice scritto order.back() a .

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

Aggiungiamo alla risposta e, come nell'algoritmo di Kahn, diminuiamo i gradi entranti dei suoi vicini. I vertici il cui grado entrante diventa zero diventano disponibili.

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

Stampiamo l'ordinamento topologico trovato, il numero di archi aggiunti e gli archi stessi.

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);