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 DSA 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 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.
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 graphRepresentaçã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 betterRepresentaçã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)) # 3Inicializaçã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]])) # 4Densidade 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 DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA 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 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 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 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