본문 바로가기
알고리즘 & 자료구조/알고리즘 개념

DFS/BFS

by 신재권 2021. 11. 6.

탐색이란 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정을 의미한다.

프로그래밍에서는 그래프, 트리 등의 자료구조 안에서 탐색을 하는 문제를 자주 다룬다.

대표적인 탐색 알고리즘으로 DBS와 BFS를 꼽을 수 있는데, 이 두 알고리즘의 원리를 제대로 이해해야 코딩 테스트의 탐색 문제 유형을 풀 수 있다.

그런데 DFS와 BFS를 제대로 이해하려면 기본 자료구조인 스택과 큐에 대한 이해가 전제되어야 한다.

자료구조는 '데이터를 표현하고 관리하고 처리하기 위한 구조'를 의미한다.

그 중 스택과 큐는 자료구조의 기초 개념으로 다음의 두 핵심적인 함수로 구성된다.

  • 삽입(Push) : 데이터를 삽입한다.
  • 삭제(Pop) : 데이터를 삭제한다.

물론 실제로 스택과 큐를 사용할 때는 삽입과 삭제 외에도 오버플로와 언더플로를 고민해야 한다.

오버플로는 특정한 자료구조가 수용할 수 있는 데이터의 크기를 이미 가득 찬 상태에서 삽입 연산을 수행할 때 발생한다.

즉, 저장 공간을 벗어나 데이터가 넘쳐 흐를 때 발생한다.

반면에 특정한 자료구조에 데이터가 전혀 들어 있지 않은 상태에서 삭제 연산을 수행하면 데이터가 전혀 없는 상태이므로 언더플로가 발생한다.


스택

스택은 박스 쌓기에 비유할 수 있다.

흔히 박스는 아래에서부터 위로 차곡차곡 쌓는다.

그리고 아래에 있는 박스를 치우기 위해서는 위에 있는 박스를 먼저 내려야 한다.

이러한 구조를 선입후출 구조 또는 후입선출 구조라고 한다.

package CodeTest;

import java.util.Stack;

public class BFS_DFS0501 {

	public static void main(String[] args) {
		Stack<Integer> s = new Stack<Integer>();
		
		s.push(5);
		s.push(2);
		s.push(3);
		s.push(7);
		s.pop();
		s.push(1);
		s.push(4);
		//스택의 최상단 원소부터 출력
		while(!s.empty()){
			System.out.println(s.peek());
			s.pop();
		}

	}

}

큐는 대기 줄에 비유할 수 있다.

우리가 흔히 놀이공원에 입장하기 위해 줄을 설 때, 먼저 온 사람이 먼저 들어가게 된다.

물론 새치기는 없다고 가정한다.

나중에 온 사람일 수록 나중에 들어가기 때문에 '공정한'자료구조라 비유된다.

이러한 구조를 선입선출 구조라 한다.

package CodeTest;

import java.util.LinkedList;
import java.util.Queue;

public class BFS_DFS0502 {

	public static void main(String[] args) {
		Queue<Integer> q = new LinkedList<Integer>();
		
		q.offer(5);
		q.offer(2);
		q.offer(3);
		q.offer(7);
		q.poll();
		q.offer(1);
		q.offer(4);
		q.poll();
		// 먼저 들어온 원소부터 추출
		while(!q.isEmpty()){
			System.out.println(q.poll());
		}

	}

}

재귀 함수

DFS와 BFS를 구현하려면 재귀함수도 이해하고 있어야 한다.

재귀 함수란 자기 자신을 다시 호출하는 함수를 의미한다.

가장 간단한 재귀 함수는 다음과 같다.

package CodeTest;

public class BFS_DFS0503 {

	public static void main(String[] args) {
		recursiveFunction();

	}
	
	public static void recursiveFunction(){
		System.out.println("재귀 함수를 호출합니다.");
		recursiveFunction();
	}

}

이 코드를 실행하면 문자열을 무한히 출력한다.

재귀함수는 수학시간에 한번씩 언급되는 프랙털 구조와 흡사하다.

재귀함수를 문제 풀이에서 사용할 때는 재귀함수가 언제 끝날지, 종료 조건을 꼭 명시해야 한다.

자칫 종료 조건을 명시하지 않으면 함수가 무한 호출될 수도 있다.

package CodeTest;

public class BFS_DFS0504 {

	public static void main(String[] args) {
		recursiveFunction(1);

	}
	
	public static void recursiveFunction(int i){
		  // 100번째 호출을 했을 때 종료되도록 종료 조건 명시
        if (i == 100) return;
        System.out.println(i + "번째 재귀 함수에서 " + (i + 1) + "번째 재귀함수를 호출합니다.");
        recursiveFunction(i + 1);
        System.out.println(i + "번째 재귀 함수를 종료합니다.");
    }

}

컴퓨터 내부에서 재귀 함수의 수행은 스택 자료구조를 이용한다.

함수를 계속 호출했을 때 가장 마지막에 호출한 함수가 먼저 수행을 끝내야 그 앞의 함수 호출이 종료되기 때문이다.

컴퓨터의 구조 측면에서 보자면 연속해서 호출되는 함수는 메인 메모리의 스택 공간에 적재되므로 재귀 함수는 스택 자료구조와 같다는 말은 틀린말이 아니다.

스택 자료구조를 활용해야 하는 상당수 알고리즘은 재귀함수를 이용해서 간편하게 구현할 수 있다.

재구함수를 이용하는 대표적인 예제로는 팩토리얼 문제가 있다.

팩토리얼 기호는 느낌표!르 사용하여 n!는 1x2x...x(n-1)x n 을 의미한다.

수학적으로 0!와 1!의 값은 1로 같다는 성질을 이용하여 팩토리얼 함수는 n이 1이하가 되었을 때 함수를 종료하는 재귀함수의 형태로 구현할 수 있다.

package CodeTest;

public class BFS_DFS0505 {

	public static void main(String[] args) {
		//각각의 방식으로 구현한 n! 출력(n = 5)
		System.out.println("반복적으로 구현 : " + factorialIterative(5));
		System.out.println("재귀적으로 구현 : " + factorialRecursive(5));
	
	}
	
	//반복적으로 구현한 n!
	public static int factorialIterative(int n){
		int result = 1;
		// 1부터 n까지의 수를 차례대로 곱하기
		for(int i=1; i<=n; i++){
			result *= i;
		}
		return result;
	}
	
	//재귀적으로 구현한 n!
	public static int factorialRecursive(int n){
		//n이 1이하인 경우를 1을 반환
		if(n <= 1) return 1;
		// n! = n * (n-1)!를 그대로 코드로 작성
		return n* factorialRecursive(n-1);
	}
}

실행결과는 동일하다.

그렇다면 반복문 대신에 재귀함수를 사용했을 때 얻을 수 있는 장점은 무엇일까? 위의 코드를 비교했을 때 재귀 함수의 코드가 더 간결한 것을 알 수 있다.

이렇게 간결해진 이유는 재귀함수가 수학의 점화식(재귀식)을 그대로 소스코드로 옮겼기 때문이다.

수학에서 점화식은 특정한 함수를 자신보다 더 작은 변수에 대한 함수와의 관계로 표현한 것을 의미한다.

팩토리얼을 수학적 점화식으로 표현하면 다음과 같다.

  1. n이 0혹은 1일때 : factorial(n) = 1
  2. n이 1보다 클 때 : factorial(n) = n x factorial(n-1)

일반적으로 우리는 점화식에서 종료 조건을 찾을 수 있는데, 앞 예시에서 종료 조건은 'n이 0 혹은 1일 때'이다.

팩토리얼은 n이 양의 정수일 때에만 유효하기 때문에 n이 1이하인 경우 1을 반환할 수 있도록 재귀함수를 작성해야 한다.

n이 1 이하인 경우를 고려하지 않으면 재귀함수가 무한히 반복되어 결과를 출력하지 못할 것이다.

또한 n의 값으로 음수가 들어왔을 때는 입력 범위의 오류로, 오류 메시지를 띄우도록 코드를 작성할수도 있다.

따라서 재귀함수 내에서 특정 조건일 때 더이상 재귀적으로 함수를 호출하지 않고 종료하도록 if문을 이용하여 꼭 종료 조건을 구현해주어야 한다.

다시 한번 점화식과 조금 전에 작성했던 팩토리얼의 재귀 함수 버전을 비교해보자.

재귀함수의 소스코드와 점화식이 매우 닮아있다는 것을 확인할 수 있다.

다시 말해 재귀 함수는 반복문을 이용하는 것과 비교했을 때 더욱 간결한 형태임을 이해할 수 있다.


DFS

DFS는 Depth-First Search, 깊이 우선 탐색이라고도 부르며, 그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘이다.

DFS를 설명하기전에 먼저 그래프의 기본 구조를 알아야 한다.

그래프는 노드와 간선으로 표현되며 이때 노드를 정점이라고도 말한다.

그래프 탐색이란 하나의 노드를 시작으로 다수의 노드를 방문하는 것을 말한다.

또한 두 노드가 간선으로 연결되어 있다면, 두 노드는 인접하다라고 표현한다.

프로그래밍에서 그래프는 크게 2가지 방식으로 표현할 수 있는데 코딩 테스트에서는 이 두 방식이 모두 필요하니 두 개념에 대해 바르게 알고 있도록 해야 한다.

  • 인접 행렬(Adjacency Matrix) : 2차원 배열로 그래프의 연결 관계를 표현하는 방식
  • 인접 리스트 (Adjacency List) : 리스트로 그래프의 연결 관계를 표현하는 방식

먼저 인접 행렬 방식은 2차원 배열에 각 노드가 연결된 형태를 기록하는 방식이다.

연결되지 않은 노드 끼리는 무한의 비용이라고 작성한다.

이렇게 그래프를 인접 행렬 방식으로 처리할 때는 다음과 같이 데이터를 초기화 한다.

package CodeTest;

public class BFS_DFS0506 {
	
	public static final int INF = 999999999;

	public static int[][] graph= {
		{0, 7, 5},
		{7, 0, INF},
		{5, INF, 0}
	};
	
	public static void main(String[] args) {
		//그래프 출력
		for(int i=0; i<3; i++){
			for(int j=0; j<3; j++){
				System.out.println(graph[i][j] + " ");
			}
			System.out.println();
		}

	}

}

인접리스트 방식에서는 모든 노드에 연결된 노드에 대한 정보를 차례대로 연결하여 저장한다.

인접리스트는 '연결 리스트'라는 자료구조를 이용해 구현하는데 프로그래밍 언어에서는 별도로 연결 리스트 기능을 위한 표준 라이브러리를 제공한다.

package CodeTest;

import java.util.ArrayList;

public class BFS_DFS0507 {
	
	//행 (Row)이 3개인 인접 리스트로 표현
	public static ArrayList<ArrayList<Node>> graph = new  ArrayList<ArrayList<Node>>();
	
	public static void main(String[] args) {
		
		//그래프 초기화
		for(int i=0; i<3; i++){
			graph.add(new ArrayList<Node>());
		}
		
		//노드 0에 연결된 노드 정보 저장 (노드, 거리)
		graph.get(0).add(new Node(1, 7));
		graph.get(0).add(new Node(2, 5));
		
		//노드 1에 연결된 노드 정보 저장 (노드, 거리)
		graph.get(1).add(new Node(0, 7));
		
		//노드 2에 연결된 노드 정보 저장 (노드, 거리)
		graph.get(2).add(new Node(0, 5));
		
		//그래프 출력
		for(int i=0; i<3; i++){
			for(int j=0; j<graph.get(i).size(); j++){
				graph.get(i).get(j).show();
			}
			System.out.println();
		}
	}

}

class Node{
	private int index;
	private int distance;
	
	public Node(int index, int distance){
		this.index = index;
		this.distance = distance;
	}
	
	public void show(){
		System.out.println("(" + this.index + ","+this.distance+")");
	}
}

이 두 방식은 어떤 차이가 있을 까?

메모리와 속도 측면을 살펴 보자.

메모리 측면에서 보자면 인접 행렬 방식은 모든 관계를 저장하므로 노드 개수가 많을 수록 메모리가 불필요하게 낭비된다.

반면에 인접 리스트 방식은 연결된 정보만을 저장하기 때문에 메모리를 효율적으로 사용한다.

하지만 이와 같은 속성 때문에 인접 리스트 방식은 인접 행렬 방식에 비해 특정한 두 노드가 연결되어 있는지에 대한 정보를 얻는 속도를 느리다.

인접 리스트 방식에서는 연결된 데이터를 하나씩 확인해야 하기 때문이다.

또 다른 예시로 그래프에서 노드 1과 노드 7이 연결되어 있는 상황을 생각하자.

인접 행렬 방식에서는 graph[1][7]만 확인하면된다.

반면에 인접리스트 방식에서는 노드 1에 대한 인접리스트를 앞에서부터 차례대로 확인해야 한다.

그러므로 특정한 노드와 연결된 모든 인접 노드를 순회해야 하는 경우, 인접 리스트 방식이 인접 행렬 방식에 비해 메모리 공간의 낭비가 적다.

DFS는 탐색을 위해서 사용되는 탐색 알고리즘이라고 했는데 구체적으로 어떻게 동작할까?

DFS는 깊이 우선 탐색 알고리즘이라고 했다.

이 알고리즘은 특정한 경로로 탐색하다가 특정한 상황에서 최대한 깊숙이 들어가서 노드를 방문한 후, 다시 돌아가 다른 경로를 탐색하는 알고리즘이다.

DFS는 스택 자료구조를 이용하여 구체적인 동작 과정은 다음과 같다.

  1. 탐색 시작 노드를 스택에 삽입하고 방문 처리를 한다.
  2. 스택의 최상단 노드에 방문하지 않은 인접 노드가 있으면 그 인접 노드를 스택에 넣고 방문 처리를 한다. 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼낸다.
  3. 2번의 과정을 더이상 수행할 수 없을 때까지 반복한다.
  • '방문 처리'는 스택에 한번 삽입되어 처리된 노드가 다시 삽입되지 않게 체크하는 것을 의미한다. 방문 처리를 함으로써 각 노드를 한 번씩만 처리할 수 있다.

일반적으로 인접한 노드 중에서 방문하지 않은 노드가 여러개 있다면 번호가 낮은 순서부터 처리한다.

  • DFS의 기능을 생각하면 순서와 상관없이 처리해도 되지만, 코딩 테스트에서는 번호가 낮은 순서부터 처리하도록 명시하는 경우가 종종있다. 따라서 관행적으로 번호가 낮은 순서부터 처리하도록 구현하는 편이다.

깊이 우선 탐색 알고리즘인 DFS는 스택 자료구조에 기초한다는 점에서 구현이 간단하다.

실제로는 스택을 쓰지 않아도 되며 탐색을 수행함에 있어서 데이터의 개수가 N개인 경우 O(N)의 시간이 소요된다는 특징이 있다.

또한 DFS는 스택을 이용하는 알고리즘이기 때문에 실제 구현은 재귀함수를 이용했을 경우 간결하게 구현할 수 있다.

package CodeTest;

import java.util.ArrayList;

public class BFS_DFS0508 {
	
	public static boolean[] visited = new boolean[9];
	public static ArrayList<ArrayList<Integer>> graph = new  ArrayList<ArrayList<Integer>>();
	
	//DFS 함수 정의
	public static void dfs(int x){
		//현재 노드를 방문 처리
		visited[x] = true;
		System.out.println(x + " ");
		//현재 노드와 연결된 다른 노드를 재귀적으로 방문
		for(int i=0; i<graph.get(x).size(); i++){
			int y = graph.get(x).get(i);
			if(!visited[y]) dfs(y);
		}
	}
	
	public static void main(String[] args) {
		//그래프 초기화
		for(int i=0; i<9; i++){
			graph.add(new ArrayList<Integer>());
		}
		
		//노드 1에 연결된 노드 정보 저장
		graph.get(1).add(2);
		graph.get(1).add(3);
		graph.get(1).add(8);
		
		// 노드 2에 연결된 노드 정보 저장 
        graph.get(2).add(1);
        graph.get(2).add(7);
        
        // 노드 3에 연결된 노드 정보 저장 
        graph.get(3).add(1);
        graph.get(3).add(4);
        graph.get(3).add(5);
        
        // 노드 4에 연결된 노드 정보 저장 
        graph.get(4).add(3);
        graph.get(4).add(5);
        
        // 노드 5에 연결된 노드 정보 저장 
        graph.get(5).add(3);
        graph.get(5).add(4);
        
        // 노드 6에 연결된 노드 정보 저장 
        graph.get(6).add(7);
        
        // 노드 7에 연결된 노드 정보 저장 
        graph.get(7).add(2);
        graph.get(7).add(6);
        graph.get(7).add(8);
        
        // 노드 8에 연결된 노드 정보 저장 
        graph.get(8).add(1);
        graph.get(8).add(7);

		dfs(1);
		
	}

}

BFS

BFS 알고리즘은 '너비 우선 탐색'이라는 의미를 가진다.

쉽게 말해 가까운 노드부터 탐색하는 알고리즘이다.

DFS는 최대한 멀리 있는 노드를 우선으로 탐색하는 방식으로 동작한다고 했는데, BFS는 그 반대다.

그렇다면 BFS는 실제로 어떤 방식으로 구현할 수 있을 까?

BFS 구현에서는 선입선출 방식인 큐 자료구조를 이용하는 것이 정석이다.

인접한 노드를 반복적으로 큐에 넣도록 알고리즘을 작성하면 자연스럽게 먼저 들어온 것이 먼저 나가게 되어, 가까운 노드부터 탐색을 진행하게 된다.

알고리즘의 정확한 동작 방식은 다음과 같다.

  1. 탐색 시작 노드를 큐에 삽입하고 방문 처리를 한다.
  2. 큐에서 노드를 꺼내 해당 노드의 인접 노드 중에서 방문하지 않은 노드를 모두 큐에 삽입하고 ㅁ방문 처리를 한다.
  3. 2번의 과정을 더 이상 수행할 수 없을 때까지 반복한다.

너비 우선 탐색 알고리즘인 BFS는 큐 자료구조에 기초한다는 점에서 구현이 간단한다.

실제 구현함에 있어 앞서 언급한 대로 큐 라이브러리를 사용하는 것이 좋으며 탐색을 수행함에 있어 O(N)의 시간이 소요된다.

일반적인 경우 실제 수행 시간은 DFS보다 좋은 편이라는 점까지만 추가로 기억하자.

  • 재귀 함수로 DFS를 구현하면 컴퓨터 시스템의 동작 특성상 실제 프로그램의 수행 시간은 느려질 수 있다. 따라서 스택 라이브러리를 이용해 시간 복잡도를 완화하는 테크닉이 필요할 때도 있다.
package CodeTest;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

public class BFS_DFS0509 {
	
	public static boolean[] visited = new boolean[9];
	public static ArrayList<ArrayList<Integer>> graph = new  ArrayList<ArrayList<Integer>>();
	
	// BFS 함수 정의
	public static void bfs(int start){
		Queue<Integer> q = new LinkedList<Integer>();
		q.offer(start);
		//현재 노드를 방문 처리
		visited[start] = true;
		//큐가 빌 때까지 반복
		while(!q.isEmpty()){
			//큐에서 하나의 원소를 뽑아 출력
			int x = q.poll();
			System.out.println(x + " ");
			//해당 원소와 연결된, 아직 방문하지 않은 원소들을 큐에 삽입
			for(int i = 0; i<graph.get(x).size(); i++){
				int y = graph.get(x).get(i);
				if(!visited[y]){
					q.offer(y);
					visited[y] = true;
				}
			}
		}
		
	}
	
	public static void main(String[] args) {
		//그래프 초기화
		for(int i=0; i<9; i++){
			graph.add(new ArrayList<Integer>());
		}
		
		//노드 1에 연결된 노드 정보 저장
		graph.get(1).add(2);
		graph.get(1).add(3);
		graph.get(1).add(8);
		
		// 노드 2에 연결된 노드 정보 저장 
        graph.get(2).add(1);
        graph.get(2).add(7);
        
        // 노드 3에 연결된 노드 정보 저장 
        graph.get(3).add(1);
        graph.get(3).add(4);
        graph.get(3).add(5);
        
        // 노드 4에 연결된 노드 정보 저장 
        graph.get(4).add(3);
        graph.get(4).add(5);
        
        // 노드 5에 연결된 노드 정보 저장 
        graph.get(5).add(3);
        graph.get(5).add(4);
        
        // 노드 6에 연결된 노드 정보 저장 
        graph.get(6).add(7);
        
        // 노드 7에 연결된 노드 정보 저장 
        graph.get(7).add(2);
        graph.get(7).add(6);
        graph.get(7).add(8);
        
        // 노드 8에 연결된 노드 정보 저장 
        graph.get(8).add(1);
        graph.get(8).add(7);

		bfs(1);
		
	}

}

DFS와 BFS의 구현에 대해 알아보았다.

간단히 정리하면 다음과 같다. 더 다양한 방식으로 구현할 수 있지만 위의 예제가 제일 간결한 방식이다.

DFS의 동작원리는 스택이며, 재귀함수를 이용한다.

BFS의 동작원리는 큐이며, 큐 자료구조를 이용한다.

앞서 DFS와 BFS를 그래프를 이용했는데, 1차원 배열이나, 2차원 배열또한 그래프 형태로 생각하면 수월하게 문제를 풀 수 있다.

특히나 DFS와 BFS 문제 유형이 그러하다.

예를 들어 게임 맵이 3 x 3 형태의 2차원 배열이고 각 데이터를 좌표라고 생각하자.

게임 캐릭터가 1,1 좌표에 있다고 표현할때 처럼 말이다.

이때 곽 좌표를 상하좌우로만 이동할 수있다면 그래프의 형태로 바꿔 생각할 수 있다.

코딩 테스트 중 2차원 배열에서의 탐색 문제를 만나면 이렇게 그래프 형태로 바꿔서 생각하면 풀이 방법을 조금 더 쉽게 떠올릴 수 있다.

그러므로 코딩 테스트에서 탐색 문제를 보면 그래프 형태로 표현한 다음 풀이법을 고민하도록 하자.


음료수 얼려 먹기

package CodeTest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class BFS_DFS0510 {
	
	public static int N, M;
	public static int[][] graph = new int[1000][1000];
	
	//DFS로 특정 노드를 방문하고 연결된 모든 노드들도 방문
	public static boolean dfs(int x, int y){
		//주어진 범위를 벗어나는 경우에는 즉시 종료
		if(x <= -1 || x >=N || y <= -1 || y >= M){
			return false;
		}
		//현재 노드를 아직 방문하지 않았다면
		if(graph[x][y] == 0){
			//해당 노드 방문 처리
			graph[x][y] = 1;
			//상,하,좌,우의 위치들도 모두 재귀적으로 호출
			dfs(x-1, y);
			dfs(x, y-1);
			dfs(x+1, y);
			dfs(x, y+1);
			return true;
		}
		return false;
	}
	

	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		N = Integer.parseInt(st.nextToken()); //세로 길이  N
		M = Integer.parseInt(st.nextToken()); // 가로 길이 M 
		
		//맵 정보 입력 받기
		for(int i=0; i<N; i++){
			String str = br.readLine();
			for(int j=0; j<M; j++){
				graph[i][j] = str.charAt(j)-'0';
			}
		}
		
		
		//모든 노드에 대하여 음료수 채우기
		int result = 0;
		for(int i=0; i<N; i++){
			for(int j=0; j<M; j++){
				//현재 위치에서 DFS 수행
				if(dfs(i,j)){
					result ++;
				}
			}
		}
		System.out.println(result);
	
	}
	
}
  1. 특정한 지점의 주변 상,하,좌,를 살펴본 뒤에 주변 지점 중에서 값이 '0'이면서 아직 방문하지 않은 지점이 있다면 해당 지점을 방문한다.
  2. 방문한 지점에서 다시 상,하,좌,우를 살펴보면서 방문을 다시 진행하면, 연결된 모든 지점을 방문할 수 있다.
  3. 1 ~ 2 번의 과정을 모든 노드에 반복하여 방문하지 않은 지점의 수를 센다.

미로 탈출

package CodeTest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

public class BFS_DFS0511 {
	
	public static int N, M;
	public static int[][] graph = new int[201][201];
	
	//이동할 네 가지 방향 정의 (상, 하, 좌, 우)
	public static int dx[] = {-1, 1, 0, 0};
	public static int dy[] = {0, 0, -1, 1};
	
	public static int bfs(int x, int y){
		//큐구현을 위해서 Queue 라이브러리 사용
		Queue<Node> q = new LinkedList<Node>();
		q.offer(new Node(x, y));
		//큐가 빌 때까지 반복하기
		while(!q.isEmpty()){
			Node node = q.poll();
			x = node.getX();
			y = node.getY();
			//현재 위치에서 4가지 방향으로의 확인
			for(int i=0; i<4; i++){
				int nx = x + dx[i];
				int ny = y + dy[i];
				//미로 찾기 공간을 벗어난 경우 무시
				if(nx < 0 || nx >= N || ny < 0 || ny >=M) continue;
				//벽인 경우 무시
				if(graph[nx][ny] == 0) continue;
				//해당 노드를 처음 방문하는 경우에만 최단 거리 기록
				if(graph[nx][ny] == 1){
					graph[nx][ny] = graph[x][y] + 1;
					q.offer(new Node(nx,ny));
				}
			}
		}
		//가장 오른쪽 아래까지의 최단 거리 반환
		return graph[N - 1][M - 1];
	}

	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		N = Integer.parseInt(st.nextToken()); //세로 길이  N
		M = Integer.parseInt(st.nextToken()); // 가로 길이 M 
		
		//맵 정보 입력 받기
		for(int i=0; i<N; i++){
			String str = br.readLine();
			for(int j=0; j<M; j++){
				graph[i][j] = str.charAt(j)-'0';
			}
		}
		
		//출력 결과
		System.out.println(bfs(0,0));
		
	}

}

class Node{
	private int x;
	private int y;
	
	public Node(int x, int y){
		this.x = x;
		this.y = y;
	}
	
	public int getX(){
		return this.x;
	}
	
	public int getY(){
		return this.y;
	}
}

BFS를 이용했을 때 효과적으로 해결할 수 있다.

BFS는 시작 지점에서 가까운 노드부터 차례대로 모든 노드를 탐색하기 때문이다.

'알고리즘 & 자료구조 > 알고리즘 개념' 카테고리의 다른 글

재귀함수 (2)  (0) 2021.11.10
재귀함수 (1)  (0) 2021.11.09
구현  (0) 2021.11.04
그리디 (Greedy)  (0) 2021.11.01
복잡도  (0) 2021.11.01