Un puente es una arista cuya eliminación aumenta el número de componentes conexas del grafo. Una arista es un puente si y solo si no se encuentra en ningún ciclo simple: si una arista pertenece a un ciclo, entonces, tras su eliminación, los extremos de la arista permanecen conectados por el resto del ciclo.
Ejecutemos una búsqueda en profundidad. Cada arista del grafo resultará ser una arista del árbol de búsqueda o una arista de retroceso. Una arista de retroceso, junto con el camino por el árbol entre sus extremos, forma un ciclo, por lo que no puede ser un puente. Por lo tanto, solo las aristas del árbol de búsqueda pueden ser puentes.
Durante la búsqueda, para cada vértice calcularemos dos etiquetas:
— el tiempo de entrada al vértice , es decir, el número de orden con el que el vértice fue visitado por primera vez;
— el menor tiempo de entrada que se puede alcanzar desde el vértice , descendiendo por las aristas del árbol hacia sus descendientes y luego pasando por, como máximo, una arista de retroceso.
Consideremos una arista del árbol (v, to), donde es hijo del vértice . Son posibles tres casos:
up[to] < d[v]. Desde algún vértice del subárbol parte una arista de retroceso hacia un ancestro propio del vértice . Dicho de otro modo, desde el vértice o alguno de sus descendientes existe una arista de retroceso hacia un ancestro de si y solo si tiene un hijo tal que up[to] < d[v].
up[to] = d[v]. El subárbol contiene una arista de retroceso que lleva exactamente al vértice .
up[to] > d[v]. Ningún vértice del subárbol puede alcanzar al vértice ni a sus ancestros sin pasar por la arista (v, to). Por lo tanto, la arista (v, to) es un puente.
En los dos primeros casos, la arista (v, to) se encuentra en un ciclo y no es un puente. De este modo, obtenemos el criterio:
Teorema. Una arista del árbol de búsqueda (v, to), donde es hijo del vértice , es un puente si y solo si up[to] > d[v].
La búsqueda en profundidad se ejecuta en O(n + m). Los números de los puentes encontrados se ordenan antes de mostrarlos.
Ejemplo
El grafo presentado en el ejemplo tiene el siguiente aspecto. Junto a cada arista se indica su número en los datos de entrada:
Ejecutemos la búsqueda en profundidad desde el vértice . Recorreremos los vecinos de cada vértice en el orden en que aparecen las aristas en los datos de entrada. El recorrido visita los vértices en el orden 1, 2, 3, 4, 5, 6. Las aristas del árbol de búsqueda resultan ser las aristas 1, 2, 3, 5 y , mientras que las aristas 4 = (3, 1) y 6 = (6, 4) resultan ser de retroceso.
Comprobemos la condición up[to] > d[v] para todas las aristas del árbol:
Arista (v, to) | Número | Resultado | ||
1 | 1 | 1 | up[2] = d[1], no es puente | |
2 | 1 | 2 | up[3] < d[2], no es puente | |
3 | 4 | 3 | up[4] > d[3], puente | |
5 | 4 | 4 | up[5] = d[4], no es puente | |
7 | 4 | 5 | up[6] < d[5], no es puente |
Por lo tanto, en el grafo hay exactamente un puente: la arista número , que une los vértices y .
Implementación del algoritmo
El grafo se almacena como una lista de adyacencia . Para cada vértice , en se guardan pares (to, id), donde es el vértice vecino e es el número de la arista (v, to) en los datos de entrada. El número de la arista es necesario por dos razones: para no retroceder durante el recorrido por la misma arista por la que se llegó, y para mostrar los puentes no como pares de vértices, sino como números de las aristas de entrada.
Los arreglos y contienen las etiquetas y . Un valor cero de significa que el vértice aún no ha sido visitado. En el arreglo bridges recopilamos los números de las aristas que son puentes.
vector<vector<pair<int, int> > > g;
vector<int> d, up, bridges;La función dfs implementa la búsqueda en profundidad desde el vértice . El parámetro es el número de la arista por la cual llegamos al vértice . Para la raíz del árbol de búsqueda no existe tal arista, por lo que por defecto p = -1. Al entrar en un vértice, asignamos a y el tiempo actual.
void dfs(int v, int p = -1)
{
d[v] = up[v] = timer++;Recorremos todas las aristas (v, to) que salen del vértice . La arista con número se omite: por ella llegamos a , y conduce al padre.
for (auto edge : g[v])
{
int to = edge.first;
int id = edge.second;
if (id == p) continue;Si el vértice ya ha sido visitado (d[to] \ne 0), entonces (v, to) es una arista de retroceso. Recalculamos teniendo en cuenta el tiempo de entrada a .
if (d[to] != 0)
up[v] = min(up[v], d[to]);En caso contrario, (v, to) es una arista del árbol. Lanzamos la búsqueda desde el vértice , pasando el número de arista . Tras el retorno, recalculamos a través de .
else
{
dfs(to, id);
up[v] = min(up[v], up[to]);Si up[to] > d[v], entonces desde el subárbol no se puede llegar a ni a sus ancestros sin pasar por la arista (v, to), por lo que la arista con número es un puente.
if (up[to] > d[v]) bridges.push_back(id);
}
}
}Parte principal del programa. Leemos el grafo de entrada. Cada arista se añade a las listas de adyacencia de ambos vértices junto con su número .
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));
}Inicializamos los arreglos de etiquetas.
d.resize(n + 1);
up.resize(n + 1);La variable almacena el tiempo actual de la búsqueda.
timer = 1;El grafo puede ser no conexo, por lo que ejecutamos la búsqueda en profundidad desde cada vértice aún no visitado.
for (i = 1; i <= n; i++)
if (d[i] == 0) dfs(i);Ordenamos los números de los puentes en orden ascendente.
sort(bridges.begin(), bridges.end());Mostramos la cantidad de puentes y, en la siguiente línea, sus números.
printf("%d\n", bridges.size());
for (i = 0; i < bridges.size(); i++)
printf("%d ", bridges[i]);
printf("\n");