Reprezentacje grafów i przygotowanie przejść
Zbudują Państwo grafy skierowane i nieskierowane za pomocą list sąsiedztwa, zainicjalizują BFS z deque oraz DFS ze stosem lub rekurencją, obsługując śledzenie odwiedzonych węzłów.
Reprezentacje grafów i przygotowanie przejść to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 1 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.
Czym jest graf?
Graf to zbiór węzłów (wierzchołków) połączonych przez krawędzie. W przeciwieństwie do drzew grafy mogą zawierać cykle, wiele ścieżek między węzłami oraz niespójne składowe. Grafy służą do modelowania rzeczywistych systemów, takich jak sieci społecznościowe, mapy dróg, drzewa zależności i odnośniki między stronami internetowymi. Niemal każda nietrywialna rozmowa kwalifikacyjna dotycząca projektowania systemów i algorytmów obejmuje grafy — opanowanie ich reprezentacji i przeszukiwania jest niezbędne.
# 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')Reprezentacja za pomocą listy sąsiedztwa
Lista sąsiedztwa przechowuje listę sąsiadów każdego wierzchołka. W Pythonie należy użyć obiektu dict mapującego każdy wierzchołek na listę sąsiednich wierzchołków. Jest to najczęściej używana reprezentacja w zadaniach rekrutacyjnych: zajmuje O(V + E) pamięci (jest wydajna w przypadku grafów rzadkich), umożliwia iterowanie po sąsiadach w czasie O(stopnia) oraz sprawdzanie sąsiedztwa w średnim czasie O(1) w wariancie opartym na zbiorze haszującym. Większość zadań grafowych na LeetCode korzysta z tego formatu.
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 graphReprezentacja za pomocą macierzy sąsiedztwa
Macierz sąsiedztwa to dwuwymiarowa tablica V×V, w której matrix[i][j] = 1 (lub waga krawędzi), jeśli istnieje krawędź z wierzchołka i do wierzchołka j, a w przeciwnym razie znajduje się 0. Zapewnia sprawdzanie istnienia krawędzi w czasie O(1), ale zużywa O(V²) pamięci niezależnie od liczby krawędzi — jest to nieefektywne w przypadku grafów rzadkich. Preferuje się ją, gdy graf jest gęsty (ma wiele krawędzi) lub gdy kluczowe jest szybkie sprawdzanie istnienia krawędzi, na przykład w algorytmie Floyda-Warshalla do znajdowania najkrótszych ścieżek między wszystkimi parami wierzchołków.
# 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 betterReprezentacja za pomocą listy krawędzi
Lista krawędzi to najprostsza reprezentacja: zwykła lista krotek (źródło, cel), opcjonalnie zawierających wagi. Zajmuje O(E) pamięci i umożliwia łatwe iterowanie po wszystkich krawędziach. Znalezienie sąsiadów danego wierzchołka wymaga jednak przeszukania wszystkich krawędzi, czyli O(E). Listy krawędzi są używane w algorytmach grafowych, które dokładnie iterują po wszystkich krawędziach, takich jak Bellman-Ford (relaksacja wszystkich krawędzi n-1 razy) oraz algorytm Kruskala do znajdowania minimalnego drzewa rozpinającego.
# 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)Konfiguracja BFS: kolejka i zbiór odwiedzonych
BFS (Breadth-First Search) przeszukuje graf poziomami, korzystając z kolejki. Kluczowym elementem jest zbiór odwiedzonych, który zapobiega ponownemu odwiedzaniu wierzchołków w grafach zawierających cykle. Bez zbioru odwiedzonych BFS w grafie cyklicznym zapętliłby się na zawsze. Standardowa konfiguracja polega na zainicjalizowaniu kolejki wierzchołkiem źródłowym, oznaczeniu go jako odwiedzonego, a następnie wielokrotnym usuwaniu elementu z kolejki, przetwarzaniu go i dodawaniu do kolejki nieodwiedzonych sąsiadów.
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]Konfiguracja DFS: stos lub rekurencja
DFS (Depth-First Search) eksploruje każdą gałąź tak głęboko, jak to możliwe, a następnie cofa się. Można go zaimplementować rekurencyjnie (z użyciem stosu wywołań) lub iteracyjnie (z użyciem jawnego stosu). W obu przypadkach w grafach cyklicznych potrzebny jest zbiór odwiedzonych. Wersja iteracyjna dodaje sąsiadów w odwrotnej kolejności, aby odwzorować kolejność przechodzenia rekurencyjnego DFS, choć kolejność eksploracji może się różnić w obu implementacjach.
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))Kiedy używać BFS, a kiedy DFS
Należy wybrać BFS, gdy potrzebna jest najkrótsza ścieżka (najmniejsza liczba krawędzi) w grafie nieważonym lub gdy wierzchołki trzeba przetwarzać poziomami. DFS należy wybrać, gdy trzeba odwiedzić wszystkie osiągalne wierzchołki, wykrywać cykle, znajdować składowe spójne, wykonywać sortowanie topologiczne lub wyliczać wszystkie ścieżki. W praktyce: BFS służy do problemów typu „najkrótsza ścieżka/minimalna liczba przeskoków”, a DFS do problemów typu „istnienie/osiągalność/wyliczanie”.
# 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')Graf w formatach danych wejściowych LeetCode
Zadania grafowe na LeetCode mogą korzystać z różnych formatów danych wejściowych. Lista krawędzi: [[0,1],[0,2]] — należy zbudować listę sąsiedztwa. Lista sąsiedztwa oparta na indeksach: graph[i] to lista sąsiadów wierzchołka i. Siatka/macierz: dwuwymiarowa tablica m×n, w której komórki są wierzchołkami, a sąsiednie komórki (w górę, w dół, w lewo i w prawo) są sąsiadami. Wierzchołek z dziećmi: niestandardowe klasy, takie jak Node(val, neighbors). Należy rozpoznawać te formaty i w pierwszym kroku konwertować je na listę sąsiedztwa.
# 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))Oznaczanie odwiedzonych pól w siatkach
W zadaniach dotyczących siatek istnieją dwa sposoby śledzenia odwiedzonych komórek. Opcja A: użycie osobnego zbioru visited krotek (row, col) — O(m*n) dodatkowej pamięci. Opcja B: modyfikowanie siatki w miejscu przez oznaczanie odwiedzonych komórek wartością specjalną (np. '#' lub 2) i przywrócenie ich później, jeśli jest to potrzebne. Podejście modyfikujące dane w miejscu zużywa O(1) dodatkowej pamięci i jest często stosowane w zadaniach typu flood fill oraz Number of Islands.
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)) # 3Inicjalizacja BFS z wieloma źródłami
BFS z wieloma źródłami rozpoczyna się jednocześnie z wielu wierzchołków: kolejkę inicjalizuje się wszystkimi wierzchołkami źródłowymi oznaczonymi jako odwiedzone. Stosuje się go w zadaniach takich jak „odległość od najbliższego zera”, „gnijące pomarańcze” i „ściany i bramy”, gdy potrzebna jest najkrótsza odległość od dowolnego wierzchołka źródłowego. BFS z wieloma źródłami działa w czasie O(V + E), czyli tak samo jak BFS z jednym źródłem, ponieważ każdy wierzchołek jest nadal odwiedzany najwyżej raz.
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]])) # 4Gęstość grafu a wybór reprezentacji
Wybór między listą sąsiedztwa a macierzą zależy od gęstości grafu — stosunku E/V². Graf rzadki (E << V²) zyskuje na zastosowaniu list sąsiedztwa: zajmują one O(V+E) pamięci w porównaniu z O(V²) w przypadku macierzy. Graf gęsty (E ≈ V²) zyskuje na zastosowaniu macierzy sąsiedztwa: sprawdzanie krawędzi zajmuje O(1), w porównaniu z O(stopnia) dla list. W zadaniach rekrutacyjnych listy sąsiedztwa są niemal zawsze właściwym wyborem, ponieważ większość zadań dotyczy grafów rzadkich.
# 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')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: trzech reprezentacji grafów (listy sąsiedztwa, macierzy i listy krawędzi) oraz tego, kiedy wybrać każdą z nich, konfiguracji BFS i DFS ze zbiorami odwiedzonych zapobiegającymi nieskończonym pętlom w grafach cyklicznych, a także praktycznych wzorców, takich jak oznaczanie komórek siatki w miejscu i BFS z wieloma źródłami. Następnie zastosujemy BFS do znajdowania najkrótszych ścieżek i przechodzenia grafu poziomami.
Często zadawane pytania
Czy lekcja „Reprezentacje grafów i przygotowanie przejść” jest bezpłatna?
Tak — pełny tekst „Reprezentacje grafów i przygotowanie przejść” 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 „Reprezentacje grafów i przygotowanie przejść”?
Zbudują Państwo grafy skierowane i nieskierowane za pomocą list sąsiedztwa, zainicjalizują BFS z deque oraz DFS ze stosem lub rekurencją, obsługując śledzenie odwiedzonych węzłów. Ć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 1 z 4.
Ile czasu zajmuje lekcja „Reprezentacje grafów i przygotowanie przejść”?
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