Detecção de Ciclos em Grafos Direcionados e Não Direcionados
Detecte ciclos em grafos não direcionados rastreando pais e em grafos direcionados usando coloração DFS, com estados visitados branco, cinza e preto.
Detecção de Ciclos em Grafos Direcionados e Não Direcionados é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.
Por que a detecção de ciclos é importante
Um ciclo em um grafo é um caminho que começa e termina no mesmo nó. A detecção de ciclos é essencial em muitos algoritmos: a ordenação topológica falha em grafos com ciclos, a resolução de dependências precisa detectar dependências circulares e a detecção de bloqueios mútuos no escalonamento do OS exige encontrar ciclos em grafos de alocação de recursos. A abordagem é diferente para grafos não direcionados e direcionados — eles exigem algoritmos fundamentalmente diferentes.
from collections import defaultdict
# Undirected cycle: A-B-C-A (triangle)
undirected = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
undirected[u].append(v)
undirected[v].append(u)
# Directed cycle: A->B->C->A
directed = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
directed[u].append(v) # one direction only
# Key difference:
# Undirected: edge A-B appears as both A->B and B->A
# Must track parent to distinguish cycle from back-edge to parent
print('Undirected and directed cycles need different detection')Detecção de ciclos em grafos não direcionados com DFS
Em um grafo não direcionado, existe um ciclo se a DFS visitar um nó que já está no caminho atual (e não apenas visitado). O desafio é que toda aresta aparece nas duas direções; portanto, quando visitamos um nó filho, a lista de vizinhos dele inclui nosso nó atual (o pai). Precisamos acompanhar o pai de cada nó para evitar sinalizar erroneamente a aresta de volta ao pai como um ciclo. Se encontrarmos um nó visitado que não seja nosso pai, teremos encontrado um ciclo.
def has_cycle_undirected(n, edges):
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
def dfs(node, parent):
visited.add(node)
for nb in graph[node]:
if nb not in visited:
if dfs(nb, node): # recurse with current as parent
return True
elif nb != parent: # visited and not parent = CYCLE
return True
return False
for node in range(n):
if node not in visited:
if dfs(node, -1): # -1 = no parent for root
return True
return False
print(has_cycle_undirected(4, [(0,1),(1,2),(2,3),(3,1)])) # True
print(has_cycle_undirected(3, [(0,1),(1,2)])) # FalseCiclo não direcionado com BFS
A detecção de ciclos com BFS em um grafo não direcionado também acompanha o pai de cada nó visitado. Ao processar os vizinhos de um nó, se um vizinho já tiver sido visitado e não for o pai do nó atual, existe um ciclo. Use um dicionário para armazenar os pais. Essa abordagem O(V + E) evita a preocupação com o limite de recursão e é a alternativa iterativa preferida para grafos grandes.
from collections import deque, defaultdict
def has_cycle_bfs_undirected(n, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
for start in range(n):
if start in visited:
continue
visited.add(start)
parent = {start: -1}
queue = deque([start])
while queue:
node = queue.popleft()
for nb in graph[node]:
if nb not in visited:
visited.add(nb)
parent[nb] = node
queue.append(nb)
elif parent[node] != nb: # visited and not parent = CYCLE
return True
return False
print(has_cycle_bfs_undirected(4, [(0,1),(1,2),(2,0)])) # TrueCiclo direcionado: por que acompanhar o pai falha
Em um grafo direcionado, acompanhar o pai não é suficiente. Considere A→C e B→C: o nó C tem dois 'pais', mas não há ciclo. A abordagem correta usa uma coloração de três estados: branco (não visitado), cinza (no caminho/pilha de DFS atual) e preto (totalmente processado). Existe um ciclo se encontrarmos um nó cinza durante a DFS — isso significa que encontramos uma aresta de retorno para um ancestral no caminho atual.
# Three-state DFS coloring:
# WHITE (0): not yet visited
# GRAY (1): currently being visited (in DFS stack)
# BLACK (2): fully visited (all descendants processed)
# Why parent fails for directed graphs:
# A -> C (no cycle)
# B -> C (no cycle)
# If we DFS from A, mark C gray
# Then DFS from B finds C is gray -- but this is NOT a cycle!
# C is gray from A's path, not B's path.
# Parent tracking only works when the back-edge goes to the IMMEDIATE parent.
print('Directed graph: use 3-state coloring (white/gray/black)')Detecção de ciclos direcionados com DFS de três estados
Use um vetor state[] com os valores 0 (branco/não visitado), 1 (cinza/na pilha) e 2 (preto/concluído). Inicie a DFS, marcando o nó como cinza ao entrar e como preto ao sair. Se a DFS alcançar um nó cinza, uma aresta de retorno foi encontrada — existe um ciclo. Se alcançar um nó preto, esse caminho já foi totalmente explorado e não contém ciclos; portanto, ignore-o.
def has_cycle_directed(n, edges):
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
state = [0] * n # 0=white, 1=gray, 2=black
def dfs(node):
state[node] = 1 # mark gray (in stack)
for nb in graph[node]:
if state[nb] == 1: # gray = back edge = CYCLE
return True
if state[nb] == 0: # white = unvisited
if dfs(nb):
return True
state[node] = 2 # mark black (fully processed)
return False
for node in range(n):
if state[node] == 0:
if dfs(node):
return True
return False
print(has_cycle_directed(4, [(0,1),(1,2),(2,0),(2,3)])) # True (0->1->2->0)
print(has_cycle_directed(3, [(0,1),(1,2)])) # FalseProgramação de cursos: ciclo em um DAG
Programação de cursos (LeetCode #207) pergunta se todos os cursos podem ser concluídos dadas as pré-requisitos. Modele os cursos como nós e os pré-requisitos como arestas direcionadas. Todos os cursos podem ser concluídos se, e somente se, o grafo for um DAG (sem ciclos). Use a detecção de ciclos com DFS de três estados — se um ciclo for encontrado, retorne falso; caso contrário, retorne verdadeiro.
from collections import defaultdict
def can_finish(num_courses, prerequisites):
graph = defaultdict(list)
for a, b in prerequisites:
graph[b].append(a) # b is prerequisite for a: b -> a
state = [0] * num_courses
def dfs(course):
if state[course] == 1: return False # cycle!
if state[course] == 2: return True # already verified
state[course] = 1 # mark as in-progress
for next_course in graph[course]:
if not dfs(next_course):
return False
state[course] = 2 # mark as done
return True
return all(dfs(i) for i in range(num_courses) if state[i] == 0)
print(can_finish(2, [[1,0]])) # True: take 0 then 1
print(can_finish(2, [[1,0],[0,1]])) # False: circular dependencyDetecção de ciclos com o algoritmo de Kahn (BFS)
Uma alternativa para detectar ciclos em grafos direcionados usa a ordenação topológica por BFS de Kahn. Conte os graus de entrada de todos os nós. Coloque em uma fila os nós com grau de entrada 0. Processe cada um: diminua os graus de entrada dos vizinhos e enfileire aqueles que chegarem a 0. Se a quantidade de nós processados for igual a V, não há ciclo; caso contrário, existe um ciclo (os nós não processados formam ciclos). Essa abordagem O(V + E) é intuitiva e mais fácil de memorizar do que a DFS de três estados.
from collections import defaultdict, deque
def has_cycle_kahn(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
# Start with all zero in-degree nodes
queue = deque(i for i in range(n) if in_degree[i] == 0)
processed = 0
while queue:
node = queue.popleft()
processed += 1
for nb in graph[node]:
in_degree[nb] -= 1
if in_degree[nb] == 0:
queue.append(nb)
return processed != n # if not all processed, cycle exists
print(has_cycle_kahn(4, [(0,1),(1,2),(2,0),(2,3)])) # True
print(has_cycle_kahn(3, [(0,1),(1,2)])) # FalseEncontrar o ciclo: coletando os nós do ciclo
Às vezes, é necessário identificar quais nós fazem parte de um ciclo, e não apenas detectar sua existência. Durante uma DFS de três estados, quando uma aresta de retorno é encontrada, percorra a pilha de chamadas (ou uma pilha de caminho) para coletar todos os nós entre o ancestral e o nó atual. Uma pilha de caminho mantida junto com o vetor de estados registra o caminho atual da DFS, permitindo reconstruir o ciclo em O(comprimento_do_ciclo).
def find_cycle_nodes(n, edges):
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
state = [0] * n
path = [] # current DFS path
cycle = []
def dfs(node):
state[node] = 1
path.append(node)
for nb in graph[node]:
if state[nb] == 1: # back edge -> found cycle
start = path.index(nb)
cycle.extend(path[start:])
return True
if state[nb] == 0 and dfs(nb):
return True
path.pop()
state[node] = 2
return False
for i in range(n):
if state[i] == 0 and dfs(i):
break
return cycle
print(find_cycle_nodes(4, [(0,1),(1,2),(2,0),(2,3)])) # [0, 1, 2]Encontrar estados eventualmente seguros
Encontrar estados eventualmente seguros (LeetCode #802) pergunta quais nós eventualmente levam a um nó terminal (sem arestas de saída) sem ficarem presos em um ciclo. Um nó é 'seguro' se todos os caminhos a partir dele levam a nós terminais. Use uma DFS de três estados: os nós pretos (totalmente processados sem detectar ciclos) são seguros. Os nós que fazem parte de um ciclo ou levam a um ciclo não são seguros.
def eventual_safe_nodes(graph):
n = len(graph)
state = [0] * n # 0=unvisited, 1=visiting, 2=safe
def dfs(node):
if state[node] == 1: # currently visiting = cycle
return False
if state[node] == 2: # already verified safe
return True
state[node] = 1 # mark as visiting
for nb in graph[node]:
if not dfs(nb):
return False # leads to cycle, not safe
state[node] = 2 # mark as safe
return True
return [i for i in range(n) if dfs(i)]
# [[1,2],[2,3],[5],[0],[5],[],[]] means:
# 0->[1,2], 1->[2,3], 2->[5], 3->[0] (cycle!), 4->[5], 5->[], 6->[]
print(eventual_safe_nodes([[1,2],[2,3],[5],[0],[5],[],[]]))
# [2, 4, 5, 6]Conexão redundante em grafo não direcionado
Conexão redundante (LeetCode #684) encontra a aresta que cria um ciclo quando adicionada a um grafo não direcionado que, de outra forma, seria acíclico. Embora isso possa ser resolvido com detecção de ciclos por DFS, a solução mais simples usa uma estrutura de união e busca (DSU): processe as arestas uma a uma; se ambos os extremos já estiverem conectados (na SAME componente), a aresta atual cria um ciclo e é a resposta. A DSU oferece O(alpha(n)) por operação — na prática, O(1).
def find_redundant_connection(edges):
n = len(edges)
parent = list(range(n + 1))
rank = [0] * (n + 1)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px == py:
return False # already connected = cycle!
if rank[px] < rank[py]: px, py = py, px
parent[py] = px
if rank[px] == rank[py]: rank[px] += 1
return True
for u, v in edges:
if not union(u, v):
return [u, v] # this edge creates the cycle
return []
print(find_redundant_connection([[1,2],[1,3],[2,3]])) # [2,3]
print(find_redundant_connection([[1,2],[2,3],[3,4],[1,4],[1,5]])) # [1,4]Resumo: estratégias de detecção de ciclos
Para resumir as ferramentas de detecção de ciclos: para grafos não direcionados, use DFS com acompanhamento do pai ou uma estrutura de união e busca. Para grafos direcionados, use DFS de três estados (branco/cinza/preto) ou a ordenação topológica por BFS de Kahn. Escolha a estrutura de união e busca quando estiver adicionando arestas uma por vez, de forma incremental. Escolha o algoritmo de Kahn quando também precisar da ordem topológica. Escolha a DFS de três estados quando precisar identificar os nós específicos do ciclo. Em entrevistas, sempre deixe clara a distinção entre grafos direcionados e não direcionados ao discutir detecção de ciclos.
# Cycle detection summary:
# Graph type | Algorithm | Complexity
# ------------|----------------------|-----------
# Undirected | DFS + parent track | O(V + E)
# Undirected | Union-Find (DSU) | O(E * alpha(V))
# Directed | DFS 3-state (W/G/B) | O(V + E)
# Directed | Kahn's BFS topo sort | O(V + E)
# When to choose:
# Online (edges added one at a time): Union-Find
# Need topological order too: Kahn's BFS
# Need cycle nodes identified: 3-state DFS with path stack
# Simple existence check: any of the above
print('Always clarify directed vs undirected before coding')Verificação rápida
Teste sua compreensão dos conceitos de Estruturas de Dados & Algoritmos — Preparação para entrevistas de programação desta lição.
Recapitulação da lição
Nesta lição, você aprendeu: detecção de ciclos não direcionados com DFS acompanhando o pai, detecção de ciclos direcionados com coloração de três estados — branco/cinza/preto —, a alternativa da BFS de Kahn para grafos direcionados e aplicações como programação de cursos, conexão redundante e estados eventualmente seguros. A seguir, mergulharemos nos fundamentos da programação dinâmica.
Aprenda Python com um tutor de IA — grátis
Escreva e execute código real no seu navegador, obtenha ajuda instantânea de um tutor de IA 24/7 e continue de onde parou na web ou no app.
- Cursos
- 30
- Aulas
- 120
Perguntas Frequentes
A aula “Detecção de Ciclos em Grafos Direcionados e Não Direcionados” é grátis?
Sim — o texto completo de “Detecção de Ciclos em Grafos Direcionados e Não Direcionados” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.
O que vou aprender em “Detecção de Ciclos em Grafos Direcionados e Não Direcionados”?
Detecte ciclos em grafos não direcionados rastreando pais e em grafos direcionados usando coloração DFS, com estados visitados branco, cinza e preto. Você pratica DSA Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar DSA Interview Prep?
Nenhuma experiência prévia é necessária. DSA Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.
Quanto tempo leva a aula “Detecção de Ciclos em Grafos Direcionados e Não Direcionados”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de DSA Interview Prep?
Sim. Cada aula de DSA Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Representações de Grafos e Configuração de Percursos
- BFS: Menor Caminho e Percurso por Níveis
- DFS: Componentes Conectados e Preenchimento por Inundação
- Detecção de Ciclos em Grafos Direcionados e Não Direcionados