철수의 토마토 농장에서는 토마토를 보관하는 큰 창고를 가지고 있다. 토마토는 아래의 그림과 같이 격자모양 상자의 칸에 하나씩 넣은 다음, 상자들을 수직으로 쌓아 올려서 창고에 보관한다.

창고에 보관되는 토마토들 중에는 잘 익은 것도 있지만, 아직 익지 않은 토마토들도 있을 수 있다. 보관 후 하루가 지나면, 익은 토마토들의 인접한 곳에 있는 익지 않은 토마토들은 익은 토마토의 영향을 받아 익게 된다. 하나의 토마토에 인접한 곳은 위, 아래, 왼쪽, 오른쪽, 앞, 뒤 여섯 방향에 있는 토마토를 의미한다. 대각선 방향에 있는 토마토들에게는 영향을 주지 못하며, 토마토가 혼자 저절로 익는 경우는 없다고 가정한다. 철수는 창고에 보관된 토마토들이 며칠이 지나면 다 익게 되는지 그 최소 일수를 알고 싶어 한다.

토마토를 창고에 보관하는 격자모양의 상자들의 크기와 익은 토마토들과 익지 않은 토마토들의 정보가 주어졌을 때, 며칠이 지나면 토마토들이 모두 익는지, 그 최소 일수를 구하는 프로그램을 작성하라. 단, 상자의 일부 칸에는 토마토가 들어있지 않을 수도 있다.

입력


첫 줄에는 상자의 크기를 나타내는 두 정수 M,N과 쌓아올려지는 상자의 수를 나타내는 H가 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 ≤ M ≤ 100, 2 ≤ N ≤ 100, 1 ≤ H ≤ 100 이다. 둘째 줄부터는 가장 밑의 상자부터 가장 위의 상자까지에 저장된 토마토들의 정보가 주어진다. 즉, 둘째 줄부터 N개의 줄에는 하나의 상자에 담긴 토마토의 정보가 주어진다. 각 줄에는 상자 가로줄에 들어있는 토마토들의 상태가 M개의 정수로 주어진다. 정수 1은 익은 토마토, 정수 0 은 익지 않은 토마토, 정수 -1은 토마토가 들어있지 않은 칸을 나타낸다. 이러한 N개의 줄이 H번 반복하여 주어진다.

토마토가 하나 이상 있는 경우만 입력으로 주어진다.

 

 

출력


여러분은 토마토가 모두 익을 때까지 최소 며칠이 걸리는지를 계산해서 출력해야 한다. 만약, 저장될 때부터 모든 토마토가 익어있는 상태이면 0을 출력해야 하고, 토마토가 모두 익지는 못하는 상황이면 -1을 출력해야 한다.

 

 

 

풀이


public class Main {
	static int N, M, H;

	static int[][][] field;

	static Queue<int[]> queue = new LinkedList<>();

	static int[] dx = {0,0,0,0,-1,1};
	static int[] dy = {0,0,1,-1,0,0};
	static int[] dz = {-1, 1};

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		M = Integer.parseInt(st.nextToken());
		N = Integer.parseInt(st.nextToken());
		H = Integer.parseInt(st.nextToken());
		field = new int[N][M][H];
		for (int h=0; h<H; h++){
			for (int i=0; i<N; i++){
				st = new StringTokenizer(br.readLine());
				for (int j=0; j<M; j++){
					field[i][j][h] = Integer.parseInt(st.nextToken());
					if (field[i][j][h] == 1){
						queue.add(new int[]{i,j,h});
					}
				}
			}
		}
		int max = 0;
		BFS();


	}
	static void BFS(){
		int count = 0;
		while (!queue.isEmpty()){
			int[] point = queue.poll();
			for (int i=0; i<6; i++){
				int nx = point[0] + dx[i];
				int ny = point[1] + dy[i];
				int nz = point[2];
				if (i < 2){
					nz = point[2] + dz[i];
				}
				if (nx >= 0 && ny >= 0 && nz >= 0 && nx < N && ny < M && nz < H){
					if (field[nx][ny][nz] == 0){
						field[nx][ny][nz] = field[point[0]][point[1]][point[2]] + 1;
						count = Math.max(count, field[nx][ny][nz]);
						queue.offer(new int[]{nx,ny,nz});
					}
				}
			}

		}
		if (check()){
			if (count == 0){
				System.out.println(count);
			} else{
				System.out.println(count -1);
			}
		} else {
			System.out.println("-1");
		}
	}

	static boolean check(){
		for (int i=0; i<H; i++){
			for (int j=0; j<N; j++){
				for (int k=0; k<M; k++){
					if (field[j][k][i] == 0){
						return false;
					}
				}
			}
		}
		return true;
	}

}

해설


https://choincnp.tistory.com/95

 

[백준/7576] - 토마토(JAVA)

철수의 토마토 농장에서는 토마토를 보관하는 큰 창고를 가지고 있다. 토마토는 아래의 그림과 같이 격자 모양 상자의 칸에 하나씩 넣어서 창고에 보관한다. 창고에 보관되는 토마토들 중에는

choincnp.tistory.com

 

기존의 토마토 문제에 상하 위치만 추가된 문제이다.

시작점이 여러개이므로 큐를 클래스 변수로 선언하고, 가로 세로 상단에 맞추어 field에 토마토를 담은 뒤

BFS를 돌려주면 되는 문제이다. 물론 BFS를 재귀 형태로 돌려주어도 되지만, 델타배열로 선언하였다.

 

BFS의 Well-Known 문제로, 특별히 어려운 점은 없는 문제이다.

'알고리즘 > 백준' 카테고리의 다른 글

[백준/2143] - 두 배열의 합(JAVA)  (0) 2023.09.27
[백준/2573] - 빙산(JAVA)  (0) 2023.09.20
[백준/14502] - 연구소(JAVA)  (0) 2023.09.19
[백준/7576] - 토마토(JAVA)  (0) 2023.09.19
[백준/1697] - 숨바꼭질(JAVA)  (0) 2023.09.18

인체에 치명적인 바이러스를 연구하던 연구소에서 바이러스가 유출되었다. 다행히 바이러스는 아직 퍼지지 않았고, 바이러스의 확산을 막기 위해서 연구소에 벽을 세우려고 한다.

연구소는 크기가 N×M인 직사각형으로 나타낼 수 있으며, 직사각형은 1×1 크기의 정사각형으로 나누어져 있다. 연구소는 빈 칸, 벽으로 이루어져 있으며, 벽은 칸 하나를 가득 차지한다. 

일부 칸은 바이러스가 존재하며, 이 바이러스는 상하좌우로 인접한 빈 칸으로 모두 퍼져나갈 수 있다. 새로 세울 수 있는 벽의 개수는 3개이며, 꼭 3개를 세워야 한다.

예를 들어, 아래와 같이 연구소가 생긴 경우를 살펴보자.

2 0 0 0 1 1 0
0 0 1 0 1 2 0
0 1 1 0 1 0 0
0 1 0 0 0 0 0
0 0 0 0 0 1 1
0 1 0 0 0 0 0
0 1 0 0 0 0 0

이때, 0은 빈 칸, 1은 벽, 2는 바이러스가 있는 곳이다. 아무런 벽을 세우지 않는다면, 바이러스는 모든 빈 칸으로 퍼져나갈 수 있다.

2행 1열, 1행 2열, 4행 6열에 벽을 세운다면 지도의 모양은 아래와 같아지게 된다.

2 1 0 0 1 1 0
1 0 1 0 1 2 0
0 1 1 0 1 0 0
0 1 0 0 0 1 0
0 0 0 0 0 1 1
0 1 0 0 0 0 0
0 1 0 0 0 0 0

바이러스가 퍼진 뒤의 모습은 아래와 같아진다.

2 1 0 0 1 1 2
1 0 1 0 1 2 2
0 1 1 0 1 2 2
0 1 0 0 0 1 2
0 0 0 0 0 1 1
0 1 0 0 0 0 0
0 1 0 0 0 0 0

벽을 3개 세운 뒤, 바이러스가 퍼질 수 없는 곳을 안전 영역이라고 한다. 위의 지도에서 안전 영역의 크기는 27이다.

연구소의 지도가 주어졌을 때 얻을 수 있는 안전 영역 크기의 최댓값을 구하는 프로그램을 작성하시오.

입력


첫째 줄에 지도의 세로 크기 N과 가로 크기 M이 주어진다. (3 ≤ N, M ≤ 8)

둘째 줄부터 N개의 줄에 지도의 모양이 주어진다. 0은 빈 칸, 1은 벽, 2는 바이러스가 있는 위치이다. 2의 개수는 2보다 크거나 같고, 10보다 작거나 같은 자연수이다.

빈 칸의 개수는 3개 이상이다.

 

 

출력


첫째 줄에 얻을 수 있는 안전 영역의 최대 크기를 출력한다.

 

 

풀이


public class Main {
	static int N, M, max;
	static int[][] field;
	static int[][] copiedField;
	static int[] dx = {0,0,-1,1};
	static int[] dy = {1,-1,0,0};

	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());
		M = Integer.parseInt(st.nextToken());
		field = new int[N][M];
		for (int i = 0; i < N; i++) {
			st = new StringTokenizer(br.readLine());
			for (int j = 0; j < M; j++) {
				field[i][j] = Integer.parseInt(st.nextToken());
			}
		}
		DFS(0);
		System.out.println(max);

	}
	static void DFS(int wall){
		if (wall == 3){
			BFS();
			return;
		}
		for (int i=0; i<N; i++){
			for (int j=0; j<M; j++){
				if (field[i][j] == 0){
					field[i][j] = 1;
					DFS(wall+1);
					field[i][j] = 0;
				}
			}
		}
	}

	static void BFS(){
		Queue<int[]> queue = new LinkedList<>();
		copiedField = new int[N][M];
		int count = 0;
		for (int i=0; i<N; i++){
			for (int j=0; j<M; j++){
				copiedField[i][j] = field[i][j];
				if (field[i][j] == 2){
					queue.offer(new int[]{i, j});
				}
			}
		}

		while (!queue.isEmpty()){
			int[] point = queue.poll();
			for (int i=0; i<4; i++){
				int nx = point[0] + dx[i];
				int ny = point[1] + dy[i];
				if (nx>=0 && ny >= 0 && nx < N && ny < M){
					if (copiedField[nx][ny] == 0){
						copiedField[nx][ny] = 2;
						queue.offer(new int[]{nx, ny});
					}
				}
			}
		}

		for (int i=0; i<N; i++){
			for (int j=0; j<M; j++){
				if (copiedField[i][j] == 0){
					count++;
				}
			}
		}
		max = Math.max(max, count);
	}
}

해설


기존의 BFS에 완전탐색이 가미된 문제이다.

인풋 기준 여러개를 분석하면 최대 안전지대를 구하기 위해서는 다음과 같은 벽을 세워야 한다.

마지막 입력같은경우에는 여러개의 케이스를 돌려봐도 가장 첫번째 아래의 3개 칸을 안전지대로 만드는게 베스트였다.

그렇다면 정확한 규칙은 찾기 어려우니 전체 케이스를 탐색하는 완전탐색의 경우가 필요하다고 생각하면 되겠다.

 

결국 DFS + BFS를 모두 이용해서

벽을 3개 세우고, 벽이 3개 채워졌다면 BFS를 이용해서 안전지대를 탐색한 후, 최대의 안전지대를 도출하면 된다.

 

public class Main {
	static int N, M, max;
	static int[][] field;
	static int[][] copiedField;
	static int[] dx = {0,0,-1,1};
	static int[] dy = {1,-1,0,0};

	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());
		M = Integer.parseInt(st.nextToken());
		field = new int[N][M];
		for (int i = 0; i < N; i++) {
			st = new StringTokenizer(br.readLine());
			for (int j = 0; j < M; j++) {
				field[i][j] = Integer.parseInt(st.nextToken());
			}
		}
		DFS(0);
		System.out.println(max);

	}

가로, 세로, 그리고 최댓값인 max 변수를 선언하고,

field배열과, 벽을 3개 세운 후 나머지를 그대로 자리에 넣어줄 카피배열을 선언한다.

그리고 DFS를 실행한 후, max값을 도출하면 된다.

static void DFS(int wall){
 if (wall == 3){
  BFS();
  return;
 }
 for (int i=0; i<N; i++){
  for (int j=0; j<M; j++){
   if (field[i][j] == 0){
    field[i][j] = 1;
    DFS(wall+1);
    field[i][j] = 0;
   }
  }
 }
}

완전탐색에 필요한 DFS 함수이다.

파라미터로 int타입의 wall을 받으며 재귀적으로 DFS를 호출하는데,

field의 값이 0일 경우에만 진행하도록 하며, wall이 3개 세워졌을 경우 BFS를 호출한다.

BFS로 최대 안전지대가 구해졌을 경우 다음 경우를 위해 field[i][j]를 0으로 바꿔준다.

바꾸지 않을 경우 field가 영구적으로 1로 변해버린다.

 

static void BFS(){
		Queue<int[]> queue = new LinkedList<>();
		copiedField = new int[N][M];
		int count = 0;
		for (int i=0; i<N; i++){
			for (int j=0; j<M; j++){
				copiedField[i][j] = field[i][j];
				if (field[i][j] == 2){
					queue.offer(new int[]{i, j});
				}
			}
		}

		while (!queue.isEmpty()){
			int[] point = queue.poll();
			for (int i=0; i<4; i++){
				int nx = point[0] + dx[i];
				int ny = point[1] + dy[i];
				if (nx>=0 && ny >= 0 && nx < N && ny < M){
					if (copiedField[nx][ny] == 0){
						copiedField[nx][ny] = 2;
						queue.offer(new int[]{nx, ny});
					}
				}
			}
		}

		for (int i=0; i<N; i++){
			for (int j=0; j<M; j++){
				if (copiedField[i][j] == 0){
					count++;
				}
			}
		}
		max = Math.max(max, count);
	}

마지막으로 BFS함수이다.

큐를 선언하고, field 배열을 돌면서 2일경우에만 큐에 집어넣도록 한다.

copiedfield = field를 하는데, BFS가 실행될 경우에는 field에 이미 벽이 3개 채워진 상태에서만 BFS가 실행되므로

copiedfield에 모든 field를 배껴도 된다. 물론 다른 깊은복사 로직을 이용해도 된다.

배열을 돌면서 큐에 시작점을 채워넣었으면, 다음에 할 로직은 똑같다.

전염시키고(copiedfield[nx][ny] = 2)

큐에 빈 배열을 집어넣어주면 된다.(copiedfield[nx][ny] == 0 >> queue.offer(new int[]{nx,ny}))

 

그리고 마지막에 배열을 돌면서 값이 0인 안전지대를 찾아준 후, max값과 비교하고 넘어가면 된다.

'알고리즘 > 백준' 카테고리의 다른 글

[백준/2573] - 빙산(JAVA)  (0) 2023.09.20
[백준/7569] - 토마토(JAVA)  (0) 2023.09.19
[백준/7576] - 토마토(JAVA)  (0) 2023.09.19
[백준/1697] - 숨바꼭질(JAVA)  (0) 2023.09.18
[백준/14940] - 쉬운 최단거리(JAVA)  (0) 2023.09.17

철수의 토마토 농장에서는 토마토를 보관하는 큰 창고를 가지고 있다. 토마토는 아래의 그림과 같이 격자 모양 상자의 칸에 하나씩 넣어서 창고에 보관한다.

창고에 보관되는 토마토들 중에는 잘 익은 것도 있지만, 아직 익지 않은 토마토들도 있을 수 있다. 보관 후 하루가 지나면, 익은 토마토들의 인접한 곳에 있는 익지 않은 토마토들은 익은 토마토의 영향을 받아 익게 된다. 하나의 토마토의 인접한 곳은 왼쪽, 오른쪽, 앞, 뒤 네 방향에 있는 토마토를 의미한다. 대각선 방향에 있는 토마토들에게는 영향을 주지 못하며, 토마토가 혼자 저절로 익는 경우는 없다고 가정한다. 철수는 창고에 보관된 토마토들이 며칠이 지나면 다 익게 되는지, 그 최소 일수를 알고 싶어 한다.

토마토를 창고에 보관하는 격자모양의 상자들의 크기와 익은 토마토들과 익지 않은 토마토들의 정보가 주어졌을 때, 며칠이 지나면 토마토들이 모두 익는지, 그 최소 일수를 구하는 프로그램을 작성하라. 단, 상자의 일부 칸에는 토마토가 들어있지 않을 수도 있다.

입력


첫 줄에는 상자의 크기를 나타내는 두 정수 M,N이 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 ≤ M,N ≤ 1,000 이다. 둘째 줄부터는 하나의 상자에 저장된 토마토들의 정보가 주어진다. 즉, 둘째 줄부터 N개의 줄에는 상자에 담긴 토마토의 정보가 주어진다. 하나의 줄에는 상자 가로줄에 들어있는 토마토의 상태가 M개의 정수로 주어진다. 정수 1은 익은 토마토, 정수 0은 익지 않은 토마토, 정수 -1은 토마토가 들어있지 않은 칸을 나타낸다.

토마토가 하나 이상 있는 경우만 입력으로 주어진다.

 

 

출력


여러분은 토마토가 모두 익을 때까지의 최소 날짜를 출력해야 한다. 만약, 저장될 때부터 모든 토마토가 익어있는 상태이면 0을 출력해야 하고, 토마토가 모두 익지는 못하는 상황이면 -1을 출력해야 한다.

 

풀이


public class Main {
    static int N, M, max;
    static int[][] field;

    static Queue<int[]> queue = new LinkedList<>();
    static int[] dx = {0,0,-1,1};
    static int[] dy = {1,-1,0,0};

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        M = Integer.parseInt(st.nextToken());
        N = Integer.parseInt(st.nextToken());
        field = new int[N][M];
        for (int i=0; i<N; i++){
            st = new StringTokenizer(br.readLine());
            for (int j=0; j<M; j++){
                field[i][j] = Integer.parseInt(st.nextToken());
                if (field[i][j] == 1) {
                    queue.offer(new int[]{i, j});
                }
            }
        }
        max = 0;
        BFS();
        for(int i=0; i<N; i++){
            for (int j=0; j<M; j++){
                if (field[i][j] == 0) {
                    max = -1;
                    break;
                }
            }
        }
        System.out.println(max == -1 ? max : max-1);

    }
    static void BFS(){
        while (!queue.isEmpty()){
            int[] point = queue.poll();
            max = Math.max(field[point[0]][point[1]], max);
            for (int i=0; i<4; i++){
                int nx = point[0] + dx[i];
                int ny = point[1] + dy[i];
                if (nx >= 0 && ny >= 0 && nx < N && ny < M){
                    if (field[nx][ny] == 0){
                        queue.add(new int[]{nx, ny});
                        field[nx][ny] = Math.max(field[nx][ny],field[point[0]][point[1]] + 1);
                    }
                }
            }
        }
    }

}

해설


BFS의 대표 문제라고 널리 알려진, 토마토 문제이다.

'토마토가 언제 익는 / 빙산이 언제 녹는 / 목표까지 최단거리가 얼마인' 문제는 거의다 BFS로 풀면 된다고 생각하면 된다.

바로 메인함수부터 살펴보자.

public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        M = Integer.parseInt(st.nextToken());
        N = Integer.parseInt(st.nextToken());
        field = new int[N][M];
        for (int i=0; i<N; i++){
            st = new StringTokenizer(br.readLine());
            for (int j=0; j<M; j++){
                field[i][j] = Integer.parseInt(st.nextToken());
                if (field[i][j] == 1) {
                    queue.offer(new int[]{i, j});
                }
            }
        }
        max = 0;
        BFS();
        for(int i=0; i<N; i++){
            for (int j=0; j<M; j++){
                if (field[i][j] == 0) {
                    max = -1;
                    break;
                }
            }
        }
        System.out.println(max == -1 ? max : max-1);

    }

메인함수는 단순한 로직으로 이루어진다.

  1. 시작점이 여러개일 수 있기 때문에 배열을 돌면서 큐에 시작점을 넣고 돌린다.
  2. BFS를 돌린다.
  3. 필드를 돌면서 0이 있으면 도달할 수 없는 부분이기 때문에 답을 -1로 프린트한다.
static void BFS(){
        while (!queue.isEmpty()){
            int[] point = queue.poll();
            max = Math.max(field[point[0]][point[1]], max);
            for (int i=0; i<4; i++){
                int nx = point[0] + dx[i];
                int ny = point[1] + dy[i];
                if (nx >= 0 && ny >= 0 && nx < N && ny < M){
                    if (field[nx][ny] == 0){
                        queue.add(new int[]{nx, ny});
                        field[nx][ny] = Math.max(field[nx][ny],field[point[0]][point[1]] + 1);
                    }
                }
            }
        }
    }

BFS함수 또한 시작점이 여러개일 수 있기 때문에, 기본 BFS 함수에서 조금 변형점을 주어야 한다.

어떤 포인트[x,y]가 있고, 시작점1에서 이 포인트에 도달할 수 있는 최소값과 시작점2에서 이 포인트에 도달할수 있는 최소값이 다를 수 있다.

그렇지만 결국 우리가 원하는건 토마토가 전부 익는데 걸리는 시간이기 때문에 max에 값을 기입해준다.

 

'알고리즘 > 백준' 카테고리의 다른 글

[백준/7569] - 토마토(JAVA)  (0) 2023.09.19
[백준/14502] - 연구소(JAVA)  (0) 2023.09.19
[백준/1697] - 숨바꼭질(JAVA)  (0) 2023.09.18
[백준/14940] - 쉬운 최단거리(JAVA)  (0) 2023.09.17
[백준/2178] - 미로 탐색(JAVA)  (0) 2023.09.17

수빈이는 동생과 숨바꼭질을 하고 있다. 수빈이는 현재 점 N(0 ≤ N ≤ 100,000)에 있고, 동생은 점 K(0 ≤ K ≤ 100,000)에 있다. 수빈이는 걷거나 순간이동을 할 수 있다. 만약, 수빈이의 위치가 X일 때 걷는다면 1초 후에 X-1 또는 X+1로 이동하게 된다. 순간이동을 하는 경우에는 1초 후에 2*X의 위치로 이동하게 된다.

수빈이와 동생의 위치가 주어졌을 때, 수빈이가 동생을 찾을 수 있는 가장 빠른 시간이 몇 초 후인지 구하는 프로그램을 작성하시오.

입력


첫 번째 줄에 수빈이가 있는 위치 N과 동생이 있는 위치 K가 주어진다. N과 K는 정수이다.

 

출력


수빈이가 동생을 찾는 가장 빠른 시간을 출력한다.

 

 

풀이


public class bojtest {
    static int N, K, next;
    static int[] field;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        field = new int[100001];
        N = Integer.parseInt(st.nextToken());
        K = Integer.parseInt(st.nextToken());
        field[N] = 1;
        if (N == K){
            System.out.println("0");
        } else {
            BFS(N);
        }
    }
    static void BFS(int x){
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(x);
        while (!queue.isEmpty()){
            int point = queue.poll();
            for (int i=0; i<3; i++){
                if (i==0){
                    next = point + 1;
                }
                else if (i==1){
                    next = point - 1;
                } else {
                    next = point * 2;
                }
                if (next == K){
                    System.out.println(field[point]);
                    return;
                }
                if (next >= 0 && next < 100001 && field[next] == 0){
                    queue.offer(next);
                    field[next] = field[point] + 1;
                }
            }
        }
    }

}

해설


'최단거리 길찾기'라는 말을 통해 BFS를 이용한 풀이임을 얼추 알 수 있다.

다른 문제와는 달리 방문 배열이 없어도 되는데, 방문 배열과 배열에 기록을 동시에 할 것이기 때문에 필요없다.

public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        field = new int[100001];
        N = Integer.parseInt(st.nextToken());
        K = Integer.parseInt(st.nextToken());
        field[N] = 1;
        if (N == K){
            System.out.println("0");
        } else {
            BFS(N);
        }
    }

 

먼저, N과 K 둘다 100,000 이내의 숫자기때문에 배열을 만들고 첫번째 시작부터 1을 준다.

static void BFS(int x){
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(x);
        while (!queue.isEmpty()){
            int point = queue.poll();
            for (int i=0; i<3; i++){
                if (i==0){
                    next = point + 1;
                }
                else if (i==1){
                    next = point - 1;
                } else {
                    next = point * 2;
                }
                if (next == K){
                    System.out.println(field[point]);
                    return;
                }
                if (next >= 0 && next < 100001 && field[next] == 0){
                    queue.offer(next);
                    field[next] = field[point] + 1;
                }
            }
        }
    }

이번 문제는 BFS 펑션이 굉장히 중요한 문제였다.

큐를 선언하고 offer하는것까진 동일한데, 위치를 잘 설정해 주어야 한다.

next를 잘 설정하고, next == k일경우 바로 return문으로 빠져나오는 문장이 있어야 하고,

여태까지 작성한 !visited[x]와 같은 역할을 하는 field[next]==0을 만족하게 되면 offer를 해준다. 그리고 field 배열에 다시 기록해준다.

<BFS 실행 순서>

먼저 5 17이라는 인풋을 가지고 예를 들어 보자,

  • 처음 [5]를 큐에 넣고 돌린다. 그러면 [5]와 연결되어 있는 [4,6,10]이 큐에 들어간다.
    • 현재 큐 = [4,6,10]
    • field[4] = field[6] = field[10] = 1로 채워진다.
  • 그 다음은 4가 뽑혀서 [3,5,8]이 큐에 들어가는데, 5는 이미 방문했으므로 큐에 들어가지 않는다.
    • 현재 큐 = [6,10,3,8]
    • field[3] = field[8] = field[4]+1 = 2로 채워진다.
  • 그다음은 6이 뽑혀서 [5, 7, 12]가 큐에 들어가야 하는데, 5는 이미 방문했으므로 큐에 들어가지 않는다.
    • 현재 큐 = [10, 3, 8, 7, 12]
    • field[7] = field[12] = field[6]+1 = 2로 채워진다.
  • 그다음은 10이 뽑혀서 [9,11,20]이 큐에 들어간다.
    • 현재 큐 = [3,8,7,12,9,11,20]
    • field[9] = field[11] = field[20] = field[10] +1 = 2로 채워진다.
  • 그 다음은 3이 뽑혀서 [2,4,6]이 큐에 들어가야 하는데, 4와 6은 이미 방문했으므로 큐에 들어가지 않는다.
    • 현재 큐 = [8,7,12,9,11,20,2]
    • field[2] = field[3] + 1 = 3으로 채워진다.
  • 그 다음은 8이 뽑혀서 [7,9,16]이 큐에 들어가야 하는데, field[7]과 field[9]는 이미 숫자가 있으므로 큐에 들어가지 않는다.
    • 현재 큐 = [7,12,9,11,20,2,16]
    • field[16] = field[8] + 1 = 3
  • 그 다음은 7이 뽑혀서 [6,8,14]가 큐에 들어가야 하는데, field[6]과 field[8]은 이미 숫자가 있으므로 큐에 들어가지 않는다.
    • 현재 큐 = [12,9,11,20,2,16,14]
    • field[14] = field[7] + 1 = 3
  • 그 다음은 12가 뽑혀서 [11,13,24]가 큐에 들어가야 하는데, field[11]은 이미 숫자가 있으므로 큐에 들어가지 않는다.
    • 현재 큐 = [9,11,20,2,16,14]
    • field[13] = field[24] = field[12] + 1 = 3
  • 그 다음은 9가 뽑혀서 [8,10,18]이 큐에 들어가야 하는데, field[8]과 field[10]은 이미 숫자가 있으므로 큐에 들어가지 않는다.
    • 현재 큐 = [11,20,2,16,14,18]
    • field[18] = field[9] + 1 = 3
  • 그 다음은 11이 뽑혀서 [10, 12, 22]가 큐에 들어가야 하는데, field[10]과 field[12]는 이미 숫자가 있으므로 큐에 들어가지 않는다.
    • 현재 큐 = [20,2,16,14,18,22]
    • field[22] = field[11] + 1 = 3
  • 그 다음은 20이 뽑혀서 [19,21,40]이 큐에 들어간다.
    • 현재 큐 = [2,16,14,18,22,19,21,40]
    • field[19] = field[21] = field[40] = field[20] + 1 = 3
  • 그 다음은 2가 뽑혀서 [1,3,4]가 큐에 들어가야 하는데, field[3]과 field[4]는 이미 숫자가 있으므로 큐에 들어가지 않는다.
    • 현재 큐 = [16,14,18,22,19,21,40.1]
    • field[1] = field[2] + 1 = 4
  • 그 다음은 16이 뽑혀서 [15,17,32]가 큐에 들어가야 하는데, 17은 우리가 원하는 Target이므로 field정보를 출력한다
    • field[17] = field[16] + 1 = 4

생각해보면, 숫자들이 전부 접근하는 최솟값임을 알 수 있다. 이렇게 늘어놓아서 굉장히 길어 보이지만, 사실은 굉장히 빠르게 연산이 되고 있다. 

 

다시 백트래킹을 해보면, 17까지 이르는 동안

5 > 4 > 8 > 16 > 17로 해왔지만, 같은 5뎁스 안에

5 > 10 > 9 > 18 > 17도 경로가 될 수 있음을 알 수 있다.

 

다른 문제에서, 이 최단 경로가 몇개인지를 찾아보도록 하자.

 

'알고리즘 > 백준' 카테고리의 다른 글

[백준/14502] - 연구소(JAVA)  (0) 2023.09.19
[백준/7576] - 토마토(JAVA)  (0) 2023.09.19
[백준/14940] - 쉬운 최단거리(JAVA)  (0) 2023.09.17
[백준/2178] - 미로 탐색(JAVA)  (0) 2023.09.17
[백준/1260] - DFS와 BFS(JAVA)  (0) 2023.09.17

지도가 주어지면 모든 지점에 대해서 목표지점까지의 거리를 구하여라.

문제를 쉽게 만들기 위해 오직 가로와 세로로만 움직일 수 있다고 하자.

입력


지도의 크기 n과 m이 주어진다. n은 세로의 크기, m은 가로의 크기다.(2 ≤ n ≤ 1000, 2 ≤ m ≤ 1000)

다음 n개의 줄에 m개의 숫자가 주어진다. 0은 갈 수 없는 땅이고 1은 갈 수 있는 땅, 2는 목표지점이다. 입력에서 2는 단 한개이다.

 

출력


각 지점에서 목표지점까지의 거리를 출력한다. 원래 갈 수 없는 땅인 위치는 0을 출력하고, 원래 갈 수 있는 땅인 부분 중에서 도달할 수 없는 위치는 -1을 출력한다.

 

 

풀이


public class Main {
    static int N, M, x, y;
    static int[][] field;
    static boolean[][] visited;
    static int[] dx = {0,0,-1,1};
    static int[] dy = {1,-1,0,0};

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        field = new int[N][M];
        visited = new boolean[N][M];
        for (int i=0; i<N; i++){
            st = new StringTokenizer(br.readLine());
            for (int j=0; j<M; j++){
                int num = Integer.parseInt(st.nextToken());
                field[i][j] = num;
                if (num == 2){
                    x = i;
                    y = j;
                }
            }
        }
        field[x][y] = 0;
        BFS(x, y);
        for (int i=0; i<N; i++){
            for (int j=0; j<M; j++){
                if (!visited[i][j] && field[i][j] != 0){
                    field[i][j] = -1;
                }
                sb.append(field[i][j]).append(" ");
            }
            sb.append("\n");
        }
        System.out.println(sb.toString());
    }
    static void BFS(int x, int y){
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{x, y});
        visited[x][y] = true;

        while (!queue.isEmpty()){
            int[] now = queue.poll();
            for (int i=0; i<4; i++){
                int nx = now[0] + dx[i];
                int ny = now[1] + dy[i];
                if (nx >= 0 && ny >= 0 && nx < N && ny < M){
                    if (!visited[nx][ny] && field[nx][ny] > 0){
                        visited[nx][ny] = true;
                        queue.offer(new int[]{nx, ny});
                        field[nx][ny] = field[now[0]][now[1]] + 1;
                    }
                }
            }
        }

    }

}

 

해설


https://choincnp.tistory.com/92

 

[백준/2178] - 미로 탐색(JAVA)

N×M크기의 배열로 표현되는 미로가 있다. 미로에서 1은 이동할 수 있는 칸을 나타내고, 0은 이동할 수 없는 칸을 나타낸다. 이러한 미로가 주어졌을 때, (1, 1)에서 출발하여 (N, M)의 위치로 이동할

choincnp.tistory.com

미로 탐색 문제보다 조금 더 어려운 BFS 문제다.

그러나 미로 탐색과 코드 자체는 동일하다.

static int N, M, x, y;
static int[][] field;
static boolean[][] visited;
static int[] dx = {0,0,-1,1};
static int[] dy = {1,-1,0,0};

시작점이 주어져 있지 않기 때문에, 배열의 크기와 시작점을 선언해놓고, 나머지 field와 visited, 그리고 가로 세로 이동을 고려한 델타 배열을 선언한다.

public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        field = new int[N][M];
        visited = new boolean[N][M];
        for (int i=0; i<N; i++){
            st = new StringTokenizer(br.readLine());
            for (int j=0; j<M; j++){
                int num = Integer.parseInt(st.nextToken());
                field[i][j] = num;
                if (num == 2){
                    x = i;
                    y = j;
                }
            }
        }
        field[x][y] = 0;
        BFS(x, y);
        for (int i=0; i<N; i++){
            for (int j=0; j<M; j++){
                if (!visited[i][j] && field[i][j] != 0){
                    field[i][j] = -1;
                }
                sb.append(field[i][j]).append(" ");
            }
            sb.append("\n");
        }
        System.out.println(sb.toString());
    }

메인스택 자체는 어렵지 않다.

다만 필드를 고려할 때 시작점이 어디에 주어질 지 모르므로 필드값을 계산하여 스타팅 포인트가 2일경우 그때의 좌표값을 저장해야 한다.

그리고 field[x][y]가 2로 시작하면, 이를 읽어들이면서 BFS 필드에 저장을 하기 때문에 모든 거리 수가 +2로 계산된다.

물론 다른 2차배열을 선언해주어도 되지만, 메모리 절약을 위해 시작값을 0으로 잡아준다.(field[x][y] = 0)

 

그리고 BFS를 돌리는데, 여기서 바로 필드값을 출력하면 안 된다.

문제의 조건에 원래 갈 수 있는 땅인 부분 중에서 도달할 수 없는 위치는 -1을 출력한다. 가 있기 때문이다.

개인적으로 그냥 기계적으로 문제를 풀다가 저 문장을 보지 못하고 계속 '왜 틀렸지?'를 생각했는데

조건을 잘 봐야 한다. 그래서 다음과 같은 조건을 생각했다.

'원래 갈 수 있는데 가지 못한 부분 = 방문하지는 못했지만 필드값이 0이 아닌 부분'

으로 해석하였고, 이 조건을 넣어주고 출력하였다.

static void BFS(int x, int y){
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{x, y});
        visited[x][y] = true;

        while (!queue.isEmpty()){
            int[] now = queue.poll();
            for (int i=0; i<4; i++){
                int nx = now[0] + dx[i];
                int ny = now[1] + dy[i];
                if (nx >= 0 && ny >= 0 && nx < N && ny < M){
                    if (!visited[nx][ny] && field[nx][ny] > 0){
                        visited[nx][ny] = true;
                        queue.offer(new int[]{nx, ny});
                        field[nx][ny] = field[now[0]][now[1]] + 1;
                    }
                }
            }
        }

    }

BFS 함수 자체가 어려운 것은 아니다. 아까 링크했던 미로탐색 문제와 완전히 동일하다.

큐를 선언하는것부터 시작하는데, 큐에 집어넣을 노드는 int형 배열이 되어야 한다.

방문 배열에 추가해주고, 큐가 빌 때까지 계속 뽑아낸다.

델타 배열을 방문하면서 방문하지 않았고 배열값이 0이 아닌 노드에 한해서

방문 배열을 체크해주고, 지나온 값에 이전까지의 뎁스를 기록한다.

마지막으로 새로운 좌표를 다시 큐에 추가해주고 계속 while문을 돌리면 된다.

 

BFS의 원리 자체만 이해했다면 크게 어렵지는 않은 문제다.

'알고리즘 > 백준' 카테고리의 다른 글

[백준/7576] - 토마토(JAVA)  (0) 2023.09.19
[백준/1697] - 숨바꼭질(JAVA)  (0) 2023.09.18
[백준/2178] - 미로 탐색(JAVA)  (0) 2023.09.17
[백준/1260] - DFS와 BFS(JAVA)  (0) 2023.09.17
[백준/1526] - 내리막길(JAVA)  (0) 2023.09.15

N×M크기의 배열로 표현되는 미로가 있다.

미로에서 1은 이동할 수 있는 칸을 나타내고, 0은 이동할 수 없는 칸을 나타낸다. 이러한 미로가 주어졌을 때, (1, 1)에서 출발하여 (N, M)의 위치로 이동할 때 지나야 하는 최소의 칸 수를 구하는 프로그램을 작성하시오. 한 칸에서 다른 칸으로 이동할 때, 서로 인접한 칸으로만 이동할 수 있다.

위의 예에서는 15칸을 지나야 (N, M)의 위치로 이동할 수 있다. 칸을 셀 때에는 시작 위치와 도착 위치도 포함한다.

입력


첫째 줄에 두 정수 N, M(2 ≤ N, M ≤ 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은 붙어서 입력으로 주어진다.

 

출력


첫째 줄에 지나야 하는 최소의 칸 수를 출력한다. 항상 도착위치로 이동할 수 있는 경우만 입력으로 주어진다.

 

풀이


public class Main {
    static int N, M;
    static int[][] field;
    static boolean[][] visited;
    static int[] dx = {0,0,-1,1};
    static int[] dy = {1,-1,0,0};

    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());
        M = Integer.parseInt(st.nextToken());
        field = new int[N][M];
        visited = new boolean[N][M];
        for (int i=0; i<N; i++){
            String row = br.readLine();
            for (int j=0; j<M; j++){
                field[i][j] = row.charAt(j)-48;
            }
        }
        BFS(0,0);
        System.out.println(field[N-1][M-1]);
    }
    static void BFS(int x, int y){
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{x, y});
        visited[x][y] = true;
        while (!queue.isEmpty()){
            int[] now = queue.poll();
            for (int i=0; i<4; i++){
                int nx = now[0] + dx[i];
                int ny = now[1] + dy[i];
                if (nx >=0 && ny >= 0 && nx < N && ny < M){
                    if (!visited[nx][ny] && field[nx][ny] != 0){
                        visited[nx][ny] = true;
                        field[nx][ny] = field[now[0]][now[1]] + 1;
                        queue.offer(new int[]{nx, ny});
                    }
                }
            }
        }
    }
}

해설


전형적인 BFS문제다. 완전탐색하며 몇번째 뎁스에서 원하는 값을 찾을 수 있는지를 확인하는 문제다.

최단거리를 구하는 것이 왜 BFS가 유리한지, BFS가 왜 '너비 우선 탐색'인지를 알 수 있는 좋은 문제다.

DFS는 일단 가면서 탐색하여 시간초과를 일으키지만 BFS는 해당 깊이에서 갈 수 있는 노드를 탐색하고 가기 때문에 최단거리 문제 풀이에 조금 더 용이하다.

먼저 DFS의 진행 상황을 분석하고 풀이로 넘어간다.

4 6
110110
110110
111111
111101

 

위의 예제를 depth를 따라가보자.

모든 '뎁스를 탐색하는 과정'에서 탐색했으면 좌표 안에 최단거리를 집어넣어줘야 한다.

 

depth1

(0,0)을 확인하고 방문 배열을 체크한 다음, 상하좌우를 확인하면서 막혀있지 않고, 숫자가 1이라면 그 좌표를 새로 큐에 추가한다. 새로 추가한 좌표는 방문 배열에 미리 체크해놓는다.

현재 큐 = [[1,0], [0,1]]

depth 2

  • (1,0)을 확인하고, 상하좌우를 확인하고 조건에 맞는 (1,1)좌표를 큐에 추가하고 방문 배열에 (1,1)을 체크한다.
  • (0,1)을 확인하고, 상하좌우를 확인하고 조건에 맞는 (0,2)좌표를 큐에 추가한다. (1,1)은 이미 방문 배열에 있으므로 추가하지 않는다.

현재 큐 = [[1,1], [2,1]]

depth 3

  • (1,1)을 확인하고, 상하좌우를 확인한 뒤 조건에 맞는 (2,1)좌표를 큐에 추가하고 방문 배열을 체크한다.
  • (0,2)을 확인하고, 상화좌우를 확인한 뒤 조건에 맞는 (0,3)좌표를 큐에 추가하고 방문 배열을 체크한다.

현재 큐 = [[2,1], [0,3]]

 

이런식으로 depth를 하나씩 늘려서 진행한다.

결국 마지막에 배열은 다음과 같이 변한다.

1 2 0 8 9 0
2 3 0 7 8 0
3 4 5 6 7 8
4 5 6 7 0 9

각 배열의 값은, 이 배열까지 오기 위한 depth의 최소값을 띄게 된다.

 

이제, 코드를 살펴보자.

    static int N, M;
    static int[][] field;
    static boolean[][] visited;
    static int[] dx = {0,0,-1,1};
    static int[] dy = {1,-1,0,0};

특이사항 없이 가로, 세로를 받을 N, M과 배열을 받고 뎁스를 기록할 field, 방문 기록용 visited 배열과 델타 배열을 선언한다.

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());
        M = Integer.parseInt(st.nextToken());
        field = new int[N][M];
        visited = new boolean[N][M];
        for (int i=0; i<N; i++){
            String row = br.readLine();
            for (int j=0; j<M; j++){
                field[i][j] = row.charAt(j)-48;
            }
        }
        BFS(0,0);
        System.out.println(field[N-1][M-1]);
    }

field배열을 채우고, (0,0)에서 BFS를 시작하고 맨 마지막에 도착지의 depth를 출력하면 된다.

static void BFS(int x, int y){
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{x, y});
        visited[x][y] = true;
        while (!queue.isEmpty()){
            int[] now = queue.poll();
            for (int i=0; i<4; i++){
                int nx = now[0] + dx[i];
                int ny = now[1] + dy[i];
                if (nx >=0 && ny >= 0 && nx < N && ny < M){
                    if (!visited[nx][ny] && field[nx][ny] != 0){
                        visited[nx][ny] = true;
                        field[nx][ny] = field[now[0]][now[1]] + 1;
                        queue.offer(new int[]{nx, ny});
                    }
                }
            }
        }
    }

가장 중요한 BFS 함수다

큐를 선언하는것부터 시작하는데, 큐에 집어넣을 노드는 int형 배열이 되어야 한다.

방문 배열에 추가해주고, 큐가 빌 때까지 계속 뽑아낸다.

델타 배열을 방문하면서 방문하지 않았고 배열값이 0이 아닌 노드에 한해서

방문 배열을 체크해주고, 지나온 값에 이전까지의 뎁스를 기록한다.

마지막으로 새로운 좌표를 다시 큐에 추가해주고 계속 while문을 돌리면 된다.

 

어렵지는 않았으나, 처음 BFS 문제를 풀 때에는 조금 어려운 감이 있다.

그러나 BFS의 원리를 알고 원리대로 프로그래밍을 풀어간다면 코드 자체는 어렵지 않다.

'알고리즘 > 백준' 카테고리의 다른 글

[백준/1697] - 숨바꼭질(JAVA)  (0) 2023.09.18
[백준/14940] - 쉬운 최단거리(JAVA)  (0) 2023.09.17
[백준/1260] - DFS와 BFS(JAVA)  (0) 2023.09.17
[백준/1526] - 내리막길(JAVA)  (0) 2023.09.15
[백준/1987] - 알파벳(JAVA)  (0) 2023.09.15

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

입력


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

출력


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

 

 

풀이


public class bojtest {
    static int N, M, V;

    static ArrayList<Integer>[] arr;

    static boolean[] visited;

    static StringBuilder sb = new StringBuilder();


    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());
        M = Integer.parseInt(st.nextToken());
        V = Integer.parseInt(st.nextToken());
        arr = new ArrayList[N+1];
        visited = new boolean[N+1];
        for (int i=1; i<=N; i++){
           arr[i] = new ArrayList<>();
        }
        for (int i=0; i<M; i++){
            st = new StringTokenizer(br.readLine());
            int s = Integer.parseInt(st.nextToken());
            int e = Integer.parseInt(st.nextToken());
            arr[s].add(e);
            arr[e].add(s);
        }
        for (int i=1; i<=N; i++){
            Collections.sort(arr[i]);
        }
        DFS(V);
        sb.append("\n");
        visited = new boolean[N+1];
        BFS(V);
        System.out.println(sb.toString());
    }
    static void DFS(int x){
        sb.append(x).append(" ");
        visited[x] = true;
        for (int i : arr[x]){
            if (!visited[i]){
                DFS(i);
            }
        }
    }

    static void BFS(int x){
        Queue<Integer> queue = new LinkedList<>();
        queue.add(x);
        visited[x] = true;

        while (!queue.isEmpty()){
            int i = queue.poll();
            sb.append(i).append(" ");
            for (int n : arr[i]){
                if (!visited[n]){
                    visited[n] = true;
                    queue.add(n);
                }
            }
        }
    }
}

해설


BFS와 DFS를 비교하는 문제였다.

단순히 DFS와 BFS를 돌리는 문제보다도, DFS와 BFS가 어떻게 실행되는지를 이해하고 언제 사용할 것인지를 파악하는데 도움이 되는 문제이다.

하나하나 천천히 코드를 뜯어보자.

static int N, M, V;
static ArrayList<Integer>[] arr;
static boolean[] visited;
static StringBuilder sb = new StringBuilder();

처음 선언해줄 변수들은 DFS에서 했던 그대로 가로, 세로, 그리고 target number와

노드의 연결고리를 기록할 ArrayList 배열, 그리고 방문 배열이 필요하다.

 

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());
        M = Integer.parseInt(st.nextToken());
        V = Integer.parseInt(st.nextToken());
        arr = new ArrayList[N+1];
        visited = new boolean[N+1];
        for (int i=1; i<=N; i++){
           arr[i] = new ArrayList<>();
        }
        for (int i=0; i<M; i++){
            st = new StringTokenizer(br.readLine());
            int s = Integer.parseInt(st.nextToken());
            int e = Integer.parseInt(st.nextToken());
            arr[s].add(e);
            arr[e].add(s);
        }
        for (int i=1; i<=N; i++){
            Collections.sort(arr[i]);
        }
        DFS(V);
        sb.append("\n");
        visited = new boolean[N+1];
        BFS(V);
        System.out.println(sb.toString());
    }

메인 스택은 3부분으로 나눌 수 있다.

1.DFS/BFS 실행을 위해 arr과 방문 배열을 선언하고 초기화하는 것, 시작과 끝 노드를 이어주는 것

2.문제에 적힌 대로 '정점 번호가 낮은 것을 먼저 선행'하기 위해 소팅을 하는것

3.DFS를 실행하고, 방문 배열을 초기화시킨 뒤 BFS를 실행하고 출력하는 것

static void DFS(int x){
        sb.append(x).append(" ");
        visited[x] = true;
        for (int i : arr[x]){
            if (!visited[i]){
                DFS(i);
            }
        }
    }

    static void BFS(int x){
        Queue<Integer> queue = new LinkedList<>();
        queue.add(x);
        visited[x] = true;

        while (!queue.isEmpty()){
            int i = queue.poll();
            sb.append(i).append(" ");
            for (int n : arr[i]){
                if (!visited[n]){
                    visited[n] = true;
                    queue.add(n);
                }
            }
        }
    }

DFS와 BFS 실행이다.

DFS는 실행 번호를 적어준 뒤 방문하지 않았던 노드를 토대로 방문하는 로직이다.

BFS는 Queue를 선언하고, 큐에 노드를 넣어주는 것부터 시작된다.

다음 예를 보자.

4 5 1
1 2
1 3
1 4
2 4
3 4

<DFS 실행순서>

DFS(1)을 실행한다. arr[1]에는 2, 3이 들어있으므로 1번 방문을 체크하고 DFS(2)를 실행한다.

DFS(2)을 실행한다. arr[2]에는 4가 들어있으므로 2번 방문을 체크하고 DFS(4)를 실행한다.

DFS(4)를 실행한다. arr[4]에는 아무것도 들어있지 않으므로 4번 방문을 체크하고 스택을 종료한다.

다시 1번으로 돌아가서, 스택에 쌓여 있는 DFS(3)을 실행한다.

DFS(3)을 실행한다. arr[3]에는 4가 들어있지만 방문하였으므로 그냥 종료한다.

<BFS 실행순서>

타겟 넘버가 1일경우 1을 큐에 넣는다.

현재 큐 상태 : [1]

그리고 큐에서 다시 1을 뽑아서, 1과 연결된 노드들을 다시 큐에 넣는다. 1번 방문은 체크한다.

위의 예제에서라면 2와 3을 큐에 넣을 것이다.

큐는 FIFO 구조로 돌아가니까, 2와 3을 큐에 넣을 것이다. 2와 3번 방문은 체크한다.

현재 큐 상태 : [2 3]

빈 큐가 아니므로, poll을 하면 2번 노드가 빠진다. 2번 배열에 있는 4번 노드가 queue에 추가된다. 4번 방문은 체크한다.

현재 큐 상태 : [3 4]

빈 큐가 아니므로, poll을 하면 3번 노드가 빠진다. 3번 배열에 있는 4번 노드가 queue에 추가되어야 하나, 방문 배열에 체크되어있기 때문에 넘어간다.

현재 큐 상태 : [4]

빈 큐가 아니므로, poll을 하면 4번 노드가 빠진다. 4번 배열은 연결되어있는 노드가 없으므로 BFS를 끝낸다.

 

 

위의 예제를 기준으로, 실행순서가 DFS는 1 2 4 3이 나오고 BFS는 1 2 3 4가 나온다.

스택을 기준으로 실행하는 DFS이냐, 큐를 기준으로 실행하는 BFS이냐는 문제를 점점 풀어가며 감을 익혀가야겠지만,

한가지는 알 수 있다. BFS는 최단거리를 찾는데 유용하고, DFS는 브루트 포스같이 모든 경로를 찾는 데에 유용하다.

 

 

너비 우선 탐색

너비 우선 탐색(BFS, breadth-first search)도 그래프를 완전 탐색하는 방법 중 하나로, 시작 노드에서 출발해 시작 노드를 기준으로 가까운 노드를 먼저 방문하면서 탐색하는 알고리즘입니다.

기능 특징 시간복잡도(노드 수 : V, 엣지 수 : E)
그래프 완전 탐색 - FIFO 탐색 - Queue 자료구조 이용 O(V+E)

너비 우선 탐색은 선입선출 방식으로 탐색하므로 큐를 이용해 구현합니다. 또한 너비 우선 탐색은 탐색 시작 노드와 가까운 노드를 우선하여 탐색하므로 목표 노드에 도착하는 경로가 여러개일 때 최단 경로를 보장합니다.

너비 우선 탐색의 핵심 이론

1. BFS를 시작할 노드를 정한 후 사용할 자료구조 초기화하기
BFS도 DFS와 마찬가지로 방문했던 노드는 다시 방문하지 않으므로 방문한 노드를 체크하기 위한 배열이 필요합니다. 그래프를 인접 리스트로 표현하는 것 역시 DFS와 동일합니다. 하지만 차이점이 있다면 탐색을 위해 스택이 아닌 큐를 사용합니다.

  • 타임라인 : 원본 그래프 -> 인접 리스트 -> 방문 배열 -> 큐 삽입

2. 큐에서 노드를 꺼낸 후 꺼낸 노드의 인접 노드를 다시 큐에 삽입하기
큐에서 노드를 꺼내면서 인접 노드를 큐에 삽입합니다. 이 때 방문 배열을 체크하여 이미 방문한 노드는 큐에 삽입하지 않습니다. 또한 큐에서 꺼낸 노드는 탐색 순서에 기록합니다.

  • 타임라인 : 큐에서 노드를 꺼내면서 탐색 순서에 꺼낸 노드를 기록 -> 인접 리스트 -> ㄷ상 노드의 인접 노드를 큐에 삽입 -> 노드를 삽입하며 방문 배열 체크

3. 큐 자료구조에 값이 없을때까지 반복하기
큐에 노드가 없을 때까지 앞선 과정을 반복합니다. 선입선출 방식으로 탐색하므로 탐색 순서가 DFS와는 다릅니다.

풀어볼 문제

문제 026 - BFS와 DFS 프로그램(1260)

문제 027 - 미로 탐색하기(2178)

문제 028 - 트리의 지름 구하기(1167)

'알고리즘' 카테고리의 다른 글

우선순위 큐를 이용한 그리디 문제  (0) 2023.10.15
이진 탐색, 이분 탐색  (0) 2023.10.04
구간합 & 투 포인터 & 슬라이딩 윈도우  (0) 2023.09.27
탐색(1) - DFS  (0) 2023.09.11
DP  (0) 2023.09.07

+ Recent posts