Connessione forte#2403

Connessione forte (Editoriale)

Il problema richiede di trovare il numero di componenti fortemente connesse di un grafo diretto. Per fare questo, utilizziamo l'algoritmo di Kosaraju, che esegue due visite in profondità:

Il grafo inverso ha le stesse componenti fortemente connesse del grafo originale: se esistono cammini da a e da a , allora dopo l'inversione degli archi questi cammini vengono preservati, cambiando solo la loro direzione. L'ordine di elaborazione dei vertici nella seconda visita garantisce che la visita in profondità non esca dalla componente corrente: tutte le componenti che potrebbe raggiungere sono già state visitate a questo punto.

La risposta è il numero di avvii della visita in profondità nella seconda visita.

Esempio

I grafi forniti negli esempi sono mostrati di seguito. Le componenti fortemente connesse sono evidenziate con colori diversi.

Il primo grafo non contiene cicli: è impossibile tornare a un qualsiasi vertice. Pertanto, ogni vertice forma una componente fortemente connessa separata: , , .

Nel secondo grafo, i vertici , e si trovano sul ciclo 1 \to 2 \to 3 \to 1 e formano un'unica componente \{1, 2, 3\}. Il vertice non può essere raggiunto dal vertice , quindi i vertici e formano componenti separate e . Pertanto, ciascuno dei grafi contiene componenti fortemente connesse.

Simuliamo l'algoritmo sul secondo grafo. La prima visita in profondità dal vertice segue il cammino 1 \to 2 \to 3, e poi dal vertice il cammino 4 \to 5. L'ordine in cui termina l'elaborazione dei vertici è top = (3, 2, 1, 5, 4). Dopo l'inversione, otteniamo (4, 5, 1, 2, 3).

Il grafo inverso contiene gli archi 2 \to 1, 3 \to 2, 1 \to 3 e 5 \to 4. Eseguiamo la seconda visita nell'ordine (4, 5, 1, 2, 3):

I vertici e sono già stati visitati, quindi non ci sono nuovi avvii. Otteniamo c = 3 componenti.

Si noti che nel grafo inverso il vertice , che appartiene a un'altra componente, è raggiungibile dal vertice . Tuttavia, il vertice viene elaborato prima del vertice , quindi nel momento in cui viene chiamata esso è già stato visitato e non entra a far parte della componente del vertice .

Implementazione dell'algoritmo

Memorizziamo il grafo di input come lista di adiacenza , e il grafo inverso come lista di adiacenza . L'array segna i vertici visitati. L'array memorizza i vertici nell'ordine in cui termina la loro elaborazione durante la prima visita in profondità.

vector<vector<int> > g, gr;
vector<int> used, top;

La funzione dfs1 esegue una visita in profondità sul grafo di input. Il vertice viene aggiunto alla fine dell'array dopo che tutti i vertici raggiungibili da esso sono stati elaborati, cioè nel momento in cui lo si lascia.

void dfs1(int v)
{
  used[v] = 1;
  for (int to : g[v])
    if (!used[to]) dfs1(to);
  top.push_back(v);
}

La funzione dfs2 esegue una visita in profondità sul grafo inverso. Tutti i vertici visitati durante una chiamata di dal programma principale formano una componente fortemente connessa.

void dfs2(int v)
{
  used[v] = 1;
  for (int to : gr[v])
    if (!used[to]) dfs2(to);
}

Parte principale del programma. Leggiamo il grafo di input. Aggiungiamo ogni arco a \to b al grafo , e l'arco opposto b \to a al grafo inverso .

scanf("%d %d", &n, &m);
g.resize(n + 1);
gr.resize(n + 1);
for(i = 1; i <= m; i++)
{
  scanf("%d %d", &a, &b);
  g[a].push_back(b);
  gr[b].push_back(a);
}

Avviamo una visita in profondità sul grafo di input a partire da ogni vertice non ancora visitato. Dopo di ciò, l'array contiene tutti i vertici del grafo nell'ordine in cui è terminata la loro elaborazione.

used.resize(n+1);
for(i = 1; i <= n; i++)
  if (!used[i]) dfs1(i);

Prima della seconda visita, azzeriamo l'array . I vertici del grafo inverso devono essere considerati nell'ordine inverso rispetto al termine della loro elaborazione, cioè l'array deve essere scorso dalla fine all'inizio. Per comodità, invertiamo l'array .

used.assign(n + 1, 0);
reverse(top.begin(), top.end());

La variabile conta il numero di componenti fortemente connesse.

c = 0;

Scorriamo l'array da sinistra a destra. Da ogni vertice non ancora visitato, avviamo la visita in profondità dfs2 sul grafo inverso. Essa attraversa esattamente una nuova componente fortemente connessa, quindi incrementiamo di .

for (int v : top)
  if (!used[v])
  {
    dfs2(v);
    c++;
  }

Stampiamo il numero di componenti fortemente connesse del grafo.

printf("%d\n",c);

Implementazione in Java

import java.util.*;

public class Main
{
  static ArrayList<Integer>[] g, gr;
  static boolean used[];
  static Vector<Integer> top = new Vector<Integer>();

  static void dfs1(int v)
  {
    used[v] = true;
    for(int i = 0; i < g[v].size(); i++)
    {
      int to = g[v].get(i);
      if (!used[to]) dfs1(to);
    }
    top.add(v);
  }

  static void dfs2(int v)
  {
    used[v] = true;
    for(int i = 0; i < gr[v].size(); i++)
    {
      int to = gr[v].get(i);
      if (!used[to]) dfs2(to);
    }
  }

  @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];
    for(int i = 0; i <= n; i++)
      g[i] = new ArrayList<Integer>();
    gr = new ArrayList[n+1];
    for(int i = 0; i <= n; i++)
      gr[i] = new ArrayList<Integer>();

    for (int i = 0; i < m; i++)
    {
      int a = con.nextInt();
      int b = con.nextInt();
      g[a].add(b);
      gr[b].add(a);
    }
    used = new boolean[n+1];
    for(int i = 1; i <= n; i++)
      if (!used[i]) dfs1(i);

    used = new boolean[n+1];
    int c = 0;
    for(int i = 1; i <= n; i++)
    {
      int v = top.get(n-i);
      if (!used[v])
      {
        dfs2(v);
        c++;
      }
    }

    System.out.println(c);
    con.close();
  }
}

Implementazione in Python

La funzione dfs1 esegue una visita in profondità sul grafo di input. Il vertice viene aggiunto alla fine della lista dopo che tutti i vertici raggiungibili da esso sono stati elaborati, cioè nel momento in cui lo si lascia.

def dfs1(v):
  global g, used, top
  used[v] = True
  for to in g[v]:
    if not used[to]: dfs1(to)
  top.append(v)

La funzione dfs2 esegue una visita in profondità sul grafo inverso. Tutti i vertici visitati durante una chiamata di dal programma principale formano una componente fortemente connessa.

def dfs2(v):
  global gr, used
  used[v] = True
  for to in gr[v]:
    if not used[to]: dfs2(to)

Parte principale del programma. Leggiamo il grafo di input. Aggiungiamo ogni arco a \to b al grafo , e l'arco opposto b \to a al grafo inverso .

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

Avviamo una visita in profondità sul grafo di input a partire da ogni vertice non ancora visitato. Dopo di ciò, la lista contiene tutti i vertici del grafo nell'ordine in cui è terminata la loro elaborazione.

used = [False] * (n + 1)
top = []
for i in range(1, n + 1):
  if not used[i]: dfs1(i)

Prima della seconda visita, azzeriamo la lista . I vertici del grafo inverso devono essere considerati nell'ordine inverso rispetto al termine della loro elaborazione, quindi invertiamo la lista .

used = [False] * (n + 1)
top.reverse()

La variabile conta il numero di componenti fortemente connesse.

c = 0

Scorriamo la lista da sinistra a destra. Da ogni vertice non ancora visitato, avviamo la visita in profondità dfs2 sul grafo inverso. Essa attraversa esattamente una nuova componente fortemente connessa, quindi incrementiamo di .

for v in top:
  if not used[v]:
    dfs2(v)
    c += 1

Stampiamo il numero di componenti fortemente connesse del grafo.

print(c)