Aplicações de DFS e Grafos: Ordenação Topológica
1. O Problema das Dependências
Imagine que você precisa se vestir. Você não pode calçar o sapato antes da meia, nem o cinto antes da calça. Ou em Computação: para compilar o módulo A, você precisa antes ter compilado B e C.
Esse problema é modelado como encontrar uma ordem linear de execução em um Grafo Direcionado. Essa ordenação chama-se Ordenação Topológica.
Requisito: O grafo não pode ter ciclos. Ele deve ser um DAG (Directed Acyclic Graph). Se houver ciclo (A precisa de B, B precisa de A), é impossível satisfazer as dependências.
2. Solução com DFS (A Mágica da Pós-Ordem)
A ideia é simples: uma tarefa só está “pronta” quando todas as suas dependências (filhos no grafo) foram finalizadas. Na DFS, o momento em que “terminamos” um nó (quando a chamada recursiva retorna) significa que já visitamos todos os seus descendentes.
Algoritmo: 1. Execute a DFS em todos os nós não visitados. 2. No momento em que a DFS de um vértice \(u\) terminar, adicione \(u\) em uma Lista. 3. No final, a Ordenação Topológica é a lista invertida (do último que terminou para o primeiro).
Stack<Integer> topoSort = new Stack<>();
boolean[] visitado = new boolean[V];
void dfsTopo(int u) {
visitado[u] = true;
for (int v : adj.get(u)) {
if (!visitado[v]) dfsTopo(v);
}
// Todos os filhos de u já foram processados.
// u já pode entrar na lista.
topoSort.push(u);
}
// No main:
for (int i=0; i<V; i++) if (!visitado[i]) dfsTopo(i);
while (!topoSort.isEmpty()) System.out.print(topoSort.pop() + " ");3. Solução com Algoritmo de Kahn (Graus de Entrada)
Geralmente preferido por ser iterativo (usa Fila) e detectar ciclos facilmente.
Algoritmo: 1. Calcule o Grau de Entrada (Indegree) de todos os vértices. 2. Adicione todos os nós com Indegree == 0 em uma Fila. (Esses não tem dependências). 3. Enquanto a fila não vazia: - Remova \(u\). Imprima (ou salve). - Para cada vizinho \(v\) de \(u\): - Decremente Indegree[v]-- (removemos a dependência \(u\)). - Se Indegree[v] virou 0, adicione \(v\) na Fila. 4. Se ao final o número de nós impressos for \(< V\), o grafo tem ciclo.
public List<Integer> kahn(int V, List<List<Integer>> adj) {
int[] inDegree = new int[V];
for (int u = 0; u < V; u++) {
for (int v : adj.get(u)) inDegree[v]++;
}
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < V; i++) {
if (inDegree[i] == 0) q.add(i);
}
List<Integer> result = new ArrayList<>();
while (!q.isEmpty()) {
int u = q.poll();
result.add(u);
for (int v : adj.get(u)) {
inDegree[v]--;
if (inDegree[v] == 0) q.add(v);
}
}
if (result.size() != V) {
throw new IllegalStateException("Grafo contém Ciclo!");
}
return result;
}4. Componentes Conexos (Flood Fill)
Em um grafo não-direcionado, queremos saber quantos “grupos” isolados existem. Exemplo: Contar quantas ilhas existem num mapa, ou quantas redes sociais desconectadas.
Algoritmo: 1. Inicialize count = 0. 2. Para cada vértice i de 0 a V-1: - Se !visitado[i]: - count++. - Chame dfs(i) (ou bfs) para marcar todo mundo que pertence ao mesmo grupo de i.
int contarComponentes() {
int count = 0;
Arrays.fill(visitado, false);
for (int i = 0; i < V; i++) {
if (!visitado[i]) {
count++;
dfs(i); // Inunda o componente
}
}
return count;
}Em matrizes (pixel art, jogos), isso se chama Flood Fill (Balde de Tinta do Paint).
5. Verificação de Grafo Bipartido (Coloração)
Um grafo é Bipartido se pudermos dividir os vértices em dois conjuntos \(A\) e \(B\) tal que todas as arestas liguem um nó de \(A\) a um de \(B\). Propriedade: Um grafo é bipartido \(\iff\) não contém ciclo de tamanho ímpar.
Algoritmo (DFS/BFS): 1. Tente colorir o nó inicial com Cor 0. 2. Pinte todos os vizinhos com Cor 1. 3. Os vizinhos dos vizinhos com Cor 0… (flip-flop). 4. Se encontrar um vizinho que já tem a mesma cor que você: Impossível, não é bipartido.
int[] color = new int[V]; // -1: sem cor, 0: Azul, 1: Vermelho
Arrays.fill(color, -1);
boolean isBipartite(int start) {
Queue<Integer> q = new LinkedList<>();
q.add(start);
color[start] = 0;
while (!q.isEmpty()) {
int u = q.poll();
for (int v : adj.get(u)) {
if (color[v] == -1) {
color[v] = 1 - color[u]; // Cor oposta
q.add(v);
} else if (color[v] == color[u]) {
return false; // Conflito de cores
}
}
}
return true;
}