0Pricing
Coding Interview Prep · Aula

Representações de Grafos e Configuração de Percursos

Crie grafos direcionados e não direcionados com listas de adjacência, inicialize BFS com um deque e DFS com uma pilha ou recursão, controlando os nós visitados.

Representações de Grafos e Configuração de Percursos é uma aula grátis de Coding Interview Prep no CoddyKit. Esta é a aula 1 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.

O que é um grafo?

Um grafo é uma coleção de nós (vértices) conectados por arestas. Diferentemente das árvores, os grafos podem conter ciclos, vários caminhos entre nós e componentes desconectados. Os grafos modelam sistemas do mundo real, como redes sociais, mapas rodoviários, árvores de dependências e links de páginas da Web. Quase toda entrevista de projeto de sistemas e algoritmos que trate de sistemas não triviais aborda grafos — dominar sua representação e travessia é essencial.

# Graph terminology:
# - V: set of vertices (nodes)
# - E: set of edges
# - Directed graph: edges have direction (A -> B but not B -> A)
# - Undirected graph: edges are bidirectional
# - Weighted graph: edges have costs/weights
# - Cyclic: contains at least one cycle
# - Acyclic: no cycles (DAG = Directed Acyclic Graph)
# - Connected: every node reachable from every other
# - Disconnected: multiple isolated components
print('Graph: nodes + edges, directed/undirected, weighted/unweighted')

Representação por Lista de Adjacência

Uma lista de adjacência armazena a lista de vizinhos de cada nó. Em Python, use um dict que associe cada nó a uma lista de nós adjacentes. Essa é a representação mais comum em problemas de entrevistas: espaço O(V + E) (eficiente para grafos esparsos), O(grau) para percorrer os vizinhos e O(1), em média, para verificar a adjacência com uma variante que usa um conjunto de dispersão. A maioria dos problemas de grafos do LeetCode usa esse formato.

from collections import defaultdict

# Build an undirected graph
def build_undirected(edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)  # both directions
    return graph

edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
graph = build_undirected(edges)
print(dict(graph))
# {0:[1,2], 1:[0,3], 2:[0,3], 3:[1,2,4], 4:[3]}

# Directed graph: only one direction
def build_directed(edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)  # only u -> v
    return graph

Representação por Matriz de Adjacência

Uma matriz de adjacência é uma matriz bidimensional V×V em que matrix[i][j] = 1 (ou o peso da aresta) se houver uma aresta de i para j, e 0 caso contrário. Ela permite consultar uma aresta em O(1), mas usa espaço O(V²), independentemente da quantidade de arestas — um desperdício em grafos esparsos. Ela é preferível quando o grafo é denso (tem muitas arestas) ou quando verificações rápidas da existência de arestas são essenciais, como nos caminhos mínimos entre todos os pares de Floyd-Warshall.

# Adjacency matrix for 5 nodes
V = 5
matrix = [[0] * V for _ in range(V)]

edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
for u, v in edges:
    matrix[u][v] = 1
    matrix[v][u] = 1  # undirected

# Print the matrix:
for row in matrix:
    print(row)
# Neighbour check: O(1)
print('Edge 0-2:', bool(matrix[0][2]))  # True
print('Edge 0-4:', bool(matrix[0][4]))  # False

# Space: O(V^2) vs adjacency list O(V+E)
# Dense graph: matrix often better; sparse: list better

Representação por Lista de Arestas

Uma lista de arestas é a representação mais simples: apenas uma lista de tuplas (origem, destino), opcionalmente com pesos. Ela usa espaço O(E) e permite percorrer facilmente todas as arestas. No entanto, para encontrar os vizinhos de um nó, é necessário examinar todas as arestas: O(E). Listas de arestas são usadas em algoritmos de grafos que percorrem todas as arestas de forma exata, como Bellman-Ford (relaxar todas as arestas n-1 vezes) e o algoritmo de árvore geradora mínima de Kruskal.

# Weighted edge list: (source, destination, weight)
edge_list = [
    (0, 1, 4),
    (0, 2, 1),
    (1, 3, 1),
    (2, 3, 5),
    (3, 4, 3)
]

# Useful for:
# Bellman-Ford: iterate all edges n-1 times
# Kruskal's MST: sort by weight then union-find

# Sort by weight for Kruskal:
edge_list_sorted = sorted(edge_list, key=lambda e: e[2])
print('Sorted by weight:', edge_list_sorted)

# Finding neighbours: O(E) scan -- inefficient for traversal
node_0_neighbors = [v for u, v, w in edge_list if u == 0]
print('Node 0 neighbors:', node_0_neighbors)

Configuração do BFS: Fila e Conjunto de Visitados

BFS (Busca em Largura) explora um grafo nível a nível usando uma fila. O componente essencial é um conjunto de visitados para evitar revisitar nós em grafos cíclicos. Sem o conjunto de visitados, o BFS em um grafo cíclico entraria em um laço infinito. A configuração padrão é: inicialize a fila com o nó de origem, marque-o como visitado e, em seguida, retire repetidamente um nó da fila, processe-o e enfileire os vizinhos ainda não visitados.

from collections import deque

def bfs(graph, start):
    visited = {start}        # mark source as visited
    queue = deque([start])   # initialise queue
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbour in graph[node]:
            if neighbour not in visited:
                visited.add(neighbour)     # mark BEFORE enqueue
                queue.append(neighbour)
    return order

from collections import defaultdict
graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,3),(2,3),(3,4)]:
    graph[u].append(v); graph[v].append(u)

print(bfs(graph, 0))  # [0, 1, 2, 3, 4]

Configuração do DFS: Pilha ou Recursão

DFS (Busca em Profundidade) explora cada ramo o mais longe possível antes de retroceder. Implemente-o recursivamente (usando a pilha de chamadas) ou iterativamente (usando uma pilha explícita). Ambos exigem um conjunto de visitados em grafos cíclicos. A versão iterativa adiciona os vizinhos na ordem inversa para corresponder à ordem de percurso do DFS recursivo, embora a ordem de exploração possa variar entre as duas implementações.

def dfs_recursive(graph, node, visited=None, order=None):
    if visited is None: visited = set(); order = []
    visited.add(node)
    order.append(node)
    for neighbour in graph[node]:
        if neighbour not in visited:
            dfs_recursive(graph, neighbour, visited, order)
    return order

def dfs_iterative(graph, start):
    visited = set()
    stack = [start]
    order = []
    while stack:
        node = stack.pop()
        if node in visited: continue
        visited.add(node)
        order.append(node)
        for neighbour in reversed(graph[node]):  # reverse for same order as recursive
            if neighbour not in visited:
                stack.append(neighbour)
    return order

print('Recursive DFS:', dfs_recursive(graph, 0))
print('Iterative DFS:', dfs_iterative(graph, 0))

Quando Usar BFS ou DFS

Escolha BFS quando precisar do caminho mínimo (com o menor número de arestas) em um grafo não ponderado ou quando precisar processar os nós nível a nível. Escolha DFS quando precisar explorar todos os nós alcançáveis, detectar ciclos, encontrar componentes conectados, realizar uma ordenação topológica ou enumerar todos os caminhos. Na prática: BFS para “caminho mínimo/menor número de saltos” e DFS para “existência/alcançabilidade/enumeração”.

# BFS use cases:
# - Shortest path in unweighted graph (fewest edges)
# - Level-order traversal
# - Word ladder (minimum transformations)
# - Clone graph

# DFS use cases:
# - Connected components (flood fill)
# - Cycle detection
# - Topological sort
# - All paths between two nodes
# - Maze solving (any path)
# - N-queens, Sudoku (backtracking)

# Both: O(V + E) time, O(V) space for visited
print('BFS: shortest hops | DFS: existence and enumeration')

Grafos a Partir dos Formatos de Entrada do LeetCode

Os problemas de grafos do LeetCode apresentam diferentes formatos de entrada. Lista de arestas: [[0,1],[0,2]] — construa uma lista de adjacência. Lista de adjacência indexada: graph[i] é a lista de vizinhos de i. Grade/matriz: uma matriz bidimensional m×n em que as células são nós e as células adjacentes (acima/abaixo/esquerda/direita) são vizinhas. Node com filhos: classes personalizadas como Node(val, neighbors). Reconheça esses formatos e converta-os em uma lista de adjacência como primeiro passo.

# Format 1: edge list -> adjacency list
def edges_to_adj(n, edges):
    graph = [[] for _ in range(n)]
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)
    return graph

# Format 2: 2D grid -> adjacency (implicit)
# Neighbours of (r, c): (r-1,c), (r+1,c), (r,c-1), (r,c+1)
DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
def grid_neighbours(grid, r, c):
    rows, cols = len(grid), len(grid[0])
    return [(r+dr, c+dc) for dr, dc in DIRS
            if 0 <= r+dr < rows and 0 <= c+dc < cols]

grid = [[1,1,0],[0,1,1],[1,0,0]]
print('Neighbours of (0,0):', grid_neighbours(grid, 0, 0))
print('Neighbours of (1,1):', grid_neighbours(grid, 1, 1))

Marcação de Visitados em Grades

Para problemas com grades, há duas maneiras de acompanhar as células visitadas. Opção A: use um conjunto separado de visited com tuplas (row, col) — espaço adicional O(m*n). Opção B: modifique a grade diretamente, marcando as células visitadas com um valor sentinela (por exemplo, '#' ou 2) e restaurando-as depois, se necessário. A abordagem direta usa espaço adicional O(1) e é comum em problemas de preenchimento por inundação e de contagem de ilhas.

def num_islands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if grid[r][c] != '1':
            return
        grid[r][c] = '#'  # mark as visited (in-place)
        dfs(r+1, c); dfs(r-1, c)
        dfs(r, c+1); dfs(r, c-1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                dfs(r, c)
                count += 1
    return count

grid = [['1','1','0','0'],
        ['1','1','0','0'],
        ['0','0','1','0'],
        ['0','0','0','1']]
print(num_islands(grid))  # 3

Inicialização do BFS com Múltiplas Origens

O BFS com múltiplas origens começa simultaneamente a partir de vários nós, inicializando a fila com todos os nós de origem marcados como visitados. Ele é usado em problemas como “distância até o 0 mais próximo”, “laranjas apodrecendo” e “paredes e portões”, nos quais você deseja a menor distância até qualquer um dos nós de origem. O BFS com múltiplas origens é executado em O(V + E), assim como o BFS com uma única origem, porque cada nó ainda é visitado no máximo uma vez.

from collections import deque

def rotting_oranges(grid):
    rows, cols = len(grid), len(grid[0])
    queue = deque()
    fresh = 0
    # Multi-source: all rotten oranges start at time=0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 2:
                queue.append((r, c, 0))  # (row, col, time)
            elif grid[r][c] == 1:
                fresh += 1
    dirs = [(0,1),(0,-1),(1,0),(-1,0)]
    time = 0
    while queue:
        r, c, t = queue.popleft()
        for dr, dc in dirs:
            nr, nc = r+dr, c+dc
            if 0<=nr<rows and 0<=nc<cols and grid[nr][nc]==1:
                grid[nr][nc] = 2  # mark rotten
                fresh -= 1
                queue.append((nr, nc, t+1))
                time = t + 1
    return time if fresh == 0 else -1

print(rotting_oranges([[2,1,1],[1,1,0],[0,1,1]]))  # 4

Densidade do Grafo e Escolha da Representação

A escolha entre uma lista de adjacência e uma matriz depende da densidade do grafo — a razão E/V². Um grafo esparso (E << V²) se beneficia de listas de adjacência: espaço O(V+E), em comparação com O(V²) para uma matriz. Um grafo denso (E ≈ V²) se beneficia de matrizes de adjacência: consulta de arestas em O(1), em comparação com O(grau) para listas. Em problemas de entrevistas, as listas de adjacência quase sempre são a escolha certa, pois a maioria dos problemas envolve grafos esparsos.

# Graph density comparison:
# Sparse: social network (V=1B users, avg 200 friends)
#   E = 200 * 1B = 200B << V^2 = 10^18 -> adjacency list
# Dense: complete graph (every node connected to every other)
#   E = V*(V-1)/2 ≈ V^2 -> adjacency matrix

# Interview rule of thumb:
# - Default to adjacency list (defaultdict(list))
# - Use matrix only when asked about dense graph or O(1) edge lookup
# - Grid problems: use implicit adjacency (4-directional neighbours)

print('Sparse graph (E << V^2): use adjacency list')
print('Dense graph (E ~ V^2): consider adjacency matrix')

Verificação Rápida

Avalie sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação apresentados nesta lição.

Resumo da Lição

Nesta lição, você aprendeu: três representações de grafos (lista de adjacência, matriz e lista de arestas) e quando escolher cada uma; a configuração do BFS e do DFS com conjuntos de visitados para evitar laços infinitos em grafos cíclicos; e padrões práticos, como a marcação direta de células em grades e o BFS com múltiplas origens. A seguir, aplicaremos BFS para encontrar caminhos mínimos e percorrer níveis.

Perguntas Frequentes

A aula “Representações de Grafos e Configuração de Percursos” é grátis?

Sim — o texto completo de “Representações de Grafos e Configuração de Percursos” é 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 “Representações de Grafos e Configuração de Percursos”?

Crie grafos direcionados e não direcionados com listas de adjacência, inicialize BFS com um deque e DFS com uma pilha ou recursão, controlando os nós visitados. 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 1 de 4.

Quanto tempo leva a aula “Representações de Grafos e Configuração de Percursos”?

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