Fundamentos: Vazamento e Ordenação
Para compreender algoritmos de decomposição em grafos, é essencial definir dois vetores principais durante uma travessia por profundidade (DFS): o vetor dfsOrder (ou discovery time) e o vetor minReach (frequentemente chamado de low link).
- dfsOrder: Representa o momento temporal exato em que um vértice é visitado pela primeira vez na árvore de DFS.
- minReach: Armazena o menor valor de
dfsOrderalcançável a partir daquele nó através de uma arista de retorno (back-edge) dentro da árvore de DFS.
Um elemento da pilha de travessia é fundamental para rastrear os componentes atuais sendo explorados. Ao iniciar a recursão no vértice atual u, inicializamos seus tempos de visita e baixa visita. A lógica difere dependendo se o nó adjacente v já foi processado ou se está ativo na pilha.
Algoritmo de Condensação de Componentes Fortemente Conexos
O objetivo deste método é identificar grupos de vértices onde cada par de nós possui um caminho cíclico entre si. Durante o processo recursivo:
- Se o vizinho
vainda não foi visitado, eniciamos a busca recursiva nele. Após o retorno, atualizamosminReach[u]com o mínimo entre seu valor atual eminReach[v]. - Se
vjá foi visitado e permanece empilhado na estrutura de dados, significa que ele é um ancestor ou parte de um ciclo. Portanto, atualizamosminReach[u]com odfsOrder[v].
Quando a condição minReach[u] == dfsOrder[u] ocorre, identificamos a raiz de um novo componente forte. Todos os nós acima dele na pilha pertencem ao mesmo grupo e devem ser removidos e rotulados.
void encontrarSCC(int u) {
dfn[u] = minRach[u] = ++tempoGlobal;
empilhar.push(u);
marcada[u] = true;
for (int i = primeiroNo[u]; i != 0; i = proximaAresta[i]) {
int vizinho = arr[vezinhos].destino;
if (!dfn[vizinho]) {
encontrarSCC(vizinho);
minRach[u] = std::min(minRach[u], minRach[vizinho]);
} else if (marcada[vizinho]) {
minRach[u] = std::min(minRach[u], dfn[vizinho]);
}
}
if (minRach[u] == dfn[u]) {
int topo = empilhar.top();
empilhar.pop();
marcado[topo] = false;
compID[topo] = contadorSCC;
while (topo != u) {
topo = empilhar.top();
empilhar.pop();
marcado[topo] = false;
compID[topo] = contadorSCC++;
}
}
}
Pontos de Corte e Pontes em Grafos Não-Direcionados
No contexto de grafos não-direcionados, definimos novos critérios. O conceito de minReach muda ligeiramente para indicar o menor número de visita alcançável sem passar pelo pai direto da árvore de DFS.
A condição clássica para um nó u (não raiz) ser um ponto de corte é minReach[v] >= dfn[u] para algum filho v. Isso implica que remover u desconecta v das partes anteriores da árvore. Para a raiz da DFS, ela é ponto de corte apenas se possuir mais de um filho imediato.
// Identificação de pontos de corte e pontes
void analisarConectividade(int u, int pai) {
dfn[u] = minRach[u] = ++tempoGlobal;
int filhosImediatos = 0;
for (int i = primeiroNo[u]; i != 0; i = proximaAresta[i]) {
int v = arr[vezinhos].destino;
if (v != pai) { // Ignora a aresta de retorno direta para o pai
if (!dfn[v]) {
filhosImediatos++;
analisarConectividade(v, u);
minRach[u] = std::min(minRach[u], minRach[v]);
// Critério para Ponto de Corte
if (pai != 0 && minRach[v] >= dfn[u]) {
isCorte[u] = true;
}
// Critério para Ponte (Estritamente maior)
if (minRach[v] > dfn[u]) {
pontesExistentes++;
}
} else {
minRach[u] = std::min(minRach[u], dfn[v]);
}
}
}
// Caso especial para a Raiz
if (pai == 0 && filhosImediatos >= 2) {
isCorte[u] = true;
}
}
Diferença entre Conectividade de Aresta e de Vértice
Dois vértices são considerados Bi-conexos por Aresta se, ao remover qualquer única aresta, eles permanecem conectados. Já a Bi-conexão por Vértice exige que eles permaneçam conectados mesmo após a remoção de qualquer único vértice intermediário.
A extração dos componentes bi-conexos pode ser feita simultaneamente à detecção de cortes usando uma pilha adicional. Quando detectamos minReach[v] >= dfn[u], sabemos que formamos um novo componente. Extrai-se os nós da pilha até encontrar v, mantendo u fora pois ele atua como conector entre múltiplos componentes.
Solução para Problemas de Escapamento (Minas)
Um cenário comum envolve determinar quantas saídas seguras são necessárias em diferentes componentes para garantir a fuga, considerando a possível falha de algumas estações (cortes). Se um componente Bi-conexo não contiver nenhum ponto de corte, duas saídas são ideais. Se tiver exatamente um ponto de corte, uma saída dentro do componente (diferente do corte) basta. O cálculo total combina essas possibilidades multiplicativas entre os componentes.
const int MAXN = 500005;
int n, m, cntEdges = 0;
int dfn[MAXN], low[MAXN], tempo = 0;
int pilha[MAXN], topoPilha = 0;
bool isCorte[MAXN];
std::vector<int> componentList[MAXN];
int dccCount = 0;
struct Aresta {
int v, nxt;
};
Aresta arestas[MAXN << 2];
int primeira[MAXD];
void add(int u, int v) {
arestas[++cntEdges] = {v, primeira[u]};
primeira[u] = cntEdges;
}
void tarjanBCC(int u, int p) {
dfn[u] = low[u] = ++tempo;
pilha[++topoPilha] = u;
bool temFilho = false;
int filhoCnt = 0;
for (int e = primeira[u]; e != 0; e = arestas[e].nxt) {
int v = arestas[e].v;
if (v == p) continue;
if (!dfn[v]) {
filhoCnt++;
tarjanBCC(v, u);
low[u] = std::min(low[u], low[v]);
if (low[v] >= dfn[u]) {
dccCount++;
int cur = pilha[topoPilha--];
do {
componentList[dccCount].push_back(cur);
if (cur == v) break;
cur = pilha[topoPilha--];
} while (false);
componentList[dccCount].push_back(u);
}
if (low[v] >= dfn[u] && p != 0) {
isCorte[u] = true;
}
temFilho = true;
} else {
low[u] = std::min(low[u], dfn[v]);
}
}
// Verificação para nó raiz
if (!p && !temFilho) {
dccCount++;
componentList[dccCount].push_back(u);
}
if (!p && filhoCnt >= 2) isCorte[u] = true;
}
int main() {
// Leitura de N e M
scanf("%d %d", &n, &m);
// Inicialização...
rep(i, 1, m) {
int u, v; scanf("%d %d", &u, &v);
add(u, v); add(v, u);
}
for(int i=1; i<=n; i++) {
if(!dfn[i]) tarjanBCC(i, 0);
}
// Lógica de contagem final baseada nos componentes
return 0;
}
</int>
Implementação Completa para Pontes
Para localizar especificamente as pontes (edges cuja remoção aumenta o número de componentes conexos), a verificação é mais restritiva. Apenas as arestas tree onde low[v] > dfn[u] são pontes reais. Arredondando esse algoritmo, podemos usar uma segunda busca (DFS) sobre os componentes restantes sem pontes para agrupar nós equivalentes.
void dfsPonte(int u, int rootId) {
visitedComp[u] = rootId;
for (int e = primeira[u]; e; e = arestas[e].nxt) {
int v = arestas[e].v;
if (isPonte[e]) continue; // Pula arestas bridge
if (!visitedComp[v]) {
dfsPonte(v, rootId);
}
}
}
Considerações Finais
A eficiência dessas técnicas reside na linearidade O(V+E), permitindo aplicações complexas em redes de grande escala onde a análise de redundância e resiliência é crítica.