BFS는 시작 지점에서 가까운 정점부터 차례대로 탐색하는 그래프 탐색 알고리즘이다.
한국어로는 너비 우선 탐색이라고 한다.
코딩테스트에서는 단순히 그래프를 탐색하는 용도뿐만 아니라 다음과 같은 문제에서 자주 사용한다.
BFS에서 가장 중요한 특징은 Queue를 사용한다는 것이다.
다음과 같은 그래프가 있다고 가정한다.

1번 노드에서 BFS 탐색을 시작하면 방문 순서는 다음과 같다.
1, 2, 3, 4, 5, 6
먼저 시작점과 가까운 노드를 모두 방문한 뒤 그다음 거리의 노드를 탐색한다.
| 거리 | 방문 노드 |
|---|---|
| 0 | 1 |
| 1 | 2, 3 |
| 2 | 4, 5, 6 |
이처럼 BFS는 같은 거리의 노드를 먼저 처리한다.
이 성질 때문에 가중치가 없는 그래프에서 최단 거리를 구하는 문제에 특히 적합하다.
BFS에서는 먼저 발견한 노드를 먼저 탐색해야 한다.
Queue는 FIFO(First In First Out) 구조를 가진다.
입력 순서
1
2
3
4
처리 순서
1
2
3
4
현재 노드에서 연결된 노드를 Queue에 넣고, Queue에서 가장 먼저 들어온 노드부터 꺼내면서 탐색한다.
따라서 자연스럽게 시작점에서 가까운 노드부터 처리된다.
Java에서는 코딩테스트에서 보통 ArrayDeque를 사용한다.
Queue<Integer> queue = new ArrayDeque<>();
LinkedList도 Queue 구현체이지만 단순 Queue 용도라면 일반적으로 ArrayDeque를 사용하는 것이 좋다.
BFS는 크게 다음 과정으로 구현한다.
핵심 구성 요소는 세 가지다.
Queue
visited
반복문
그래프가 인접 리스트 형태로 주어졌다고 가정한다.
import java.util.*;
public class Main {
static List<Integer>[] graph;
static boolean[] visited;
static void bfs(int start) {
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(start);
visited[start] = true;
while (!queue.isEmpty()) {
int current = queue.poll();
System.out.println(current);
for (int next : graph[current]) {
if (visited[next]) {
continue;
}
visited[next] = true;
queue.offer(next);
}
}
}
}
BFS 문제에서 가장 기본이 되는 형태다.
특히 다음 세 줄의 의미를 정확하게 이해하는 것이 중요하다.
queue.offer(start);
visited[start] = true;
시작점을 Queue에 넣으면서 방문 처리한다.
그리고 반복문에서
int current = queue.poll();
현재 탐색할 노드를 꺼낸다.
이후
for (int next : graph[current])
현재 노드와 연결된 노드를 검사한다.
그래프에서는 서로 연결된 노드를 다시 방문할 수 있다.
예를 들어 다음과 같은 그래프가 있다고 생각해보자.

방문 여부를 확인하지 않으면 다음과 같은 상황이 반복될 수 있다.
1에서 2 방문
2에서 3 방문
3에서 4 방문
4에서 다시 1 방문
그래서 일반적인 BFS에서는 반드시 방문 여부를 관리한다.
boolean[] visited = new boolean[n + 1];
방문하지 않은 노드만 Queue에 넣는다.
if (!visited[next]) {
visited[next] = true;
queue.offer(next);
}
BFS에서 자주 실수하는 부분이다.
방문 처리는 일반적으로 Queue에 넣을 때 한다.
visited[next] = true;
queue.offer(next);
다음처럼 Queue에서 꺼낼 때 방문 처리하는 것은 피하는 것이 좋다.
int current = queue.poll();
visited[current] = true;
이렇게 하면 하나의 노드가 여러 경로를 통해 Queue에 중복으로 들어갈 수 있다.
예를 들어 2번과 3번 노드가 모두 4번과 연결되어 있다면
2가 4 발견
3도 4 발견
4가 아직 Queue에서 나오지 않았기 때문에 두 번 들어갈 수 있다.
따라서 기본적으로 다음 순서를 기억하면 된다.
방문 여부 확인
방문 처리
Queue 삽입
BFS의 가장 중요한 활용 중 하나다.
모든 간선의 이동 비용이 동일하다면 BFS로 최단 거리를 구할 수 있다.
예를 들어 그래프에서
1번 노드에서 각 노드까지 몇 번 이동해야 하는가?
를 구한다고 해보자.
거리 배열을 만든다.
int[] distance = new int[n + 1];
Arrays.fill(distance, -1);
-1을 아직 방문하지 않은 상태로 사용할 수 있다.
static void bfs(int start) {
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(start);
distance[start] = 0;
while (!queue.isEmpty()) {
int current = queue.poll();
for (int next : graph[current]) {
if (distance[next] != -1) {
continue;
}
distance[next] = distance[current] + 1;
queue.offer(next);
}
}
}
이 경우 distance 자체가 방문 배열 역할도 한다.
예를 들어
distance[5] = 3;
이라면 시작점에서 5번 노드까지 최소 3번 이동해야 한다는 의미다.
코딩테스트에서 BFS가 가장 많이 등장하는 형태 중 하나가 2차원 배열 탐색이다.
예를 들어 다음과 같은 미로가 있다고 가정한다.

조건은 다음과 같다.
1 : 이동 가능
0 : 이동 불가능
상하좌우로 이동해야 한다면 방향 배열을 사용한다.
static int[] dx = {-1, 1, 0, 0};
static int[] dy = {0, 0, -1, 1};
각각
상
하
좌
우
이동을 의미한다.
2차원 BFS에서는 Queue에 노드 번호 대신 좌표를 저장한다.
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Queue는 다음처럼 만든다.
Queue<Point> queue = new ArrayDeque<>();
static void bfs(int startX, int startY) {
Queue<Point> queue = new ArrayDeque<>();
queue.offer(new Point(startX, startY));
visited[startX][startY] = true;
while (!queue.isEmpty()) {
Point current = queue.poll();
for (int i = 0; i < 4; i++) {
int nx = current.x + dx[i];
int ny = current.y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (map[nx][ny] == 0) {
continue;
}
if (visited[nx][ny]) {
continue;
}
visited[nx][ny] = true;
queue.offer(new Point(nx, ny));
}
}
}
2차원 BFS 문제는 대부분 이 구조를 기반으로 조건만 변경된다.
격자 문제에서는 보통 다음 세 가지를 검사한다.
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (map[nx][ny] == 0) {
continue;
}
if (visited[nx][ny]) {
continue;
}
세 조건을 통과하면 이동할 수 있다.
visited[nx][ny] = true;
queue.offer(new Point(nx, ny));
미로의 최단 거리를 구한다면 visited 대신 거리 배열을 사용할 수도 있다.
int[][] distance = new int[n][m];
시작점을 기준으로 거리를 기록한다.
distance[startX][startY] = 1;
다음 위치의 거리는 현재 거리 + 1이다.
distance[nx][ny] = distance[current.x][current.y] + 1;
전체 코드는 다음 형태가 된다.
static void bfs(int startX, int startY) {
Queue<Point> queue = new ArrayDeque<>();
queue.offer(new Point(startX, startY));
distance[startX][startY] = 1;
while (!queue.isEmpty()) {
Point current = queue.poll();
for (int i = 0; i < 4; i++) {
int nx = current.x + dx[i];
int ny = current.y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (map[nx][ny] == 0) {
continue;
}
if (distance[nx][ny] != 0) {
continue;
}
distance[nx][ny]
= distance[current.x][current.y] + 1;
queue.offer(new Point(nx, ny));
}
}
}
코딩테스트에서 다음과 같은 표현이 나온다면 BFS를 고려할 수 있다.
| 문제 표현 | BFS 활용 |
|---|---|
| 최소 몇 번 | 최단 거리 |
| 가장 빠르게 | 최단 이동 |
| 몇 번 이동해야 하는가 | 거리 계산 |
| 미로 탈출 | 2차원 BFS |
| 연결된 영역의 개수 | 영역 탐색 |
| 섬의 개수 | 영역 탐색 |
| 특정 거리의 도시 | 거리 계산 |
| 바이러스 확산 | 확산 시뮬레이션 |
| 토마토가 익는 날짜 | 다중 시작점 BFS |
다만 최단 거리라는 단어만 보고 무조건 BFS를 사용하면 안 된다.
BFS로 일반적인 최단 거리를 구할 수 있는 조건은 보통
모든 이동 비용이 동일한 경우
이다.
이동 비용이 서로 다르면 다익스트라 등의 알고리즘을 고려해야 한다.
BFS는 최단 거리뿐만 아니라 연결된 영역의 개수를 구하는 문제에서도 자주 사용된다.
예를 들어

에서 연결된 1 영역의 개수를 구한다고 해보자.
전체 배열을 순회한다.
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (map[i][j] == 1 && !visited[i][j]) {
bfs(i, j);
count++;
}
}
}
핵심은
새로운 방문하지 않은 영역 발견
시 BFS를 한 번 실행한다는 것이다.
BFS 한 번이 하나의 연결된 영역 전체를 방문하므로 BFS가 실행된 횟수가 영역의 개수가 된다.
BFS에서 상당히 중요한 패턴이다.
대표적으로 백준 토마토 문제처럼 여러 위치에서 동시에 확산되는 문제가 있다.
시작점이 하나라면
queue.offer(start);
하나만 넣는다.
하지만 시작점이 여러 개라면 처음부터 모두 Queue에 넣는다.
for (...) {
if (map[i][j] == START) {
queue.offer(new Point(i, j));
}
}
그 이후 BFS를 한 번 실행한다.
이것을 Multi-source BFS라고 한다.
여러 시작점에서 각각 BFS를 따로 실행하는 것이 아니다.
잘못된 방식
시작점 A BFS
시작점 B BFS
시작점 C BFS
처음부터 하나의 Queue에 모두 넣는다.
Queue<Point> queue = new ArrayDeque<>();
queue.offer(startA);
queue.offer(startB);
queue.offer(startC);
그러면 모든 시작점에서 동시에 한 칸씩 퍼지는 효과를 만들 수 있다.
그래프를 인접 리스트로 구현했다면 BFS의 시간 복잡도는 일반적으로
O(V + E)
이다.
V: 정점의 개수E: 간선의 개수각 정점을 한 번 방문하고 각 간선을 확인하기 때문이다.
2차원 배열이 N × M 크기라면 일반적으로
O(N × M)
으로 생각할 수 있다.
각 칸을 최대 한 번 방문하기 때문이다.
queue.offer(next);
만 하고 방문 처리를 나중에 하면 같은 노드가 Queue에 여러 번 들어갈 수 있다.
따라서 보통 다음 두 작업은 같이 한다.
visited[next] = true;
queue.offer(next);
다음처럼 Queue에만 넣는 경우가 있다.
queue.offer(start);
시작점도 반드시 방문 처리한다.
queue.offer(start);
visited[start] = true;
2차원 BFS에서 가장 많이 발생하는 실수 중 하나다.
map[nx][ny]
를 확인하기 전에 반드시 범위부터 검사해야 한다.
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
2차원 배열에서는 보통
map[row][column]
형태다.
즉,
map[x][y]
라고 작성했다면 코드 전체에서 x, y의 의미를 일관되게 유지하는 것이 중요하다.
BFS의 핵심 자료구조는 Queue다.
Queue<Integer> queue = new ArrayDeque<>();
Stack을 사용하면 BFS가 아니라 DFS 형태의 탐색이 된다.
문제를 읽자마자 코드를 작성하기보다 먼저 다음을 확인하는 것이 좋다.
그래프의 정점
2차원 배열의 칸
상태
연결된 간선
상하좌우
특정 조건을 만족하는 상태
boolean[] visited
boolean[][] visited
int[] distance
int[][] distance
하나
여러 개
필요하다면 BFS의 거리 단위를 어떻게 기록할지 결정한다.
이 다섯 가지를 먼저 결정하면 대부분의 BFS 구현 구조가 자연스럽게 나온다.
그래프 BFS의 기본 형태는 다음 정도는 익숙하게 작성할 수 있는 것이 좋다.
static void bfs(int start) {
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(start);
visited[start] = true;
while (!queue.isEmpty()) {
int current = queue.poll();
for (int next : graph[current]) {
if (visited[next]) {
continue;
}
visited[next] = true;
queue.offer(next);
}
}
}
2차원 BFS는 다음 형태를 기본 틀로 잡을 수 있다.
static void bfs(int x, int y) {
Queue<Point> queue = new ArrayDeque<>();
queue.offer(new Point(x, y));
visited[x][y] = true;
while (!queue.isEmpty()) {
Point current = queue.poll();
for (int i = 0; i < 4; i++) {
int nx = current.x + dx[i];
int ny = current.y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (visited[nx][ny]) {
continue;
}
if (!canMove(nx, ny)) {
continue;
}
visited[nx][ny] = true;
queue.offer(new Point(nx, ny));
}
}
}
문제마다 달라지는 부분은 대부분 canMove()에 해당하는 이동 조건이다.
BFS 자체의 구조는 크게 달라지지 않는다.
단순 그래프 탐색이라면
Queue<Integer>
좌표가 필요하다면
Queue<Point>
거리까지 상태 자체에 포함해야 한다면
class Node {
int x;
int y;
int distance;
}
처럼 문제에 필요한 정보만 관리한다.
최단 거리 문제에서는
int[][] distance
를 방문 여부와 거리 기록에 동시에 사용할 수 있다.
if (distance[nx][ny] != -1) {
continue;
}
별도의 visited 배열이 필요 없어 코드가 단순해진다.
모든 이동 비용이 동일한 BFS에서는 가까운 거리부터 탐색한다.
따라서 목적지를 처음 발견한 순간의 거리가 최단 거리다.
if (nx == targetX && ny == targetY) {
return distance[nx][ny];
}
모든 영역을 탐색할 필요가 없는 문제라면 이를 이용해 탐색을 조기에 종료할 수도 있다.
BFS 구현 자체는 대부분 비슷하다.
어려운 BFS 문제는 보통 다음 부분에서 난도가 올라간다.
무엇을 하나의 상태로 볼 것인가?
단순한 미로라면
(x, y)
만 있으면 된다.
하지만 벽을 한 번 부술 수 있다면
(x, y, 벽을 부쉈는지 여부)
까지 상태가 된다.
따라서 방문 배열 역시
visited[x][y][broken]
처럼 관리해야 할 수 있다.
결국 BFS 문제에서 중요한 것은 Queue 자체보다 동일한 상태를 어떻게 정의하고 중복 방문을 어떻게 막을 것인가다.
BFS를 우선적으로 고려하기 좋은 조건은 다음과 같다.
| 조건 | BFS |
|---|---|
| 그래프 전체 탐색 | 가능 |
| 연결된 영역 탐색 | 적합 |
| 가중치 없는 최단 거리 | 매우 적합 |
| 최소 이동 횟수 | 매우 적합 |
| 여러 지점에서 동시에 확산 | 매우 적합 |
| 서로 다른 가중치의 최단 거리 | 일반 BFS 부적합 |
특히 문제에서
최소 횟수
최소 이동
몇 단계
몇 초 후
몇 칸
같은 표현이 나오고, 한 번의 이동 비용이 동일하다면 BFS를 먼저 떠올려볼 수 있다.
BFS는 Queue를 이용하여 시작점에서 가까운 위치부터 탐색하는 알고리즘이다.
코딩테스트에서는 단순히 BFS 코드를 외우는 것보다 다음 내용을 이해하는 것이 중요하다.
1. 탐색할 상태를 정의한다.
2. Queue에 시작 상태를 넣는다.
3. 시작 상태를 방문 처리한다.
4. Queue에서 상태를 하나 꺼낸다.
5. 다음으로 이동 가능한 상태를 검사한다.
6. 방문하지 않았다면 방문 처리 후 Queue에 넣는다.
7. Queue가 빌 때까지 반복한다.
특히 기억해야 할 부분은 다음과 같다.
BFS의 기본 코드는 크게 변하지 않는다.
결국 코딩테스트에서 필요한 능력은 문제를 보고 이것을 BFS 탐색 문제로 모델링할 수 있는지 판단하는 것이다.