본문 바로가기
C.W.K.
Stream
Lesson 03 of 06 · published

너비 우선 탐색: 물결처럼 퍼져 나가기

~12 min · graphs, bfs, shortest-path

Level 0호기심 많은 입문자
0 XP0/85 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"BFS는 연못에 이는 물결처럼 시작점에서 퍼져 나가. 두 걸음 떨어진 곳으로 가기 전에 한 걸음 떨어진 곳을 전부 끝내지. 그 물결이 어떤 자리에 처음 닿는 순간이 곧 최단 경로인 이유가 여기 있어."

알고리즘: 큐와 방문 집합

너비 우선 탐색은 스택과 큐 트랙에서 본 그 큐 기반 경계를 이제 진짜 그래프 위에 올린 거야. 순서는 이래.

  1. 시작 노드를 큐에 넣고 방문 표시를 해.
  2. 노드를 하나 꺼내서 처리해.
  3. 아직 방문하지 않은 이웃마다 방문 표시를 하고 큐에 넣어.
  4. 큐가 빌 때까지 반복해.

큐가 먼저 들어온 걸 먼저 내보내니까 언제나 발견한 순서대로 노드를 처리해. 시작점에서의 거리 순으로 엄격하게 말이야. 한 걸음 떨어진 노드를 전부 보고, 그다음 두 걸음, 이런 식이지. 경계가 동심원처럼 넓어지는 거야.

왜 최단 경로를 찾을까

이 한 겹씩 퍼지는 방식이 BFS의 초능력이야. BFS가 어떤 노드에 처음 닿는 순간, 그건 가능한 가장 적은 간선으로 도착한 거야. 더 가까운 경로가 나중에 발견될 수가 없거든. BFS는 바깥으로 나가기 전에 각 거리의 노드를 전부 소진하니까. 그래서 가중치 없는 그래프에서는 BFS가 최단 경로를 공짜로 풀어줘. 가면서 각 노드의 부모를 기록해 두면 목표 지점에서 부모를 거슬러 올라가 실제 최단 경로까지 복원할 수 있고. "최소 이동 횟수", "가장 짧은 연결 고리", "가장 가까운 조건 만족 노드"는 전부 BFS야.

BFS는 큐로 경계를 관리하면서 시작점에서 거리 순으로 한 겹씩 퍼져 나가. 어떤 노드에 처음 닿는 게 곧 간선 수가 가장 적은 경로라서, BFS가 가중치 없는 최단 경로를 풀어. 그리고 방문 집합은 반드시 들고 다녀. 그래프에는 순환이 있으니까.

다들 부딪히는 두 가지 버그

첫째, 순환이 있거나 여러 경로가 있는 그래프에서는 방문 관리가 반드시 필요해. 둘째, 보통의 단일 출발 BFS는 enqueue하는 시점에 발견 표시를 해서 같은 노드가 큐에 여러 번 들어가는 걸 막아. dequeue할 때까지 미루면 중복해서 큐에 쌓이니 시간과 메모리가 늘고, 부모를 덮어쓰는 구현이라면 경로 기록까지 흐트러질 수 있어. 다만 그렇다고 거리 값 자체가 언제나 틀린다고 일반화하진 마. 어떤 불변식을 쓰느냐에 따라 늦게 표시해도 답은 맞을 수 있어. 비효율적일 뿐이지.

피파의 고백

내가 처음 짠 BFS는 dequeue할 때 방문 표시를 했는데, 조밀한 그래프에서 큐가 부풀어 올랐어. 같은 노드가 다섯 번씩 들어앉아 있더라. 아빠의 수정은 줄 하나를 옮기는 거였어. enqueue할 때 방문 표시를 하는 것. 그러니까 각 노드가 경계에 한 번씩만 들어가고 전체가 깔끔한 O(V+E)가 됐지. 그래프 코드에서는 방문 표시를 한다는 사실만큼이나 언제 하느냐가 중요하다는 걸 그때 배웠어. 줄 하나의 위치가 우아함과 제곱 복잡도를 가르더라.

Code

부모를 기록해 경로까지 복원하는 BFS 최단 경로·python
from collections import deque

graph = {
    "A": ["B", "C"], "B": ["A", "D", "E"], "C": ["A", "F"],
    "D": ["B"], "E": ["B", "F"], "F": ["C", "E"],
}

def bfs_shortest(graph, start, goal):
    """가중치 없는 그래프의 최단 경로 (최소 엣지). O(V + E)."""
    visited = {start}                  # ENQUEUE 시점에 표시
    parent = {start: None}             # 경로 재구성용
    q = deque([start])
    while q:
        node = q.popleft()             # FIFO -> 거리순 탐색
        if node == goal:
            break
        for nb in graph[node]:
            if nb not in visited:
                visited.add(nb)        # 다시-큐 되기 전에 지금 표시
                parent[nb] = node
                q.append(nb)
    # goal 에서 부모를 거슬러 걸어 경로 재구성
    if goal not in parent: return None
    path, cur = [], goal
    while cur is not None:
        path.append(cur); cur = parent[cur]
    return path[::-1]

print(bfs_shortest(graph, "A", "F"))   # ['A', 'C', 'F'] — 엣지 2개, 최단

External links

Exercise

그래프 A–B, A–C, B–D, C–D, D–E에서 A부터 BFS를 손으로 돌려 봐. 노드가 dequeue되는 순서와 A에서 각 노드까지의 최단 거리를 적어. 그다음 '소셜 네트워크에서 두 사용자가 최소 몇 단계로 이어지는가'에 왜 DFS가 아니라 BFS가 맞는 선택인지, 그리고 실제 경로까지 돌려주려면 무슨 데이터를 더 기록해야 하는지 설명해.
Hint
dequeue 순서는 A, B, C, D, E야. 거리는 A가 0, B와 C가 1, D가 2, E가 3이고. BFS는 거리 순으로 훑으니까 처음 도착하는 게 곧 최소 단계야. DFS는 더 긴 길로 D에 먼저 닿을 수도 있고. 실제 경로를 돌려주려면 노드마다 부모 포인터를 기록해 뒀다가 거슬러 올라가면 돼.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.