Si chiama ponte un arco la cui rimozione aumenta il numero di componenti connesse del grafo. Un arco è un ponte se e solo se non giace su alcun ciclo semplice: se un arco appartiene a un ciclo, dopo la sua rimozione gli estremi dell'arco restano collegati dal resto del ciclo.
Eseguiamo una visita in profondità (DFS). Ogni arco del grafo risulterà essere o un arco dell'albero di visita, oppure un arco all'indietro. Un arco all'indietro, insieme al percorso nell'albero tra i suoi estremi, forma un ciclo, quindi non può essere un ponte. Di conseguenza, solo gli archi dell'albero di visita possono essere ponti.
Durante la visita, per ogni vertice calcoliamo due etichette:
— il tempo di ingresso nel vertice , ossia il numero progressivo con cui il vertice è stato visitato per la prima volta;
— il tempo di ingresso minimo raggiungibile dal vertice , scendendo lungo gli archi dell'albero verso i suoi discendenti e poi percorrendo al più un arco all'indietro.
Consideriamo un arco dell'albero (v, to), dove è figlio del vertice . Sono possibili tre casi:
up[to] < d[v]. Da un qualche vertice del sottoalbero di parte un arco all'indietro verso un antenato proprio del vertice . In altre parole, dal vertice o da un suo discendente esiste un arco all'indietro verso un antenato di se e solo se possiede un figlio tale che up[to] < d[v].
up[to] = d[v]. Il sottoalbero di contiene un arco all'indietro che conduce esattamente al vertice .
up[to] > d[v]. Nessun vertice del sottoalbero di può raggiungere il vertice o i suoi antenati senza passare per l'arco (v, to). Di conseguenza, l'arco (v, to) è un ponte.
Nei primi due casi l'arco (v, to) giace su un ciclo e non è un ponte. Otteniamo così il seguente criterio:
Teorema. Un arco dell'albero di visita (v, to), dove è figlio del vertice , è un ponte se e solo se up[to] > d[v].
La visita in profondità viene eseguita in O(n + m). I numeri dei ponti trovati vengono ordinati prima dell'output.
Esempio
Il grafo riportato nell'esempio ha il seguente aspetto. Accanto a ogni arco è indicato il suo numero nei dati di input:
Eseguiamo una visita in profondità a partire dal vertice . Esamineremo i vicini di ciascun vertice nell'ordine in cui gli archi compaiono nei dati di input. La visita raggiunge i vertici nell'ordine 1, 2, 3, 4, 5, 6. Diventano archi dell'albero di visita gli archi 1, 2, 3, 5 e , mentre gli archi 4 = (3, 1) e 6 = (6, 4) risultano essere archi all'indietro.
Verifichiamo la condizione up[to] > d[v] per tutti gli archi dell'albero:
Arco (v, to) | Numero | Conclusione | ||
1 | 1 | 1 | up[2] = d[1], non è un ponte | |
2 | 1 | 2 | up[3] < d[2], non è un ponte | |
3 | 4 | 3 | up[4] > d[3], ponte | |
5 | 4 | 4 | up[5] = d[4], non è un ponte | |
7 | 4 | 5 | up[6] < d[5], non è un ponte |
Quindi, nel grafo c'è esattamente un ponte — l'arco numero , che collega i vertici e .
Implementazione dell'algoritmo
Il grafo viene memorizzato come lista di adiacenza . Per ogni vertice , in sono registrate coppie (to, id), dove è il vertice adiacente e è il numero dell'arco (v, to) nei dati di input. Il numero dell'arco è necessario per due motivi: per non ripercorrere durante la visita lo stesso arco da cui siamo arrivati, e per restituire i ponti non come coppie di vertici, ma come numeri degli archi di input.
Gli array e contengono le etichette e . Il valore nullo di significa che il vertice non è ancora stato visitato. Nell'array bridges raccogliamo i numeri degli archi che sono ponti.
vector<vector<pair<int, int> > > g;
vector<int> d, up, bridges;La funzione dfs implementa la visita in profondità a partire dal vertice . Il parametro è il numero dell'arco attraverso cui siamo arrivati al vertice . Per la radice dell'albero di visita tale arco non esiste, quindi per default p = -1. All'ingresso nel vertice assegniamo a e il tempo corrente.
void dfs(int v, int p = -1)
{
d[v] = up[v] = timer++;Scorriamo tutti gli archi (v, to) che escono dal vertice . Saltiamo l'arco con numero : è quello attraverso cui siamo arrivati a , e conduce al genitore.
for (auto edge : g[v])
{
int to = edge.first;
int id = edge.second;
if (id == p) continue;Se il vertice è già stato visitato (d[to] \ne 0), allora (v, to) è un arco all'indietro. Ricalcoliamo tenendo conto del tempo di ingresso in .
if (d[to] != 0)
up[v] = min(up[v], d[to]);Altrimenti, (v, to) è un arco dell'albero. Avviamo la visita a partire dal vertice , passando il numero dell'arco . Dopo il ritorno, ricalcoliamo tramite .
else
{
dfs(to, id);
up[v] = min(up[v], up[to]);Se up[to] > d[v], allora dal sottoalbero di non è possibile raggiungere o i suoi antenati senza passare per l'arco (v, to), quindi l'arco con numero è un ponte.
if (up[to] > d[v]) bridges.push_back(id);
}
}
}Parte principale del programma. Leggiamo il grafo in input. Ogni arco viene aggiunto alle liste di adiacenza di entrambi i vertici insieme al suo numero .
scanf("%d %d", &n, &m);
g.resize(n + 1);
for (i = 1; i <= m; i++)
{
scanf("%d %d", &a, &b);
g[a].push_back(make_pair(b, i));
g[b].push_back(make_pair(a, i));
}Inizializziamo gli array delle etichette.
d.resize(n + 1);
up.resize(n + 1);La variabile memorizza il tempo corrente della visita.
timer = 1;Il grafo può essere non connesso, quindi avviamo la visita in profondità da ogni vertice non ancora visitato.
for (i = 1; i <= n; i++)
if (d[i] == 0) dfs(i);Ordiniamo i numeri dei ponti in ordine crescente.
sort(bridges.begin(), bridges.end());Stampiamo il numero di ponti, e nella riga successiva i loro numeri.
printf("%d\n", bridges.size());
for (i = 0; i < bridges.size(); i++)
printf("%d ", bridges[i]);
printf("\n");