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:
— i vertici disponibili: tutti gli archi entranti in essi sono già stati "rimossi". Se non facciamo nulla, l'algoritmo di Kahn sceglie il vertice più piccolo di ;
— i vertici rinviati. Sono vertici che erano disponibili, ma che abbiamo deciso di nascondere. Per ciascuno di essi ci impegniamo ad aggiungere un arco entrante. L'origine di questo arco la sceglieremo più tardi — sarà il vertice scritto immediatamente prima di quello rinviato.
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:
contiene almeno due vertici. Allora, dopo aver rinviato , nella posizione corrente verrà messo il vertice successivo di in ordine crescente, che è maggiore di ;
contiene solo il vertice , ma contiene un vertice rinviato y > x. Allora, dopo aver rinviato , l'insieme diventa vuoto, e nella posizione corrente verrà messo il più grande vertice rinviato — un numero maggiore di .
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:
se non è vuoto, scriviamo il vertice più piccolo di — questo è richiesto dalla regola dell'ordinamento lessicograficamente minimo;
se è vuoto, scriviamo il vertice rinviato più grande di e aggiungiamo un arco dall'ultimo vertice scritto a . È proprio questo arco che "nascondeva" il vertice : finora era non disponibile, e ora, quando il suo nuovo predecessore è stato scritto, diventa l'unico vertice disponibile. Gli altri vertici rinviati restano nascosti — i loro archi verranno aggiunti più tardi.
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à:
— min-heap che memorizza i vertici disponibili, con il vertice di numero più piccolo in cima.
— max-heap che memorizza i vertici rinviati, con il vertice di numero più grande in cima.
Dichiariamo i seguenti array:
indeg[v] — il numero di archi entranti nel vertice non ancora elaborati;
— l'ordinamento topologico risultante;
— la lista degli archi aggiunti.
Passo 1. Inizializzazione
Calcolare il grado entrante di ogni vertice.
Aggiungere ad tutti i vertici con grado entrante zero.
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:
contiene almeno due vertici;
contiene un solo vertice , e contiene un vertice tale che y > x.
Estrarre il vertice più piccolo da e inserirlo in . Questo vertice verrà elaborato più tardi.
2. Scegliere il vertice successivo.
Se non è vuoto, estrarre da esso il vertice più piccolo .
Altrimenti, estrarre il vertice più grande da . Se non è vuoto, aggiungere un arco dall'ultimo vertice di a .
Aggiungere in fondo ad .
3. Aggiornare il grafo.
Per ogni arco uscente v \to u:
Diminuire indeg[u] di uno.
Se indeg[u] diventa zero, aggiungere ad .
Ripetere questi passi finché tutti i vertici non sono stati aggiunti ad .
Passo 3. Output
Stampare la sequenza .
Stampare il numero di archi aggiunti.
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.
Passo 1. A = \{1, 5\} contiene due vertici, quindi rinviamo il più piccolo — . Ora A = \{5\} contiene un vertice, e non ha vertici maggiori di , quindi smettiamo di rinviare. Scriviamo .
Passo 2. è vuoto. Scriviamo il più grande vertice rinviato e aggiungiamo l'arco 5 \to 1. Rimuoviamo gli archi 1 \to 4 e 1 \to 3: i vertici e diventano disponibili, A = \{3, 4\}.
Passo 3. contiene due vertici — rinviamo , e il budget di archi è esaurito (k = 0). Scriviamo , dopodiché l'arco 4 \to 2 viene rimosso e A = \{2\}.
Passo 4. Non sono possibili altri rinvii. Scriviamo .
Passo 5. è vuoto. Scriviamo il vertice rinviato e aggiungiamo l'arco 2 \to 3.
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.
Passo 1. A = \{1, 2, 4, 6\}, k = 4. Rinviamo il vertice , poiché contiene quattro vertici, poi (tre vertici in ), poi (due vertici in ). Il budget scende a k = 1. Ora A = \{6\} contiene un vertice, e il più grande vertice rinviato è minore di — rinviare ancora non conviene. Scriviamo . L'arco 6 \to 3 viene rimosso, e A = \{3\}.
Passo 2. A = \{3\}, B = \{4, 2, 1\}, k = 1. contiene un vertice, ma contiene il vertice 4 > 3, quindi rinviamo anche ; il budget è esaurito, k = 0. è vuoto — scriviamo il più grande vertice rinviato e aggiungiamo l'arco 6 \to 4. L'arco 4 \to 8 viene rimosso, e A = \{8\}.
Passo 3. Non sono possibili altri rinvii. Scriviamo , dopodiché è vuoto.
Passo 4. Scriviamo il più grande vertice rinviato e aggiungiamo l'arco 8 \to 3. L'arco 3 \to 5 viene rimosso, e A = \{5\}.
Passo 5. Scriviamo . L'arco 5 \to 7 viene rimosso, ma nel vertice entrano ancora archi da e , quindi è vuoto.
Passo 6. Scriviamo il vertice rinviato e aggiungiamo l'arco 5 \to 2. L'arco 2 \to 7 viene rimosso.
Passo 7. Scriviamo il vertice rinviato e aggiungiamo l'arco 2 \to 1. L'arco 1 \to 7 viene rimosso, e A = \{7\}.
Passo 8. Scriviamo .
La risposta:
6 4 8 3 5 2 1 7
4
6 4
8 3
5 2
2 1Questo 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-heapInizialmente 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);