DFS와 BFS

DFS와 BFS

작성자
태인태인
카테고리
📗 스터디
작성일
2024년 02월 03일
태그
Java
Algorithm
DFS와 BFS는 그래프 완전 탐색 기법이다. 지금부터 DFS와 BFS 알고리즘에 대해 알아보고, 관련 문제를 풀어보자.

DFS(깊이 우선 탐색)

DFS는 그래프의 시작 노드에서 출발하여 탐색할 한 쪽 분기를 정하여 최대 깊이까지 탐색을 마친 후에, 다른 쪽 분기로 이동하여 다시 탐색을 진행하는 알고리즘이다. 즉 다음 분기로 넘어가기 전 해당 분기를 먼저 완벽하게 탐색한다.
재귀 함수로 구현하며, 스택 자료구조를 이용한다. 이때, 재귀 함수를 이용하기 때문에 스택 오버플로에 주의할 필요가 있다. 또한, 찾은 경로가 최단 경로임을 보장할 수는 없다.
DFS를 사용할 수 있는 문제의 유형에는 단절점 찾기, 단절선 찾기, 사이클 찾기, 위상 정렬 등이 있다.
DFS의 핵심은 한 번 방문한 노드는 다시 방문하지 않는 것이다. 따라서, 노드 방문 여부를 저장해둘 배열이 필요하다.
 
스택 자료구조를 이용해 DFS의 작동 과정을 살펴보자.
다음과 같이 원본 그래프가 있을 때, 우리는 최종적으로 방문 순서를 구해야 한다.
출처 : 김종관,  「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 146쪽
출처 : 김종관, 「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 146쪽
먼저, 원본 그래프를 인접 리스트로 표현해야 한다. 시작점이 1이므로, 방문 배열에서 1을 체크해주자. 그런 다음 스택에 1을 추가해준다.
출처 : 김종관,  「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 146쪽
출처 : 김종관, 「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 146쪽
다음으로 스택에서 pop을 수행해 노드를 꺼낸다. 이때 꺼낸 순서가 노드 방문 순서가 된다. 이후, 인접 리스트에 있는 인접 노드를 방문한다. 이때도 방문 배열에 체크하는 것을 잊지 말자.
위의 과정을 스택이 비어있을 때까지 반복한다.
출처 : 김종관,  「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 147쪽
출처 : 김종관, 「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 147쪽

DFS를 구현하는 방법에 대해 알아보자. 구현에는 보통 재귀 함수를 사용한다.
먼저, 인접 리스트와 방문 여부를 체크할 배열을 만들어주자.
ArrayList<Integer> A[]; boolean visited[]; ... for(int i=0; i<N+1; i++) { A[i] = new ArrayList<Integer>(); //각 노드에 대한 인접 리스트 초기화 }
인접 리스트에 각 노드에 대한 인접 노드 값을 모두 채웠다고 가정하자.
DFS 함수를 생성해주자. 여기서 s 는 각 탐색의 시작 노드이다.
private static void DFS(int s) { System.out.print(s + " "); visited[s] = true; for (int i: A[s]) { if(!visited[i]) { DFS(i); } } }
먼저 방문 배열 visited에 방문 여부를 체크해준다. 그런 다음 해당 노드의 인접 노드에 아직 방문하지 않았다면, DFS를 수행해준다(재귀 함수)
이제, DFS(시작점) 을 실행하면 방문 순서를 구할 수 있다.

BFS (너비 우선 탐색)

BFS는 시작 노드에서부터 가까운 노드를 방문해가며 탐색하는 알고리즘이다. FIFO(First In FIrst Out) 탐색이며, 큐 자료구조를 이용한다.
BFS는 두 노드 사이 최단 거리, 최단 경로 탐색을 보장한다.
 
BFS 역시 DFS와 마찬가지로 방문했던 노드는 다시 방문하지 않는 것이 중요하므로, 방문 체크를 위한 배열이 필요하다. 원본 그래프를 인접 리스트로 표현하는 것도 DFS와 같다. 다만, 스택 대신 큐를 사용한다는 차이점이 있다.
먼저 시작점인 1의 방문을 체크해주고, 큐 자료구조에 1을 추가해준다.
출처 : 김종관,  「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 162쪽
출처 : 김종관, 「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 162쪽
DFS와 유사하게, 큐에서 노드를 꺼낸 순서를 탐색 순서로 한다.
다음으로, 1과 인접한 노드를 큐에 삽입하고 방문을 체크한다(큐에 삽입하며 방문 처리하도록 구현)
출처 : 김종관,  「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 163쪽
출처 : 김종관, 「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 163쪽
이제 2, 3 순서로 노드를 꺼내며 인접 노드를 큐에 추가한다. 이때 2의 인접 노드인 5, 6은 방문하지 않았으므로 큐에 삽입하며 방문 처리한다. 3도 마찬가지로 인접 노드인 4를 큐에 삽입하며 방문 처리한다.
5, 6은 인접 노드가 없으므로 탐색 순서에 기록하고 꺼낸다.
4를 꺼낼 때는 인접 노드인 6을 이미 방문했으므로, 큐에 삽입하지 않고 꺼낸다.
출처 : 김종관,  「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 163쪽
출처 : 김종관, 「Do it! 알고리즘 코딩 테스트: 자바 편」, 이지스퍼블리싱, 2022, 163쪽
큐가 빌 때까지 이 과정을 반복하면 탐색 순서를 결정할 수 있다.
 
BFS를 구현하는 방법을 살펴보자. BFS는 큐 자료구조를 이용해 구현한다.
DFS의 예시와 마찬가지로 A는 각 노드의 인접 노드가 담겨있는 리스트이다.
 
먼저 큐를 생성해주고, 시작점 s를 큐에 삽입하면서 방문 처리한다.
그런 다음, 큐가 비어있을 때까지 다음 과정을 반복한다.
  • queue의 앞에 있는 값을 제거하고 해당 노드를 방문 순서로 기록(출력)한다.
  • 방금 제거한 노드의 인접 노드에 대해서도 아직 방문하지 않았다면 큐에 삽입하며 방문 처리한다.
Queue <Integer> queue = new LinkedList<>(); queue.add(s); visited[s] = true; while (!queue.isEmpty()) { int node = queue.poll(); System.out.print(node + " "); for (int i: A[node]) { if (!visited[i]) { queue.add(i); visited[i] = true; } } }
 

DFS와 BFS를 활용해 문제를 풀어보자.

백준 #1260번 : DFS와 BFS

문제

그래프를 DFS로 탐색한 결과와 BFS로 탐색한 결과를 출력하는 프로그램을 작성하시오. 단, 방문할 수 있는 정점이 여러 개인 경우에는 정점 번호가 작은 것을 먼저 방문하고, 더 이상 방문할 수 있는 점이 없는 경우 종료한다. 정점 번호는 1번부터 N번까지이다.

입력

첫째 줄에 정점의 개수 N(1 ≤ N ≤ 1,000), 간선의 개수 M(1 ≤ M ≤ 10,000), 탐색을 시작할 정점의 번호 V가 주어진다. 다음 M개의 줄에는 간선이 연결하는 두 정점의 번호가 주어진다. 어떤 두 정점 사이에 여러 개의 간선이 있을 수 있다. 입력으로 주어지는 간선은 양방향이다.

출력

첫째 줄에 DFS를 수행한 결과를, 그 다음 줄에는 BFS를 수행한 결과를 출력한다. V부터 방문된 점을 순서대로 출력하면 된다.

풀이

위에서 설명한 DFS, BFS의 구현 방법을 적용하면 해결할 수 있다.
문제 조건에서 정점이 여러개인 경우 정점 번호가 작은 것을 먼저 방문한다고 했으므로, 각 노드에 대한 인접 리스트는 오름차순으로 정렬해줘야 한다.
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; import java.util.*; public class Main { static ArrayList<Integer> A[]; static boolean visited[]; public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); int N = Integer.parseInt(st.nextToken()); int M = Integer.parseInt(st.nextToken()); int V = Integer.parseInt(st.nextToken()); A = new ArrayList[N+1]; for(int i=0; i<N+1; i++) { A[i] = new ArrayList<Integer>(); } for(int i=0; i<M; i++) { st = new StringTokenizer(br.readLine()); int s = Integer.parseInt(st.nextToken()); int e = Integer.parseInt(st.nextToken()); A[s].add(e); A[e].add(s); } //인접 노드 오름차순 정렬 for(int i=0; i<N+1; i++) { Collections.sort(A[i]); } visited = new boolean[N+1]; DFS(V); System.out.println(); visited = new boolean[N+1]; BFS(V); } private static void BFS(int s) { Queue<Integer> queue = new LinkedList<>(); queue.add(s); visited[s] = true; while(!queue.isEmpty()) { int node = queue.poll(); System.out.print(node + " "); for (int i: A[node]) { if(!visited[i]) { queue.add(i); visited[i] = true; } } } } private static void DFS(int s) { System.out.print(s + " "); visited[s] = true; for (int i: A[s]) { if(!visited[i]) { DFS(i); } } } }
결과
메모리
시간
맞았습니다!!
20720 KB
360 ms

좀 더 응용이 필요한 문제를 풀어보자.

백준 #1012번 : 유기농 배추

문제

차세대 영농인 한나는 강원도 고랭지에서 유기농 배추를 재배하기로 하였다. 농약을 쓰지 않고 배추를 재배하려면 배추를 해충으로부터 보호하는 것이 중요하기 때문에, 한나는 해충 방지에 효과적인 배추흰지렁이를 구입하기로 결심한다. 이 지렁이는 배추근처에 서식하며 해충을 잡아 먹음으로써 배추를 보호한다. 특히, 어떤 배추에 배추흰지렁이가 한 마리라도 살고 있으면 이 지렁이는 인접한 다른 배추로 이동할 수 있어, 그 배추들 역시 해충으로부터 보호받을 수 있다. 한 배추의 상하좌우 네 방향에 다른 배추가 위치한 경우에 서로 인접해있는 것이다.
한나가 배추를 재배하는 땅은 고르지 못해서 배추를 군데군데 심어 놓았다. 배추들이 모여있는 곳에는 배추흰지렁이가 한 마리만 있으면 되므로 서로 인접해있는 배추들이 몇 군데에 퍼져있는지 조사하면 총 몇 마리의 지렁이가 필요한지 알 수 있다. 예를 들어 배추밭이 아래와 같이 구성되어 있으면 최소 5마리의 배추흰지렁이가 필요하다. 0은 배추가 심어져 있지 않은 땅이고, 1은 배추가 심어져 있는 땅을 나타낸다.
1
1
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
1
1
0
0
0
1
1
1
0
0
0
0
1
0
0
1
1
1

입력

입력의 첫 줄에는 테스트 케이스의 개수 T가 주어진다. 그 다음 줄부터 각각의 테스트 케이스에 대해 첫째 줄에는 배추를 심은 배추밭의 가로길이 M(1 ≤ M ≤ 50)과 세로길이 N(1 ≤ N ≤ 50), 그리고 배추가 심어져 있는 위치의 개수 K(1 ≤ K ≤ 2500)이 주어진다. 그 다음 K줄에는 배추의 위치 X(0 ≤ X ≤ M-1), Y(0 ≤ Y ≤ N-1)가 주어진다. 두 배추의 위치가 같은 경우는 없다.

출력

각 테스트 케이스에 대해 필요한 최소의 배추흰지렁이 마리 수를 출력한다.

풀이

문제를 이해해보면, 최종적으로 구해야 하는 답은 1이 가로, 세로로 연결되어 있는 군집의 개수라는 것을 알 수 있다.
주어진 예제에서는 다음과 같이 5개의 군집이 형성되므로, 답은 5이다.
notion image
시작점에서부터 1이 있는 방향으로 끝까지 탐색해야 군집을 찾을 수 있으므로, 깊이 우선 탐색 (DFS)를 사용하면 된다.
 
먼저 2차원 배열 map 과 방문 체크 배열 visited 를 생성하고, 초기값을 모두 false로 채워주자. 입력받은 배추가 심어진 위치에 해당하는 map 값은 true로 저장한다. 그럼 이제 우리는 map에서 true인 값들만 탐색 대상이다.
 
map을 왼쪽 위부터 순서대로 탐색한다. 해당 위치가 배추가 심어진 위치(true)이고, 아직 방문하지 않았다면 군집 개수를 증가시키고 DFS를 진행한다.
int count = 0; //군집 개수 for (int i = 0; i < N; i++) { for (int j = 0; j < M; j++) { if (map[i][j] && !visited[i][j]) { count++; DFS(i, j); } } }
이때 DFS는 상하좌우(인접 노드)를 탐색하는 로직을 추가하여 구현해야 한다.
dx, dy 배열을 선언하고, for문으로 상하좌우를 탐색할 수 있다. 해당 노드의 상하좌우 각각에 배추가 심어져 있고(true), 아직 방문하지 않았다면 DFS를 수행한다.
static int[] dx = { 0, -1, 0, 1 }; static int[] dy = { 1, 0, -1, 0 }; private static void DFS(int x, int y) { if (visited[x][y]) return; visited[x][y] = true; for (int i = 0; i < 4; i++) { int cx = x + dx[i]; int cy = y + dy[i]; if (cx >= 0 && cy >= 0 && cx < N && cy < M) { if (map[cx][cy] && !visited[cx][cy]) DFS(cx, cy); } } }
 
전체 코드는 다음과 같다.
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; import java.util.*; public class Main { static boolean map[][]; static boolean visited[][]; static int N, M; static int[] dx = { 0, -1, 0, 1 }; static int[] dy = { 1, 0, -1, 0 }; public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); int T = Integer.parseInt(st.nextToken()); for(int t=0; t<T; t++) { st = new StringTokenizer(br.readLine()); M = Integer.parseInt(st.nextToken()); //가로 N = Integer.parseInt(st.nextToken()); //세로 int K = Integer.parseInt(st.nextToken()); //배추 개수 map = new boolean[N][M]; visited = new boolean[N][M]; for(int i=0; i<N; i++) { for(int j=0; j<M; j++) { map[i][j] = false; visited[i][j] = false; } } for(int i=0; i<K; i++) { st = new StringTokenizer(br.readLine()); int x = Integer.parseInt(st.nextToken()); int y = Integer.parseInt(st.nextToken()); map[y][x] = true; } int count = 0; for(int i=0; i<N; i++) { for(int j=0; j<M; j++) { if(map[i][j] && !visited[i][j]) { count++; DFS(i, j); } } } System.out.println(count); } } private static void DFS(int x, int y) { if(visited[x][y]) return; visited[x][y] = true; for (int i = 0; i < 4; i++) { int cx = x + dx[i]; int cy = y + dy[i]; if (cx >= 0 && cy >= 0 && cx < N && cy < M) { if(map[cx][cy] && !visited[cx][cy]) DFS(cx, cy); } } } }
결과
메모리
시간
맞았습니다!!
15792 KB
156 ms
 

댓글

guest