본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Algorithm

너비 우선 탐색 (BFS)

작성 2026. 10. 13.추가
목차6개 항목

BFS(Breadth-First Search, 너비 우선 탐색)는 그래프에서 시작 노드와 가까운 노드부터 차례로 탐색하는 알고리즘이다. 물결이 퍼지듯 거리 1인 노드를 모두 본 뒤 거리 2, 거리 3으로 넓혀 가므로, 모든 간선의 비용이 같은 그래프에서 최단 거리를 구할 때 가장 먼저 떠올려야 할 방법이다. 미로 탈출 최단 경로, 감염·전파 범위 계산 등에 쓰인다.

동작 과정

가까운 노드부터 처리하려면 먼저 넣은 것을 먼저 꺼내는 큐(Queue)가 필요하다.

  1. 시작 노드를 큐에 넣고 방문 처리한다.
  2. 큐에서 노드를 하나 꺼내, 아직 방문하지 않은 인접 노드를 모두 큐에 넣고 방문 처리한다.
  3. 큐가 빌 때까지 2를 반복한다.

방문 처리는 같은 노드를 큐에 두 번 넣지 않도록 표시하는 것이다. 꺼낼 때가 아니라 넣을 때 표시해야 중복 삽입이 생기지 않는다.

예제 그래프

위 그래프에서 노드 1부터 시작하고, 인접 노드가 여러 개면 번호가 작은 것부터 처리한다고 하자.

단계큐에서 꺼낸 노드새로 넣은 노드큐 상태
시작-1[1]
112, 3[2, 3]
228[3, 8]
334, 5[8, 4, 5]
486, 7[4, 5, 6, 7]
5~4, 5, 6, 7없음[]

탐색 순서는 1 → 2 → 3 → 8 → 4 → 5 → 6 → 7이다.

기본 구현

from collections import deque
 
def bfs(graph, start, visited):
    queue = deque([start])
    visited[start] = True
    while queue:
        v = queue.popleft()
        print(v, end=' ')
        for nxt in graph[v]:
            if not visited[nxt]:
                visited[nxt] = True
                queue.append(nxt)
 
# 인덱스 = 노드 번호 (0번은 비워 둔다)
graph = [
    [],
    [2, 3],
    [1, 8],
    [1, 4, 5],
    [3, 5],
    [3, 4],
    [7, 8],
    [6, 8],
    [2, 6, 7],
]
visited = [False] * 9
bfs(graph, 1, visited)  # 1 2 3 8 4 5 6 7

큐는 반드시 collections.deque를 쓴다. list.pop(0)은 꺼낼 때마다 나머지 원소를 앞으로 당기느라 O(n)이 걸려, 노드가 많으면 전체가 O(V²)로 느려진다.

노드 V개, 간선 E개일 때 인접 리스트 기준 시간 복잡도는 O(V + E)다. 메모리는 큐에 한 "층(level)"의 노드가 한꺼번에 들어가므로 그래프의 너비에 비례한다.

격자(미로) BFS 패턴

방문 여부 대신 거리를 기록하면 바로 최단 거리 표가 된다. 다음 칸의 거리 = 현재 칸의 거리 + 1이다.

코딩 테스트에서 가장 흔한 형태는 N×M 격자에서 상하좌우로 이동하는 문제다. 좌표를 노드로, 인접한 칸을 간선으로 본다. 미로(0 통로, 1 벽)에서 출발 칸부터 도착 칸까지 지나야 하는 최소 칸 수를 구하는 문제로 핵심 루프를 보자.

from collections import deque
 
def shortest_cells(board, n, start, goal):
    dist = [[-1] * n for _ in range(n)]      # -1 = 미방문, 그 외 = 출발점에서의 거리
    dist[start[0]][start[1]] = 0
    queue = deque([start])
    while queue:
        ci, cj = queue.popleft()
        if (ci, cj) == goal:
            return dist[ci][cj] - 1          # 출발·도착 칸을 뺀 '지나는 칸' 수
        for di, dj in ((-1, 0), (1, 0), (0, -1), (0, 1)):
            ni, nj = ci + di, cj + dj
            if 0 <= ni < n and 0 <= nj < n and dist[ni][nj] == -1 and board[ni][nj] != '1':
                dist[ni][nj] = dist[ci][cj] + 1
                queue.append((ni, nj))
    return 0                                 # 도달 불가
  • dist 배열 하나가 방문 표시와 거리 기록을 겸한다. 통과 조건은 아직 방문하지 않은 칸(dist == -1)이다. 실수로 "이미 방문한 칸만 통과"로 쓰면 탐색이 한 발도 나가지 못한다.
  • 방향 배열 (di, dj), 범위 검사, 벽 검사, 방문 검사 네 가지를 한 조건문에 묶는 것이 격자 BFS의 기본형이다.

상황별 변형 포인트

같은 루프를 두고 문제가 묻는 것에 따라 어디를 바꾸는지만 알면 된다.

상황BFS 변형 포인트
가장 늦게 도달하는 노드 찾기 (동시에 퍼지는 연락망·전염)거리 = 도달 "시각"이다. 큐에서 꺼낼 때마다 (dist, 번호)를 비교해 최댓값을 갱신한다. 방향 그래프의 중복 간선은 set으로 걸러 둔다
정해진 시간(단계) 안에 도달 가능한 칸 세기거리가 한도에 닿은 칸은 꺼낸 뒤 확장하지 않고 continue한다. 칸마다 열린 방향이 다르면 현재 칸이 그 방향으로 열려 있고 다음 칸도 반대 방향으로 열려 있는지 둘 다 확인한다
연결 요소마다 따로 계산 (값 차이가 1인 칸들의 묶음 크기 등)전체 칸을 순회하며 미방문 칸을 만날 때마다 BFS를 새로 시작한다. 한 번의 BFS가 끝나면 그 묶음의 크기·최솟값을 모아 최선값과 비교한다

BFS와 DFS, 언제 무엇을 쓸까

상황선택
가중치 없는 그래프의 최단 거리·최소 횟수BFS (처음 도달한 순간이 최단)
레벨(시간) 단위로 퍼지는 시뮬레이션BFS
모든 경로·조합을 다 봐야 하는 문제, 백트래킹DFS
연결 요소 개수 세기둘 다 가능

BFS는 한 레벨의 노드를 모두 큐에 담으므로 그래프가 넓을수록 메모리를 많이 쓰고, DFS는 현재 경로만 스택에 담으므로 깊이에 비례한다. 트리에서 BFS는 레벨 순회(level order)와 같다.

핵심 정리

  • BFS = 큐 + 넣을 때 방문 처리, 시간 복잡도 O(V + E)
  • 가중치가 모두 같으면 BFS가 곧 최단 거리 알고리즘이다. (가중치가 다르면 다익스트라)
  • 격자 문제는 방향 배열 (di, dj), 범위 검사, 방문(거리) 배열을 세트로 기억해 둔다.

관련 노트: 큐와 덱, DFS, 다익스트라, 그래프의 기본, 웹 검색엔진의 동작: 크롤링, 색인, 랭킹, 질의 처리

댓글

GitHub 계정으로 댓글을 남길 수 있어요.