Percursos e componentes em grafos

BFS, DFS, ordenação topológica, bipartição e componentes fortemente conexas com Kosaraju e Tarjan.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Num grafo dirigido com arestas 0→10\to1, 0→20\to2, 1→31\to3, 2→32\to3 e 3→43\to4, existem dois caminhos de 0 até 4, ambos com três arestas. A pesquisa em largura encontra um deles ao expandir os vértices por distância crescente. A pesquisa em profundidade segue primeiro um ramo até ao fim e usa o regresso para analisar ciclos e dependências. A ordem de visita e o estado guardado durante cada pesquisa servem depois para ordenar tarefas e para separar regiões onde todos os vértices se alcançam mutuamente.

Esta lição assume grafos dirigidos salvo indicação em contrário e usa n=∣V∣n=|V| vértices e m=∣E∣m=|E| arestas. Os custos indicados valem para listas de adjacência.

O vídeo de Rodrigo Dias reúne grafos, força bruta, gulosos e divisão e conquista, como apoio à revisão dessas estratégias.

Abrir no YouTube

BFS e caminhos sem pesos

A pesquisa em largura, BFS, mantém uma fila. Começa com d[s]=0d[s]=0, marca ss e coloca-o na fila. Quando retira uu, descobre cada vizinho ainda não marcado, atribui d[v]=d[u]+1d[v]=d[u]+1, guarda pai[v]=u e coloca vv na fila.

Marca um vértice quando o inseres, para que duas arestas não o insiram duas vezes. A fila processa primeiro todos os vértices de distância 0, depois os de distância 1, e assim sucessivamente, por isso quando descobre vv pela primeira vez nenhum caminho mais curto ficou por explorar. Logo d[v]d[v] é o mínimo número de arestas de um caminho de ss a vv.

Com arestas 0→10\to1, 0→20\to2, 1→31\to3, 2→32\to3, 3→43\to4, a fila evolui assim, supondo vizinhos por ordem numérica:

RetiradoFila depois de descobrir vizinhosNova informação
01, 2d[1]=d[2]=1d[1]=d[2]=1
12, 3d[3]=2d[3]=2, pai de 3 é 1
233 já foi descoberto
34d[4]=3d[4]=3
4vaziaTermina

O caminho reconstruído é 0,1,3,40,1,3,4. 0,2,3,40,2,3,4 também é mínimo; a ordem de adjacência desempata. Um vértice isolado 5 mantém distância desconhecida e não tem caminho. Não lhe atribuas distância zero.

O programa lê n m s, seguido de m pares dirigidos u v. Os vértices vão de 0 a n-1; aceita até 2000 vértices e 20000 arestas.

BFS com distância e predecessores
#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>

int main() {
    int n, m, s;
    if (!(std::cin >> n >> m >> s) || n < 1 || n > 2000 ||
        m < 0 || m > 20000 || s < 0 || s >= n) {
        std::cout << "entrada invalida\n";
        return 0;
    }
    std::vector<std::vector<int>> adj(n);
    for (int i = 0; i < m; ++i) {
        int u, v;
        if (!(std::cin >> u >> v) || u < 0 || u >= n || v < 0 || v >= n) {
            std::cout << "entrada invalida\n";
            return 0;
        }
        adj[u].push_back(v);
    }
    std::vector<int> dist(n, -1), pai(n, -1);
    std::queue<int> fila;
    dist[s] = 0;
    fila.push(s);
    while (!fila.empty()) {
        int u = fila.front();
        fila.pop();
        for (int v : adj[u]) {
            if (dist[v] != -1) continue;
            dist[v] = dist[u] + 1;
            pai[v] = u;
            fila.push(v);
        }
    }
    for (int v = 0; v < n; ++v)
        std::cout << dist[v] << (v + 1 == n ? '\n' : ' ');
    int destino = n - 1;
    if (dist[destino] == -1) {
        std::cout << "sem caminho\n";
        return 0;
    }
    std::vector<int> caminho;
    for (int v = destino; v != -1; v = pai[v]) caminho.push_back(v);
    std::reverse(caminho.begin(), caminho.end());
    for (int v : caminho) std::cout << v << ' ';
    std::cout << '\n';
}
Resultado

Executa o código para ver a saída e os gráficos.

A saída dá as distâncias 0 1 1 2 3 -1; o caminho para o último vértice é sem caminho. Muda n para 5 e o programa reconstrói 0 1 3 4. Com listas de adjacência, a BFS usa O(n+m)O(n+m) tempo e O(n)O(n) memória auxiliar. Pesos distintos deixam de ser medidos pelo número de arestas; nesse caso usa os algoritmos ponderados.

DFS, tempos e ciclos

A pesquisa em profundidade, DFS, continua pelo vizinho ainda não visitado até não poder avançar; depois regressa. Uma chamada recursiva ou uma pilha com o estado da chamada implementa esse regresso. Uma DFS a partir de ss só percorre os alcançáveis. Para percorrer todo o grafo, inicia outra DFS em cada vértice ainda por visitar.

Mantém três estados: branco, ainda não descoberto; cinzento, chamada ativa; preto, chamada terminada. Num grafo dirigido, uma aresta para um vértice cinzento fecha um ciclo, porque existe um caminho de chamadas ativas desse vértice até ao atual. Uma aresta para um preto não prova um ciclo.

Se guardares tempos de descoberta e término, os intervalos de dois vértices são disjuntos ou um contém o outro. A contenção corresponde à relação ancestral na floresta DFS. Uma aresta de árvore descobre um branco; uma aresta de retorno vai para um ancestral cinzento. Num grafo não dirigido, a aresta de volta para o pai não conta como ciclo. Com arestas paralelas, distinguir a aresta usada para chegar é mais seguro do que ignorar todo o vizinho pai.

DFS também custa O(n+m)O(n+m). A pilha pode ter profundidade nn. Grafos muito grandes podem exigir uma implementação iterativa para não esgotar a pilha de execução.

Ordenação topológica

Uma ordenação topológica de um grafo dirigido põe uu antes de vv para toda a aresta u→vu\to v. Existe exatamente quando o grafo é um DAG, isto é, não tem ciclos dirigidos.

O algoritmo de Kahn mantém graus de entrada. Insere numa fila todos os vértices com grau zero; retira um, emite-o e reduz os graus dos seus sucessores. Se um sucessor passa a zero, insere-o.

O invariante é que um vértice emitido não tem predecessores por emitir. Se foram emitidos menos de nn, o subgrafo restante tem cada vértice com algum predecessor restante, por isso seguir esses predecessores num conjunto finito repete um vértice e revela um ciclo. Não devolvas uma ordenação parcial como se fosse válida.

Para a→ca\to c, b→cb\to c, c→dc\to d, b→eb\to e, tanto a,b,c,d,ea,b,c,d,e como b,e,a,c,db,e,a,c,d são válidas. Não existe uma única ordem obrigatória. Uma fila de prioridade produz o menor disponível segundo uma regra de desempate, com custo adicional. Outra solução é inverter a ordem de término de uma DFS, depois de excluir arestas de retorno.

Componentes fortemente conexas

Uma componente fortemente conexa, SCC, é um conjunto maximal em que cada vértice alcança todos os outros por caminhos dirigidos. “Maximal” significa que não podes acrescentar outro vértice mantendo a propriedade. Não é o mesmo que procurar a componente de maior tamanho.

Neste grafo, as SCC são {a,b,c}\{a,b,c\}, {d,e}\{d,e\} e {f}\{f\}. A direção c→dc\to d não as funde: não há regresso de dd a cc. Contrair cada SCC num vértice produz um DAG, a condensação. Se a condensação tivesse um ciclo, essas componentes seriam mutuamente alcançáveis e formariam uma única SCC.

Kosaraju

  1. Faz uma DFS completa em GG e guarda os vértices por ordem de término.
  2. Constrói o transposto GTG^T, invertendo todas as arestas.
  3. Faz DFS em GTG^T por ordem decrescente de término da primeira passagem. Cada nova árvore é uma SCC.

Entre SCC distintas, uma aresta C→DC\to D implica que o maior tempo de término de CC é superior ao de DD. Na segunda passagem, começamos numa SCC sem entrada na condensação ainda não processada de GG; no transposto, ela não tem saída para uma SCC não processada. A DFS recolhe exatamente os seus vértices. Dentro de uma SCC, inverter as arestas conserva a alcançabilidade mútua.

A primeira DFS deve abranger todos os vértices, não apenas os alcançáveis de um escolhido. As duas passagens e o transposto custam O(n+m)O(n+m).

Tarjan

Tarjan encontra SCC numa única DFS. Para cada vértice uu, guarda index[u], o número de descoberta, e low[u], o menor índice a que a exploração de uu ainda pode ligar-se dentro da região ativa. Mantém ainda uma pilha dos vértices descobertos que não pertencem a uma SCC já fechada.

Ao descobrir uu, atribui o mesmo valor a index e low e empilha-o. Para cada aresta u→vu\to v:

  • Se vv é novo, faz DFS em vv e usa low[u] = min(low[u], low[v]).
  • Se vv já está na pilha, usa low[u] = min(low[u], index[v]).
  • Se vv saiu da pilha, pertence a uma SCC fechada e não deve reduzir low[u].

No fim de uu, se low[u] == index[u], uu é a raiz de uma SCC. Desempilha até retirar uu, incluindo-o. A pilha de Tarjan é distinta da pilha de chamadas: um vértice pode terminar a sua chamada e continuar à espera da raiz da sua SCC.

No ciclo a→b→c→aa\to b\to c\to a, com índices 0, 1, 2, a aresta c→ac\to a dá low[c]=0, que regressa até low[b]=0. Só aa fecha a SCC.

Segue a DFS por a,b,c,d,e,fa,b,c,d,e,f no grafo do exemplo:

AcontecimentoPilha de Tarjan, da base para o topoEfeito
Descobrir a,b,ca,b,ca,b,ca,b,cÍndices 0,1,2
Examinar c→ac\to aa,b,ca,b,clow[c]=0low[c]=0; a chamada ainda não termina
Descobrir d,ed,ea,b,c,d,ea,b,c,d,eÍndices 3,4; e→de\to d dá low[e]=3low[e]=3
Descobrir e terminar ffa,b,c,d,ea,b,c,d,eff fecha sozinho e sai da pilha
Terminar ee e dda,b,ca,b,clow[d]=index[d]=3low[d]=index[d]=3: fecha {d,e}\{d,e\}
Regressar por c,b,ac,b,aVaziaO valor 0 regressa a bb; aa fecha {a,b,c}\{a,b,c\}

Ao regressar de dd para cc, o valor low[d]=3low[d]=3 não reduz low[c]=0low[c]=0. A ligação só vai da primeira SCC para a segunda. Se, noutra exploração, encontrarmos um vizinho já retirado da pilha, não usamos o seu índice para reduzir low. Uma marca visited não distingue esse caso de um vértice ativo; precisamos de onStack.

O custo é O(n+m)O(n+m) tempo e O(n)O(n) memória auxiliar.

Testar bipartição

Um grafo não dirigido é bipartido se podemos dar duas cores aos vértices, com cores diferentes nos extremos de cada aresta. Faz BFS em cada componente; atribui ao vizinho novo a cor oposta. Se uma aresta liga duas cores iguais, rejeita.

Um ciclo ímpar impede a alternância de fechar corretamente. Reciprocamente, se há conflito, os caminhos da árvore até aos extremos da aresta produzem um ciclo ímpar depois de remover o prefixo comum. Uma árvore e um ciclo de comprimento par são bipartidos; um triângulo não é. A bipartição será usada na redução de emparelhamento a fluxo.

Exercícios

Uma ordem que fica a meio

Num grafo dirigido há arestas a→b, b→c, c→b e d→a. Faz Kahn e explica por que o resultado parcial não é uma ordenação topológica. Identifica as SCC.

Pista

Começa pelos graus de entrada: só d tem grau zero.

Mais uma pista

Depois de emitir d e a, b ainda recebe uma aresta de c e c recebe uma de b.

Solução

Kahn emite d e depois a. Restam b e c com grau positivo, por isso não emite os quatro vértices e rejeita a ordenação. O ciclo b→c→b impede qualquer ordem com todos os predecessores antes dos sucessores. As SCC são {d}, {a} e {b,c}; a condensação é d→a→{b,c}, um DAG.

Confere a tua resposta:

  • Obtive a lista parcial d,a.
  • Identifiquei o ciclo e rejeitei a ordem.
  • Separei as três SCC.

Erros frequentes

Uma lista parcial não pode ser apresentada como resposta válida. Estar numa componente não dirigida não chega para formar uma SCC; é preciso alcançabilidade nos dois sentidos.

Voltar à explicação
Uma aresta para uma SCC já fechada

Tarjan já fechou a SCC {x,y}. Mais tarde descobre z com uma aresta z→x, mas sem caminho de x para z. Deve essa aresta reduzir low[z]? Explica usando a pilha.

Pista

Distingue um vértice já visitado de um vértice ainda na pilha de Tarjan.

Mais uma pista

x foi desempilhado quando a sua SCC fechou. A aresta só permite ir de z para essa componente.

Solução

Não. Para um vizinho já visitado, Tarjan só usa index[x] se x ainda estiver na pilha. Como {x,y} já fechou e saiu da pilha, a sua aresta de entrada não pode provar regresso a um ancestral ativo de z. Sem outros caminhos, z fecha uma SCC separada. Reduzir low[z] para o índice antigo de x impediria fechar corretamente a nova componente.

Confere a tua resposta:

  • Usei a condição onStack.
  • Mantive z separado de {x,y}.
  • Expliquei a direção da alcançabilidade.

Erros frequentes

A pilha de componentes não é a pilha de chamadas. Usar apenas visited em vez de onStack junta informação de regiões que já foram concluídas.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.