Представление графов и настройка обхода
Создавайте ориентированные и неориентированные графы со списками смежности, инициализируйте BFS с deque и DFS со стеком или рекурсией, отслеживая посещённые вершины
«Представление графов и настройка обхода» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Что такое граф?
Граф — это набор узлов (вершин), соединённых рёбрами. В отличие от деревьев, графы могут содержать циклы, несколько путей между вершинами и несвязные компоненты. Графы моделируют реальные системы, такие как социальные сети, дорожные карты, деревья зависимостей и ссылки между веб-страницами. Почти на каждом собеседовании по нетривиальному проектированию систем и алгоритмам затрагиваются графы — важно освоить их представление и обход.
# 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')Представление в виде списка смежности
Список смежности хранит список соседей для каждой вершины. В Python используйте dict, сопоставляющий каждой вершине список смежных вершин. Это самое распространённое представление в задачах на собеседованиях: O(V + E) памяти (эффективно для разреженных графов), O(degree) для перебора соседей и в среднем O(1) для проверки смежности с помощью варианта на основе хеш-множества. В большинстве задач LeetCode на графы используется именно этот формат.
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Представление в виде матрицы смежности
Матрица смежности — это двумерный массив размера V×V, где matrix[i][j] = 1 (или вес ребра), если существует ребро из i в j, и 0 в противном случае. Она обеспечивает проверку наличия ребра за O(1), но использует O(V²) памяти независимо от количества рёбер — это расточительно для разреженных графов. Матрица предпочтительна для плотных графов (с большим количеством рёбер) или когда критически важна быстрая проверка существования ребра, например при поиске кратчайших путей между всеми парами вершин алгоритмом Флойда — Уоршелла.
# 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Представление в виде списка рёбер
Список рёбер — самое простое представление: всего лишь список кортежей (исходная вершина, конечная вершина), при необходимости с весами. Он использует O(E) памяти, а перебрать все рёбра с его помощью легко. Однако для поиска соседей вершины требуется просмотреть все рёбра: O(E). Списки рёбер используют алгоритмы, которые последовательно обрабатывают каждое ребро, например алгоритм Беллмана — Форда (выполняет релаксацию всех рёбер n-1 раз) и алгоритм Краскала построения минимального остовного дерева.
# 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)Настройка BFS: очередь и множество посещённых вершин
BFS (поиск в ширину) обходит граф по уровням с помощью очереди. Ключевой компонент — множество посещённых вершин, которое не позволяет повторно посещать вершины в циклических графах. Без этого множества BFS в циклическом графе зациклится навсегда. Стандартная настройка такова: инициализируйте очередь исходной вершиной, отметьте её как посещённую, затем неоднократно извлекайте вершину из очереди, обрабатывайте её и добавляйте непосещённых соседей.
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]Настройка DFS: стек или рекурсия
DFS (поиск в глубину) идёт по каждой ветви настолько далеко, насколько возможно, прежде чем вернуться назад. Реализовать его можно рекурсивно (используя стек вызовов) или итеративно (используя явный стек). Для циклических графов в обоих вариантах требуется множество посещённых вершин. В итеративном варианте соседей добавляют в обратном порядке, чтобы получить тот же порядок обхода, что и при рекурсивном DFS, хотя порядок исследования может различаться в двух реализациях.
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))Когда использовать BFS, а когда DFS
Выбирайте BFS, когда нужен кратчайший путь (с наименьшим количеством рёбер) в невзвешенном графе или когда вершины нужно обрабатывать по уровням. Выбирайте DFS, когда нужно исследовать все достижимые вершины, обнаружить циклы, найти связные компоненты, выполнить топологическую сортировку или перечислить все пути. На практике: BFS — для «кратчайшего пути/минимального числа переходов», DFS — для «существования/достижимости/перечисления».
# 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')Граф из форматов входных данных LeetCode
В задачах LeetCode на графы используются разные форматы входных данных. Список рёбер: [[0,1],[0,2]] — постройте список смежности. Список смежности с индексацией: graph[i] — это список соседей вершины i. Сетка/матрица: двумерный массив размера m×n, где ячейки являются вершинами, а соседние ячейки (сверху, снизу, слева и справа) — соседями. Вершина с дочерними вершинами: пользовательские классы, такие как Node(val, neighbors). Научитесь распознавать эти форматы и первым шагом преобразовывать их в список смежности.
# 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))Пометка посещённых ячеек в сетках
В задачах с сетками есть два способа отслеживать посещённые ячейки. Вариант A: использовать отдельное множество visited из кортежей (row, col) — O(m*n) дополнительной памяти. Вариант B: изменять сетку на месте, помечая посещённые ячейки специальным значением (например, '#' или 2), а затем при необходимости восстанавливая их. Такой подход использует O(1) дополнительной памяти и часто применяется в задачах на заливку и подсчёт островов.
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Инициализация BFS из нескольких источников
BFS из нескольких источников начинается одновременно из нескольких вершин: очередь инициализируется всеми исходными вершинами, помеченными как посещённые. Такой подход используется в задачах о «расстоянии до ближайшего нуля», «гниющих апельсинах» и «стенах и воротах», где нужно найти кратчайшее расстояние от любой исходной вершины. BFS из нескольких источников выполняется за O(V + E) — столько же, сколько BFS из одного источника, — поскольку каждая вершина всё равно посещается не более одного раза.
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Плотность графа и выбор представления
Выбор между списком смежности и матрицей зависит от плотности графа — отношения E/V². Разреженный граф (E << V²) выигрывает от списка смежности: O(V+E) памяти против O(V²) для матрицы. Плотный граф (E ≈ V²) выигрывает от матрицы смежности: проверка ребра за O(1) против O(degree) для списка. В задачах на собеседованиях список смежности почти всегда является правильным выбором, поскольку большинство задач связано с разреженными графами.
# 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')Быстрая проверка
Проверьте понимание концепций «Структуры данных и алгоритмы — подготовка к собеседованию по программированию» из этого урока.
Итоги урока
В этом уроке вы узнали о трёх представлениях графа (списке смежности, матрице и списке рёбер) и о том, когда выбирать каждое из них, о настройке BFS и DFS с множествами посещённых вершин для предотвращения бесконечных циклов в циклических графах, а также о практических приёмах, таких как пометка ячеек сетки на месте и BFS из нескольких источников. Далее мы применим BFS для поиска кратчайших путей и обхода по уровням.
Часто задаваемые вопросы
Урок «Представление графов и настройка обхода» бесплатный?
Да — полный текст урока «Представление графов и настройка обхода» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Представление графов и настройка обхода»?
Создавайте ориентированные и неориентированные графы со списками смежности, инициализируйте BFS с deque и DFS со стеком или рекурсией, отслеживая посещённые вершины Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Представление графов и настройка обхода»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Представление графов и настройка обхода
- BFS: кратчайший путь и обход по уровням
- DFS: компоненты связности и заливка
- Обнаружение циклов в ориентированных и неориентированных графах