Puntos de articulación#1945

Puntos de articulación (Editorial)

Los puntos de articulación se buscan mediante búsqueda en profundidad. Para cada vértice se calculan dos marcas:

Un vértice , que no es la raíz del árbol de recorrido, es un punto de articulación si tiene un hijo en el árbol de recorrido en profundidad, para el cual se cumple la desigualdad up[to] \ge d[v]. Esta desigualdad significa que desde el subárbol con raíz en , siguiendo aristas de retroceso, no se puede subir más arriba que el vértice . Por lo tanto, al eliminar , este subárbol quedará desconectado del resto del grafo.

La raíz del árbol de recorrido es un punto de articulación si y solo si tiene al menos dos hijos en el árbol de recorrido.

Ejemplo

El grafo del ejemplo tiene la siguiente forma:

Las líneas en negrita indican las aristas del árbol de recorrido en profundidad. Junto a cada vértice se indican las marcas d[v] / up[v]. Los puntos de articulación están resaltados en color.

Implementación del algoritmo

El grafo se almacena como una lista de adyacencia . El arreglo marca los vértices visitados, y los arreglos y almacenan las marcas del mismo nombre. La variable contiene el tiempo actual del recorrido en profundidad. Los puntos de articulación encontrados se recolectan en el conjunto : este excluye repeticiones y almacena los vértices en orden ascendente.

vector<vector<int>> g;
vector<int> d, up, used;
set<int> ArtP;

La función dfs realiza la búsqueda en profundidad desde el vértice y, de paso, encuentra los puntos de articulación. El parámetro — es el padre del vértice en el árbol de recorrido. Para la raíz del árbol, p = -1, este es el valor por defecto.

void dfs(int v, int p = -1)
{

Al entrar en el vértice , lo marcamos como visitado. Las marcas y reciben el valor actual del temporizador, después de lo cual el temporizador se incrementa.

  used[v] = true;
  d[v] = up[v] = timer++;

La variable children cuenta los hijos del vértice en el árbol de recorrido; este número será necesario si resulta ser la raíz.

  int children = 0;

Recorremos los vértices adyacentes a . La arista (v, to) puede ser de uno de tres tipos:

  1. una arista del árbol, examinada en dirección inversa, hacia el padre (to = p). Esta arista se omite;

  2. una arista de retroceso (el vértice ya está visitado y to \ne p);

  3. una arista del árbol, que lleva a un vértice aún no visitado.

  for (int to : g[v])
  {
    if (to == p) continue;

Si el vértice ya está visitado, entonces (v, to) — es una arista de retroceso. Recalculamos el valor de .

    if (used[to])
      up[v] = min(up[v], d[to]);   // back edge
    else
    {

De lo contrario, (v, to) — es una arista del árbol. Iniciamos la búsqueda en profundidad desde el vértice , cuyo padre pasa a ser . Después de regresar de la recursión, recalculamos el valor de .

      dfs(to, v);
      up[v] = min(up[v], up[to]);

Si up[to] \ge d[v] y el vértice no es la raíz (p \ne -1), entonces — es un punto de articulación: desde el subárbol con raíz en no se puede subir más arriba que , por lo que eliminar cortará este subárbol del resto del grafo.

      if (up[to] >= d[v] && p != -1) // vértice no raíz
        ArtP.insert(v);

El vértice — es otro hijo de en el árbol de recorrido, incrementamos el contador children.

      children++;
    }
  }

La raíz del árbol de recorrido (p = -1) es un punto de articulación si y solo si tiene más de un hijo.

  if (p == -1 && children > 1)       // raíz del árbol DFS
    ArtP.insert(v);
}

Parte principal del programa. Leemos los datos de entrada.

scanf("%d %d", &n, &m);
g.resize(n + 1);
for (int i = 0; i < m; i++)
{
  scanf("%d %d", &a, &b);

El grafo es no dirigido, por lo que cada arista se añade a las listas de adyacencia de ambos extremos.

  g[a].push_back(b);
  g[b].push_back(a);
}

Reservamos memoria para los arreglos , y .

d.resize(n + 1);
up.resize(n + 1);
used.resize(n + 1);

Inicializamos el temporizador e iniciamos la búsqueda en profundidad desde cada vértice aún no visitado: el grafo puede ser no conexo.

timer = 1;
for (int i = 1; i <= n; i++)
  if (!used[i]) dfs(i);

Mostramos la cantidad de puntos de articulación, y luego los propios vértices. El conjunto está ordenado, por lo que los vértices se muestran en orden ascendente.

printf("%d\n", ArtP.size());
for (int v : ArtP)
  printf("%d\n", v);

Implementación en Java

import java.util.*;

public class Main
{
  static ArrayList<Integer>[] g;
  static int used[], d[], up[];
  static int time;
  static TreeSet<Integer> ArtPoints = new TreeSet<Integer>();
  
  static void dfs (int v, int p)
  {
    int i, to, children;
    used[v] = 1;
    d[v] = up[v] = time++;
    children = 0;
    for (i = 0; i < g[v].size(); i++) 
    {
      to = g[v].get(i);
      if (to == p)  continue;
      if (used[to] == 1)
        up[v] = Math.min(up[v], d[to]);
      else 
      {
        dfs (to, v);
        up[v] = Math.min(up[v], up[to]);
        if ((up[to] >= d[v]) && (p != -1)) ArtPoints.add(v);
        children++;
      }
    }
    if ((p == -1) && (children > 1)) ArtPoints.add(v);
  }

  @SuppressWarnings("unchecked") 
  public static void main(String[] args)
  {
    Scanner con = new Scanner(System.in);
    int n = con.nextInt();
    int m = con.nextInt();
    g = new ArrayList[n+1]; // unchecked
    used = new int[n+1];
    d = new int[n+1];
    up = new int[n+1];

    for (int i = 0; i <= n; i++)
      g[i] = new ArrayList<Integer>();
   
    for (int i = 0; i < m; i++)
    {
      int a = con.nextInt();
      int b = con.nextInt();     
      g[a].add(b);
      g[b].add(a);
    }
    
    time = 1;
    for(int i = 1; i <= n; i++)
      if (used[i] == 0) dfs(i,-1);
    
    System.out.println(ArtPoints.size());
    for(int i : ArtPoints)
      System.out.println(i);
    con.close();
  }
}

Implementación en Python

Aumentemos la profundidad máxima de recursión.

import sys
sys.setrecursionlimit(10**6)

El grafo se almacena como una lista de adyacencia . La lista marca los vértices visitados, y en las listas y se almacenan las marcas del mismo nombre. Los puntos de articulación encontrados se recolectan en el conjunto ArtPoints.

g = []
used = []
d = []
up = []
ArtPoints = set()

La función dfs realiza la búsqueda en profundidad desde el vértice y, de paso, encuentra los puntos de articulación. El parámetro — es el padre del vértice en el árbol de recorrido; para la raíz, p = -1. La variable children cuenta los hijos del vértice en el árbol de recorrido.

def dfs(v, p = -1):
  global t
  children = 0

Al entrar en el vértice , lo marcamos como visitado. Las marcas y reciben el valor actual de , después de lo cual se incrementa.

  used[v] = 1
  d[v] = up[v] = t
  t += 1

Recorremos los vértices adyacentes a . La arista (v, to) puede ser de uno de tres tipos:

  1. una arista del árbol, examinada en dirección inversa, hacia el padre (to = p). Esta arista se omite;

  2. una arista de retroceso (el vértice ya está visitado y to \ne p);

  3. 3. una arista del árbol, que lleva a un vértice aún no visitado.

  for to in g[v]:
    if to == p: continue

Si el vértice ya está visitado, entonces (v, to) — es una arista de retroceso. Recalculamos el valor de .

    if used[to]:
      up[v] = min(up[v], d[to])
    else:

De lo contrario, (v, to) — es una arista del árbol. Iniciamos la búsqueda en profundidad desde el vértice , cuyo padre pasa a ser . Después de regresar de la recursión, recalculamos el valor de .

      dfs(to, v)
      up[v] = min(up[v], up[to])

Si up[to] \ge d[v] y el vértice no es la raíz (p ≠ -1), entonces — es un punto de articulación.

      if up[to] >= d[v] and p != -1:
        ArtPoints.add(v)

El vértice — es otro hijo de en el árbol de recorrido, incrementamos el contador children.

      children += 1

La raíz del árbol de recorrido (p = -1) es un punto de articulación si y solo si tiene más de un hijo.

    if p == -1 and children > 1:
      ArtPoints.add(v)

Parte principal del programa. Creamos las listas y leemos los datos de entrada.

n, m = map(int, input().split())
g = [[] for _ in range(n + 1)]
used = [0] * (n + 1)
d = [0] * (n + 1)
up = [0] * (n + 1)

Leemos las aristas del grafo no dirigido y llenamos las listas de adyacencia.

for _ in range(m):
  a, b = map(int, input().split())
  g[a].append(b)
  g[b].append(a)

Iniciamos la búsqueda en profundidad desde cada vértice aún no visitado: el grafo puede ser no conexo.

t = 1
for i in range(1, n + 1):
  if not used[i]: dfs(i)

Mostramos la cantidad de puntos de articulación, y luego los propios vértices en orden ascendente.

print(len(ArtPoints))
for x in sorted(ArtPoints):
  print(x)