Coding Interview Prep · Aula

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.

Aula 4 de 413 etapas

Detecção de Ciclos em Grafos Direcionados e Não Direcionados é uma aula grátis de Coding 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 Coding Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Coding 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)]))               # False

Ciclo 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)]))  # True

Ciclo 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)]))               # False

Programaçã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 dependency

Detecçã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)]))               # False

Encontrar 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.

Grátis para começar

Aprenda Coding Interview Prep 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
90
Aulas
360

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 Coding Interview Prep, atualize para CoddyKit PRO. O curso de Coding 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 Coding 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 Coding Interview Prep?

Nenhuma experiência prévia é necessária. Coding 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 Coding Interview Prep?

Sim. Cada aula de Coding 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

  1. Representações de Grafos e Configuração de Percursos
  2. BFS: Menor Caminho e Percurso por Níveis
  3. DFS: Componentes Conectados e Preenchimento por Inundação
  4. Detecção de Ciclos em Grafos Direcionados e Não Direcionados
← Voltar para Coding Interview Prep