BFS: кратчайший путь и обход по уровням
Используйте BFS для поиска кратчайшего пути в невзвешенном графе, решайте задачу word-ladder по уровням и клонируйте граф с помощью хеш-таблицы
«BFS: кратчайший путь и обход по уровням» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
BFS и кратчайший путь в невзвешенных графах
BFS находит кратчайший путь (с наименьшим количеством рёбер) в невзвешенном графе, поскольку обходит вершины в порядке возрастания расстояния от исходной вершины. При первом достижении вершины используется кратчайший из возможных путей. Для DFS это свойство не выполняется. Для взвешенных графов с неотрицательными весами используйте алгоритм Дейкстры — BFS неявно считает, что вес каждого ребра равен 1.
from collections import deque, defaultdict
def shortest_path(graph, start, end):
if start == end:
return 0
visited = {start}
queue = deque([(start, 0)]) # (node, distance)
while queue:
node, dist = queue.popleft()
for neighbour in graph[node]:
if neighbour == end:
return dist + 1
if neighbour not in visited:
visited.add(neighbour)
queue.append((neighbour, dist + 1))
return -1 # no path found
graph = defaultdict(list)
for u, v in [(0,1),(1,2),(2,3),(0,3),(1,4)]:
graph[u].append(v); graph[v].append(u)
print(shortest_path(graph, 0, 3)) # 1 (direct edge)
print(shortest_path(graph, 0, 4)) # 2 (0->1->4)Отслеживание фактического кратчайшего пути
Чтобы восстановить сам путь, а не только его длину, поддерживайте словарь родителей, в котором фиксируется, как была достигнута каждая вершина. Достигнув конечной вершины, пройдите по карте родителей от конца к началу и разверните полученный результат. Это добавляет O(V) памяти для карты родителей, но после завершения BFS позволяет получить полный путь за время, пропорциональное его длине.
from collections import deque, defaultdict
def shortest_path_with_route(graph, start, end):
parent = {start: None}
queue = deque([start])
while queue:
node = queue.popleft()
if node == end:
break
for nb in graph[node]:
if nb not in parent:
parent[nb] = node
queue.append(nb)
if end not in parent:
return [] # no path
# Reconstruct path by tracing back
path = []
node = end
while node is not None:
path.append(node)
node = parent[node]
return path[::-1] # reverse
graph = defaultdict(list)
for u, v in [(0,1),(1,2),(2,3),(0,4),(4,3)]:
graph[u].append(v); graph[v].append(u)
print(shortest_path_with_route(graph, 0, 3)) # [0, 4, 3] or [0, 1, 2, 3]Лестница слов: BFS на неявном графе
Лестница слов (LeetCode #127) — задача о поиске минимального количества изменений одного символа, необходимых для преобразования начального слова в конечное, при условии, что каждое промежуточное слово есть в словаре. Это BFS на неявном графе, где вершины — слова, а рёбра соединяют слова, различающиеся одной буквой. Порождайте все варианты с изменением одной буквы и проверяйте, есть ли они в множестве слов. BFS гарантирует минимальную последовательность преобразований.
from collections import deque
def word_ladder(begin_word, end_word, word_list):
word_set = set(word_list)
if end_word not in word_set:
return 0
queue = deque([(begin_word, 1)])
visited = {begin_word}
while queue:
word, steps = queue.popleft()
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
new_word = word[:i] + c + word[i+1:]
if new_word == end_word:
return steps + 1
if new_word in word_set and new_word not in visited:
visited.add(new_word)
queue.append((new_word, steps + 1))
return 0
print(word_ladder('hit', 'cog', ['hot','dot','dog','lot','log','cog'])) # 5Обход по уровням: отслеживание расстояния
Обход по уровням группирует вершины по их расстоянию от исходной вершины, что непосредственно полезно в задачах, требующих обработки каждого уровня. Расстояние можно хранить в элементе очереди как кортеж (node, dist) или использовать приём с размером очереди: перед каждым уровнем запоминать размер очереди, обрабатывать ровно столько вершин, а затем увеличивать счётчик уровня. Оба подхода дают одинаковый результат.
from collections import deque, defaultdict
def bfs_levels(graph, start):
levels = {}
visited = {start}
queue = deque([start])
dist = 0
while queue:
# Process all nodes at current distance
for _ in range(len(queue)):
node = queue.popleft()
levels[node] = dist
for nb in graph[node]:
if nb not in visited:
visited.add(nb)
queue.append(nb)
dist += 1
return levels
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_levels(graph, 0)) # {0:0, 1:1, 2:1, 3:2, 4:3}Клонирование графа
Клонирование графа (LeetCode #133) создаёт глубокую копию связного неориентированного графа. Используйте BFS и хеш-таблицу, сопоставляющую исходные вершины их клонам. При первом посещении вершины создайте её клон и добавьте его в таблицу. Обрабатывая соседей, находите или создавайте их клоны и соединяйте рёбра. Хеш-таблица выполняет две задачи: отслеживает посещённые вершины и сопоставляет исходные вершины с копиями.
from collections import deque
class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
def clone_graph(node):
if not node:
return None
old_to_new = {node: Node(node.val)}
queue = deque([node])
while queue:
curr = queue.popleft()
for nb in curr.neighbors:
if nb not in old_to_new:
old_to_new[nb] = Node(nb.val)
queue.append(nb)
old_to_new[curr].neighbors.append(old_to_new[nb])
return old_to_new[node]
# Build a simple graph: 1 -- 2 -- 3 -- 4 -- 1
n1 = Node(1); n2 = Node(2); n3 = Node(3); n4 = Node(4)
n1.neighbors = [n2, n4]; n2.neighbors = [n1, n3]
n3.neighbors = [n2, n4]; n4.neighbors = [n3, n1]
cloned = clone_graph(n1)
print(cloned.val, [n.val for n in cloned.neighbors]) # 1 [2, 4]Двунаправленный BFS
Двунаправленный BFS запускается одновременно от исходной и конечной вершин, расширяя по одному уровню с каждого конца. Когда две границы поиска встречаются, кратчайший путь найден. Для больших графов это уменьшает пространство поиска с O(b^d) до O(2 * b^(d/2)), где b — коэффициент ветвления, а d — длина пути. Это даёт значительное ускорение для сильно связанных графов, например в задаче о лестнице слов с большими словарями.
from collections import defaultdict
def word_ladder_bidir(begin, end, word_list):
word_set = set(word_list)
if end not in word_set:
return 0
front, back = {begin}, {end}
visited = {begin, end}
steps = 1
while front and back:
# Always expand the smaller frontier
if len(front) > len(back):
front, back = back, front
next_front = set()
for word in front:
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
nw = word[:i] + c + word[i+1:]
if nw in back: # frontiers met!
return steps + 1
if nw in word_set and nw not in visited:
visited.add(nw)
next_front.add(nw)
front = next_front
steps += 1
return 0
print(word_ladder_bidir('hit','cog',['hot','dot','dog','lot','log','cog'])) # 5BFS 0–1 для взвешенных графов
BFS 0–1 работает с графами, в которых веса рёбер равны только 0 или 1. Вместо обычной очереди используйте двустороннюю очередь: добавляйте в конец рёбра с весом 1 (следующий уровень), а в начало — рёбра с весом 0 (тот же уровень). Это даёт поиск кратчайшего пути за O(V + E) — быстрее, чем O((V+E) log V) у алгоритма Дейкстры, когда веса принимают только два значения. Такой подход часто используется в задачах с сетками, где одни перемещения бесплатны, а другие стоят 1.
from collections import deque
def zero_one_bfs(graph, start, n):
# graph: list of (neighbour, weight) where weight is 0 or 1
dist = [float('inf')] * n
dist[start] = 0
dq = deque([start])
while dq:
node = dq.popleft()
for nb, w in graph[node]:
if dist[node] + w < dist[nb]:
dist[nb] = dist[node] + w
if w == 0:
dq.appendleft(nb) # same level
else:
dq.append(nb) # next level
return dist
# Simple test:
graph = [[(1, 0), (2, 1)], # node 0: free to 1, cost 1 to 2
[(3, 1)], # node 1: cost 1 to 3
[(3, 0)], # node 2: free to 3
[]]
print(zero_one_bfs(graph, 0, 4)) # [0, 0, 1, 1]Стены и ворота (BFS из нескольких источников)
Стены и ворота заполняет каждую пустую комнату расстоянием до ближайших ворот. Используйте BFS из нескольких источников: одновременно инициализируйте очередь всеми воротами (со значением 0) и расширяйте поиск наружу. Значение каждой ячейки устанавливается равным уровню, на котором она была достигнута впервые. Это решение за O(mn) эффективнее, чем отдельный запуск BFS из каждой пустой комнаты, который занял бы O(m²n²).
from collections import deque
def walls_and_gates(rooms):
if not rooms:
return
rows, cols = len(rooms), len(rooms[0])
INF = float('inf')
queue = deque()
# Multi-source: all gates at distance 0
for r in range(rows):
for c in range(cols):
if rooms[r][c] == 0: # gate
queue.append((r, c))
dirs = [(0,1),(0,-1),(1,0),(-1,0)]
while queue:
r, c = queue.popleft()
for dr, dc in dirs:
nr, nc = r+dr, c+dc
if 0<=nr<rows and 0<=nc<cols and rooms[nr][nc]==INF:
rooms[nr][nc] = rooms[r][c] + 1
queue.append((nr, nc))
rooms = [[float('inf'),-1,0,float('inf')],
[float('inf'),float('inf'),float('inf'),-1],
[float('inf'),-1,float('inf'),-1],
[0,-1,float('inf'),float('inf')]]
walls_and_gates(rooms)
print(rooms[0][0], rooms[1][1]) # 3, 2BFS для игры «Змеи и лестницы»
Змеи и лестницы (LeetCode #909) — задача на поиск кратчайшего пути с помощью BFS на числовой доске. Представьте доску как невзвешенный граф, где с любой клетки можно переместиться на 1–6 клеток и попасть на змею или лестницу, которые телепортируют игрока. BFS находит минимальное количество бросков кубика. Главная сложность — преобразование между одномерной позицией и двумерными координатами доски с учётом зигзагообразной раскладки, в которой направление строк чередуется.
from collections import deque
def snakes_and_ladders(board):
n = len(board)
def get_board(pos):
r, c = divmod(pos - 1, n)
if r % 2 == 1: c = n - 1 - c # alternating direction
return board[n - 1 - r][c]
visited = {1}
queue = deque([(1, 0)])
while queue:
pos, moves = queue.popleft()
for dice in range(1, 7):
next_pos = pos + dice
if next_pos > n * n:
break
val = get_board(next_pos)
if val != -1:
next_pos = val # snake or ladder
if next_pos == n * n:
return moves + 1
if next_pos not in visited:
visited.add(next_pos)
queue.append((next_pos, moves + 1))
return -1
print('BFS models game as an unweighted shortest-path problem')Сложность и оптимизация BFS
Временная сложность BFS равна O(V + E), поскольку каждая вершина добавляется в очередь один раз, а каждое ребро проверяется постоянное число раз. Пространственная сложность равна O(V) из-за множества посещённых вершин и очереди. Для графов-сеток V = m*n и E = 4*m*n (у каждой ячейки 4 соседа), поэтому BFS на сетке выполняется за O(mn). Важная оптимизация: используйте множество посещённых вершин (проверка за O(1)), а не список (проверка за O(n)). Отмечайте вершину как посещённую при добавлении в очередь, а не при извлечении.
# BFS on a graph with V vertices and E edges:
# Time: O(V + E) -- each vertex and edge visited once
# Space: O(V) -- visited set + queue
# BFS on an m x n grid:
# V = m*n cells
# E <= 4*m*n edges (4 directions, max)
# Time: O(m*n)
# Space: O(m*n)
# Common pitfalls:
# 1. Marking visited on dequeue (not enqueue) -> same node queued multiple times
# 2. Using a list for visited -> O(n) membership check -> O(V*E) total
# 3. Not handling disconnected graph -> BFS from single source misses components
print('O(V+E) time, O(V) space -- mark visited on enqueue')Ближайший 0 в двоичной матрице
Матрица 01 (LeetCode #542) находит расстояние от каждой ячейки до ближайшего 0. BFS из нескольких источников, запущенный одновременно из всех нулей, даёт оптимальное решение за O(mn). Инициализируйте очередь всеми ячейками со значением 0 и расстоянием 0, а для всех ячеек со значением 1 установите бесконечное расстояние. BFS распространяет расстояния от нулей наружу, устанавливая расстояние каждой ячейки со значением 1 при первом достижении — гарантированно кратчайшее.
from collections import deque
def update_matrix(mat):
rows, cols = len(mat), len(mat[0])
dist = [[float('inf')] * cols for _ in range(rows)]
queue = deque()
for r in range(rows):
for c in range(cols):
if mat[r][c] == 0:
dist[r][c] = 0
queue.append((r, c))
dirs = [(0,1),(0,-1),(1,0),(-1,0)]
while queue:
r, c = queue.popleft()
for dr, dc in dirs:
nr, nc = r+dr, c+dc
if 0<=nr<rows and 0<=nc<cols:
if dist[r][c] + 1 < dist[nr][nc]:
dist[nr][nc] = dist[r][c] + 1
queue.append((nr, nc))
return dist
mat = [[0,0,0],[0,1,0],[1,1,1]]
result = update_matrix(mat)
for row in result: print(row) # [[0,0,0],[0,1,0],[1,2,1]]Быстрая проверка
Проверьте понимание концепций «Структуры данных и алгоритмы — подготовка к собеседованию по программированию» из этого урока.
Итоги урока
В этом уроке вы узнали о BFS для поиска кратчайшего пути в невзвешенных графах с отслеживанием родителей для восстановления маршрута, о лестнице слов как классическом примере BFS на неявном графе, о двунаправленном BFS для больших графов и о BFS из нескольких источников для задач с несколькими начальными точками. Далее мы применим DFS для поиска связных компонент и выполнения заливки.
Часто задаваемые вопросы
Урок «BFS: кратчайший путь и обход по уровням» бесплатный?
Да — полный текст урока «BFS: кратчайший путь и обход по уровням» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «BFS: кратчайший путь и обход по уровням»?
Используйте BFS для поиска кратчайшего пути в невзвешенном графе, решайте задачу word-ladder по уровням и клонируйте граф с помощью хеш-таблицы Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «BFS: кратчайший путь и обход по уровням»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Представление графов и настройка обхода
- BFS: кратчайший путь и обход по уровням
- DFS: компоненты связности и заливка
- Обнаружение циклов в ориентированных и неориентированных графах