BFS: 최단 경로와 레벨 순회
BFS로 가중치가 없는 그래프의 최단 경로를 찾고, word-ladder를 레벨별로 해결하며, 해시 맵을 사용해 그래프를 복제합니다.
BFS: 최단 경로와 레벨 순회은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Coding Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
비가중치 그래프에서의 BFS와 최단 경로
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가 끝난 후 O(경로 길이) 시간에 전체 경로를 얻을 수 있습니다.
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는 시작점과 도착점 양쪽에서 동시에 BFS를 시작하고, 각 끝에서 한 번에 한 레벨씩 확장합니다. 두 탐색 경계가 만나면 최단 경로를 찾은 것입니다. 큰 그래프에서는 분기 계수가 b이고 경로 길이가 d일 때 탐색 공간을 O(b^d)에서 O(2 * b^(d/2))로 줄입니다. 따라서 큰 사전을 사용하는 단어 사다리처럼 연결성이 높은 그래프에서 크게 향상됩니다.
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'])) # 5가중치 그래프를 위한 0-1 BFS
0-1 BFS는 간선 가중치가 0 또는 1뿐인 그래프를 처리합니다. 일반 큐 대신 덱을 사용합니다. 가중치가 1인 간선에는 뒤쪽에 append하여 다음 레벨로 보내고, 가중치가 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, 2뱀과 사다리 BFS
뱀과 사다리(LeetCode #909)는 숫자가 매겨진 격자에서의 BFS 최단 경로 문제입니다. 보드를 비가중치 그래프로 모델링하면 어느 칸에서든 1~6만큼 주사위를 굴릴 수 있고, 뱀이나 사다리에 도착하면 다른 곳으로 이동할 수 있습니다. BFS는 필요한 주사위 굴리기 횟수의 최솟값을 찾습니다. 핵심 과제는 지그재그(행 방향이 번갈아 바뀌는) 배치를 고려하여 1차원 위치와 2차원 보드 좌표를 서로 변환하는 것입니다.
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까지의 거리를 구합니다. 모든 0에서 동시에 시작하는 다중 출발점 BFS가 최적인 O(mn) 해법을 제공합니다. 모든 0인 셀을 거리 0으로 큐에 넣고, 모든 1인 셀은 거리를 무한대로 초기화합니다. BFS는 0인 셀에서 바깥쪽으로 거리를 전파하며, 각 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 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“BFS: 최단 경로와 레벨 순회”에서 뭘 배우나요?
BFS로 가중치가 없는 그래프의 최단 경로를 찾고, word-ladder를 레벨별로 해결하며, 해시 맵을 사용해 그래프를 복제합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“BFS: 최단 경로와 레벨 순회” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 그래프 표현과 순회 설정
- BFS: 최단 경로와 레벨 순회
- DFS: 연결 요소와 플러드 필
- 방향 그래프와 무방향 그래프의 순환 탐지