본문 바로가기
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

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

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