Grafos AvanzadosgrafosSCCKosarajuTarjancomponentes

Componentes Fuertemente Conexas (SCC)

Aprende a encontrar SCCs con los algoritmos de Kosaraju y Tarjan

OOI Oaxaca9 de febrero de 20265 min read

¿Qué es una SCC?

Una Componente Fuertemente Conexa (Strongly Connected Component) es un conjunto maximal de nodos donde puedes llegar de cualquier nodo a cualquier otro siguiendo las aristas dirigidas.

    1 → 2 → 5
    ↑   ↓   ↓
    4 ← 3   6 ↔ 7

SCCs: {1, 2, 3, 4}, {5}, {6, 7}

Algoritmo de Kosaraju

Idea: Usar dos DFS - uno para ordenar por tiempos de finalización, otro en el grafo transpuesto.

const int MAXN = 100005;
vector<int> adj[MAXN];     // Grafo original
vector<int> adjRev[MAXN];  // Grafo transpuesto
bool visitado[MAXN];
stack<int> orden;
int componente[MAXN];

void dfs1(int u) {
    visitado[u] = true;
    for (int v : adj[u]) {
        if (!visitado[v]) {
            dfs1(v);
        }
    }
    orden.push(u);  // Agregar al terminar
}

void dfs2(int u, int comp) {
    componente[u] = comp;
    for (int v : adjRev[u]) {
        if (componente[v] == 0) {
            dfs2(v, comp);
        }
    }
}

int kosaraju(int n) {
    // Paso 1: DFS en grafo original
    fill(visitado, visitado + n + 1, false);
    for (int i = 1; i <= n; i++) {
        if (!visitado[i]) {
            dfs1(i);
        }
    }

    // Paso 2: DFS en grafo transpuesto
    fill(componente, componente + n + 1, 0);
    int numSCC = 0;

    while (!orden.empty()) {
        int u = orden.top();
        orden.pop();

        if (componente[u] == 0) {
            numSCC++;
            dfs2(u, numSCC);
        }
    }

    return numSCC;
}

Algoritmo de Tarjan

Idea: Un solo DFS usando tiempos de descubrimiento y low-link values.

const int MAXN = 100005;
vector<int> adj[MAXN];
int disc[MAXN], low[MAXN], comp[MAXN];
bool enStack[MAXN];
stack<int> st;
int timer = 0, numSCC = 0;

void tarjan(int u) {
    disc[u] = low[u] = ++timer;
    st.push(u);
    enStack[u] = true;

    for (int v : adj[u]) {
        if (disc[v] == 0) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (enStack[v]) {
            low[u] = min(low[u], disc[v]);
        }
    }

    // Si u es raíz de una SCC
    if (low[u] == disc[u]) {
        numSCC++;
        while (true) {
            int v = st.top();
            st.pop();
            enStack[v] = false;
            comp[v] = numSCC;
            if (v == u) break;
        }
    }
}

int encontrarSCCs(int n) {
    fill(disc, disc + n + 1, 0);
    fill(low, low + n + 1, 0);
    fill(comp, comp + n + 1, 0);
    fill(enStack, enStack + n + 1, false);
    timer = numSCC = 0;

    for (int i = 1; i <= n; i++) {
        if (disc[i] == 0) {
            tarjan(i);
        }
    }

    return numSCC;
}

Condensar el grafo

Crear un nuevo grafo donde cada SCC es un nodo:

vector<int> adjCondensado[MAXN];

void condensar(int n) {
    set<pair<int, int>> aristas;

    for (int u = 1; u <= n; u++) {
        for (int v : adj[u]) {
            if (comp[u] != comp[v]) {
                aristas.insert({comp[u], comp[v]});
            }
        }
    }

    for (auto [u, v] : aristas) {
        adjCondensado[u].push_back(v);
    }
}

Aplicaciones

2-SAT

Determinar si una fórmula booleana en forma 2-CNF es satisfacible:

// Para variable x: nodo x representa x, nodo x+n representa ¬x
// Implicación a → b se representa como arista a → b

const int MAXN = 200005;
vector<int> adj[MAXN];
int comp[MAXN];

void agregarClausula(int a, bool negA, int b, bool negB, int n) {
    // (a ∨ b) equivale a (¬a → b) ∧ (¬b → a)
    int nodeA = negA ? a : a + n;
    int nodeNotA = negA ? a + n : a;
    int nodeB = negB ? b : b + n;
    int nodeNotB = negB ? b + n : b;

    adj[nodeNotA].push_back(nodeB);    // ¬a → b
    adj[nodeNotB].push_back(nodeA);    // ¬b → a
}

bool esSatisfacible(int n) {
    encontrarSCCs(2 * n);

    for (int i = 1; i <= n; i++) {
        if (comp[i] == comp[i + n]) {
            return false;  // x y ¬x en la misma SCC
        }
    }
    return true;
}

vector<bool> obtenerSolucion(int n) {
    vector<bool> solucion(n + 1);

    for (int i = 1; i <= n; i++) {
        // Si comp[i] > comp[i+n], entonces x = true
        solucion[i] = comp[i] > comp[i + n];
    }

    return solucion;
}

Template completo (Tarjan)

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
vector<int> adj[MAXN];
int disc[MAXN], low[MAXN], comp[MAXN];
bool onStack[MAXN];
stack<int> st;
int timer, numSCC;

void tarjan(int u) {
    disc[u] = low[u] = ++timer;
    st.push(u);
    onStack[u] = true;

    for (int v : adj[u]) {
        if (!disc[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (onStack[v]) {
            low[u] = min(low[u], disc[v]);
        }
    }

    if (low[u] == disc[u]) {
        numSCC++;
        while (true) {
            int v = st.top();
            st.pop();
            onStack[v] = false;
            comp[v] = numSCC;
            if (v == u) break;
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, m;
    cin >> n >> m;

    for (int i = 0; i < m; i++) {
        int a, b;
        cin >> a >> b;
        adj[a].push_back(b);
    }

    timer = numSCC = 0;
    for (int i = 1; i <= n; i++) {
        if (!disc[i]) tarjan(i);
    }

    cout << numSCC << "\n";
    for (int i = 1; i <= n; i++) {
        cout << comp[i] << " ";
    }
    cout << "\n";

    return 0;
}

Ejercicios recomendados

  1. CSES - Planets and Kingdoms
  2. CSES - Giant Pizza (2-SAT)
  3. Codeforces - Scheme
  4. SPOJ - CAPCITY