BFS: najkrótsza ścieżka i przejście poziomami
Wykorzystają Państwo BFS do znalezienia najkrótszej ścieżki w grafie nieważonym, rozwiążą zadanie word-ladder poziomami i sklonują graf za pomocą mapy haszującej.
BFS: najkrótsza ścieżka i przejście poziomami to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DSA Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.
BFS i najkrótsza ścieżka w grafach nieważonych
BFS znajduje najkrótszą ścieżkę (o najmniejszej liczbie krawędzi) w grafie nieważonym, ponieważ odwiedza wierzchołki w kolejności rosnącej odległości od źródła. Pierwsze dotarcie do wierzchołka podczas BFS odbywa się najkrótszą możliwą ścieżką. Ta właściwość nie zachodzi w przypadku DFS. Dla grafów ważonych z nieujemnymi wagami należy użyć algorytmu Dijkstry — BFS domyślnie traktuje wszystkie krawędzie tak, jakby miały wagę 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)Śledzenie rzeczywistej najkrótszej ścieżki
Aby odtworzyć rzeczywistą ścieżkę, a nie tylko jej długość, należy użyć słownika rodziców, który zapisuje, w jaki sposób osiągnięto każdy wierzchołek. Po dotarciu do celu należy prześledzić mapę rodziców od końca do początku, a następnie odwrócić wynik. Zwiększa to zużycie pamięci o O(V) na mapę rodziców, ale po zakończeniu BFS pozwala uzyskać pełną ścieżkę w czasie O(długości ścieżki).
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]Word Ladder: BFS w grafie niejawnym
Word Ladder (LeetCode #127) wymaga znalezienia minimalnej liczby jednoliterowych zmian potrzebnych do przekształcenia słowa początkowego w słowo końcowe, przy czym każde słowo pośrednie musi znajdować się w słowniku. Jest to BFS w grafie niejawnym, w którym wierzchołkami są słowa, a krawędzie łączą słowa różniące się jedną literą. Należy wygenerować wszystkie jednoliterowe mutacje i sprawdzać, czy znajdują się one w zbiorze słów. BFS gwarantuje znalezienie minimalnej sekwencji przekształceń.
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'])) # 5Przechodzenie poziomami: śledzenie odległości
Przechodzenie poziomami grupuje wierzchołki według ich odległości od źródła, co jest bezpośrednio przydatne w zadaniach wymagających przetwarzania każdego poziomu osobno. Odległość można śledzić, zapisując ją w elemencie kolejki jako krotkę (node, dist) albo korzystając z techniki rozmiaru kolejki (przed każdym poziomem należy zapisać rozmiar kolejki, przetworzyć dokładnie tyle wierzchołków, a następnie zwiększyć licznik poziomu). Oba podejścia dają identyczne wyniki.
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}Clone Graph
Clone Graph (LeetCode #133) tworzy głęboką kopię spójnego grafu nieskierowanego. Należy użyć BFS oraz mapy haszującej mapującej oryginalne wierzchołki na ich kopie. Przy pierwszym odwiedzeniu wierzchołka należy utworzyć jego kopię i dodać ją do mapy. Podczas przetwarzania sąsiadów należy wyszukać ich kopie lub je utworzyć, a następnie połączyć krawędziami. Mapa haszująca pełni podwójną funkcję: śledzi odwiedzone wierzchołki i mapuje oryginały na kopie.
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 dwukierunkowy
BFS dwukierunkowy rozpoczyna przeszukiwanie jednocześnie od źródła i celu, rozwijając po jednym poziomie z każdego końca. Gdy oba fronty się spotkają, znaleziona zostaje najkrótsza ścieżka. W dużych grafach zmniejsza to przestrzeń przeszukiwania z O(b^d) do O(2 * b^(d/2)), gdzie b oznacza współczynnik rozgałęzienia, a d długość ścieżki — jest to znaczne usprawnienie w przypadku silnie połączonych grafów, takich jak graf Word Ladder z dużymi słownikami.
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 dla grafów ważonych
BFS 0-1 obsługuje grafy, w których wagi krawędzi wynoszą wyłącznie 0 lub 1. Zamiast zwykłej kolejki należy użyć deque: dla krawędzi o wadze 1 dodawać element na końcu (następny poziom), a dla krawędzi o wadze 0 — na początku (ten sam poziom). Pozwala to obliczać najkrótsze ścieżki w czasie O(V + E), czyli szybciej niż algorytm Dijkstry działający w czasie O((V+E) log V), gdy wagi są binarne. Podejście to jest często używane w zadaniach dotyczących siatek, w których niektóre ruchy są darmowe, a inne kosztują 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]Walls and Gates (BFS z wieloma źródłami)
Walls and Gates wypełnia każde puste pomieszczenie odległością do najbliższej bramy. Należy użyć BFS z wieloma źródłami: jednocześnie zainicjalizować kolejkę wszystkimi bramami (wartość 0) i rozpocząć rozwijanie przeszukiwania na zewnątrz. Wartość każdej komórki jest ustawiana na poziom, na którym zostaje ona po raz pierwszy osiągnięta. To rozwiązanie o złożoności O(mn) jest wydajniejsze niż osobne uruchamianie BFS z każdego pustego pomieszczenia, które miałoby złożoność 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 w zadaniu Snakes and Ladders
Snakes and Ladders (LeetCode #909) to problem znajdowania najkrótszej ścieżki za pomocą BFS na ponumerowanej planszy. Planszę należy zamodelować jako graf nieważony, w którym z dowolnego pola można wykonać rzut o 1–6 pól, a następnie trafić na węża lub drabinę, które teleportują gracza. BFS znajduje minimalną liczbę rzutów kostką. Kluczowym wyzwaniem jest konwersja między pozycją jednowymiarową a współrzędnymi planszy 2D z uwzględnieniem układu boustrofedonowego (naprzemiennego kierunku wierszy).
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')Złożoność i optymalizacje BFS
Złożoność czasowa BFS wynosi O(V + E), ponieważ każdy wierzchołek jest dodawany do kolejki raz, a każda krawędź jest analizowana stałą liczbę razy. Złożoność pamięciowa wynosi O(V) ze względu na zbiór odwiedzonych i kolejkę. W grafach reprezentowanych przez siatkę V = m*n, a E = 4*m*n (każda komórka ma 4 sąsiadów), więc BFS na siatce działa w czasie O(mn). Kluczowa optymalizacja: należy używać zbioru odwiedzonych (sprawdzanie w czasie O(1)), a nie listy (sprawdzanie w czasie O(n)). Wierzchołek należy oznaczać jako odwiedzony przy dodawaniu do kolejki, a nie przy usuwaniu z niej.
# 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')Najbliższe 0 w macierzy binarnej
01 Matrix (LeetCode #542) znajduje odległość od każdej komórki do najbliższego zera. BFS z wieloma źródłami, uruchomiony jednocześnie dla wszystkich zer, zapewnia optymalne rozwiązanie o złożoności O(mn). Należy zainicjalizować kolejkę wszystkimi komórkami zawierającymi 0 w odległości 0, a wszystkim komórkom zawierającym 1 przypisać odległość równą nieskończoności. BFS propaguje odległości na zewnątrz od zer, ustawiając odległość każdej komórki zawierającej 1 przy pierwszym dotarciu do niej, co gwarantuje najkrótszą odległość.
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]]Szybki test
Sprawdź swoje zrozumienie zagadnień z Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.
Podsumowanie lekcji
W tej lekcji nauczyli się Państwo: zastosowania BFS do znajdowania najkrótszych ścieżek w grafach nieważonych wraz ze śledzeniem rodziców w celu odtworzenia trasy, Word Ladder jako wzorcowego zastosowania BFS w grafie niejawnym, BFS dwukierunkowego w dużych grafach oraz BFS z wieloma źródłami w zadaniach z wieloma punktami początkowymi. Następnie zastosujemy DFS do znajdowania składowych spójnych i wykonywania operacji flood fill.
Ucz się Python dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 30
- Lekcje
- 120
Często zadawane pytania
Czy lekcja „BFS: najkrótsza ścieżka i przejście poziomami” jest bezpłatna?
Tak — pełny tekst „BFS: najkrótsza ścieżka i przejście poziomami” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DSA Interview Prep, przejdź na CoddyKit PRO. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.
Co nauczysz się w „BFS: najkrótsza ścieżka i przejście poziomami”?
Wykorzystają Państwo BFS do znalezienia najkrótszej ścieżki w grafie nieważonym, rozwiążą zadanie word-ladder poziomami i sklonują graf za pomocą mapy haszującej. Ćwiczysz DSA Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć DSA Interview Prep?
Nie wymagamy żadnego doświadczenia. DSA Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.
Ile czasu zajmuje lekcja „BFS: najkrótsza ścieżka i przejście poziomami”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji DSA Interview Prep?
Tak. Każda lekcja DSA Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Reprezentacje grafów i przygotowanie przejść
- BFS: najkrótsza ścieżka i przejście poziomami
- DFS: spójne składowe i flood fill
- Wykrywanie cykli w grafach skierowanych i nieskierowanych