구간 합

구간 합은 합 배열을 이용하여 시간 복잡도를 더 줄이기 위해 사용하는 특수한 목적의 알고리즘입니다.
배열 A가 있을 때 합 배열 S는 다음과 같이 정의합니다.
합 배열 S
S[i] = A[0] + A[1] + A[2] + ... + A[i-1] + A[i]
합 배열은 기존의 배열을 전처리한 배열이라고 생각하면 됩니다. 이렇게 합 배열을 미리 구해놓으면 기존 배열의 일정 범위의 합을 구하는 시간 복잡도가 O(n)에서 O(1)로 감소합니다.
책 p 42 인덱스 / 배열 / 합배열 그림 참고
합 배열 S를 만드는 공식
S[i] = S[i-1] + A[i]
구간 합을 구하는 공식
S[j] - S[i-1] : i에서 j까지의 구간 합

 

풀어볼 문제

문제 003 - 구간 합 구하기 (11659)

BufferedReader + StringTokenizer VS Scanner class

입력 데이터를 처리하기 위해 Java로 작업할 때 입력을 읽고 구문 분석할 수 있는 몇 가지 옵션이 있습니다. 두 가지 일반적인 방법은 Scanner.nextInt를 사용하는 것과 StringTokenizer와 결합된 BufferedReader를 사용하는 것입니다. 두 방법 모두 동일한 목적을 수행하지만 성능 및 구현 측면에서 약간의 차이가 있습니다.

  1. '스캐너':
  • 스캐너는 InputStream, File 등과 같은 다양한 소스에서 입력을 읽을 수 있는 메서드를 제공하는 Java의 유틸리티 클래스입니다. 정규식을 사용하여 기본 유형 및 문자열을 구문 분석할 수 있습니다.
  • Scanner.nextInt 사용 시 입력을 직접 정수로 읽어옵니다. 구문 분석 및 유형 변환을 자동으로 처리하기 때문에 간단한 경우에 더 편리할 수 있습니다.
  • 그러나 Scanner는 일반적으로 BufferedReader보다 느립니다. 특히 많은 양의 데이터를 처리할 때 구문 분석에 정규식을 사용하기 때문에 계산 비용이 많이 들 수 있습니다.
  1. BufferedReaderStringTokenizer:
  • BufferedReader는 문자, 배열 및 행을 효율적으로 읽을 수 있도록 문자를 버퍼링하여 문자 입력 스트림에서 텍스트를 읽는 데 사용되는 Java의 클래스입니다.
  • StringTokenizer는 지정된 구분 기호에 따라 문자열을 토큰(하위 문자열)으로 나누는 Java의 유틸리티 클래스입니다.
  • StringTokenizer와 함께 BufferedReader를 사용하려면 입력을 문자열로 읽은 다음 지정된 구분 기호(예: 공백)를 기준으로 문자열을 토큰화하고 Integer.parseInt() 또는 유사한 메서드를 사용하여 토큰을 정수(또는 다른 유형)로 변환해야 합니다. 이 접근 방식을 사용하면 입력 구문 분석 프로세스를 더 잘 제어할 수 있습니다.
  • 이 조합은 특히 많은 양의 데이터로 작업할 때 스캐너를 사용하는 것보다 빠릅니다. 정규 표현식의 오버헤드를 피하고 효율적인 버퍼 읽기가 가능하기 때문입니다.

요약하면 StringTokenizer와 함께 Scanner.nextIntBufferedReader를 사용하는 것의 주요 차이점은 성능과 입력 구문 분석에 대해 제공하는 제어 수준에 있습니다. 스캐너는 단순한 경우에 더 편리하고 사용하기 쉽지만 속도가 느릴 수 있는 반면 StringTokenizer가 포함된 BufferedReader는 특히 큰 입력 데이터를 처리할 때 구문 분석에 대해 더 나은 성능과 더 많은 제어를 제공합니다.

 

풀어볼 문제

문제 004 - 구간 합 구하기2 (11660)

문제 005 - 나머지 합 구하기(10986)

투 포인터

투 포인터는 2개의 포인터로 알고리즘의 시간 복잡도를 최적화한다.

 

풀어볼 문제

문제 006 - 연속된 자연수의 합 구하기(2018)

문제 007 - 주몽의 명령(1940)

문제 008 - 좋은 수 구하기(1253)

슬라이딩 윈도우

슬라이딩 윈도우 알고리즘은 2개의 포인터로 범위를 지정한 다음 범위(window)를 유지한 채로 이동(sliding)하며 문제를 해결합니다. 투 포인트 알고리즘과 매우 비슷하고, 원리도 간단합니다.

 

풀어볼 문제

문제 009 - DNA 비밀번호

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

우선순위 큐를 이용한 그리디 문제  (0) 2023.10.15
이진 탐색, 이분 탐색  (0) 2023.10.04
탐색(2) - BFS  (0) 2023.09.17
탐색(1) - DFS  (0) 2023.09.11
DP  (0) 2023.09.07

https://www.acmicpc.net/problem/3009

문제를 풀다 새로운 것을 이해했다.

public class Main {
    public static void main(String[] args){
        Integer A = 3;
        Integer B = 3;
        System.out.println("A == B ? "+(A==B));
        Integer C = 128;
        Integer D = 128;
        System.out.println("C == D ? "+(C==D));
    }
}

두개의 결과는 모두 참일까? 그렇지 않다.

첫 번째의 비교는 True, 두 번째의 비교는 False가 나온다.

늘 자바에서 문자열이 아닐 때에는 ==비교를 했는데, 이상하게도 결과가 false가 나왔다.

 

왜 그럴까?

자바에서 int는 primitive type이고, Integer는 객체다.

그래서 primitive type끼리는 ==를 해도 되지만, 객체끼리의 비교는 ==을 쓰면 안된다.(String에서 ==를 안 쓰고 equals를 쓰는 이유와 동일하다.)

그런데, 자바의 내부 동작 구조 원리상 Integer는 -128 ~ 127까지만 동일 객체의 캐시를 가져다 쓰기 때문에

그 전과 그 후의 값들은 주소값이 달라 ==비교가 불가능한 것이다.

 

 

 

참고)

https://romcanrom.tistory.com/177

 

java/ Integer를 '==' 연산자가 아닌 'equals' 메서드로 비교해야 하는 이유 (Integer Cache)

Integer 값을 비교할 때 'equals' 메서드를 사용하지 않고 '==' 연산자를 사용해 코드를 작성한 경우, 일부 값은 정확하게 비교가 되는 반면 일부 값에 대해서는 같은 값임에도 false를 반환하는 경우가

romcanrom.tistory.com

 

자바를 이용해 프로그래밍을 하거나 PS를 하게 되면

코드 중에 제목의 3가지를 이용해 반복문을 빠져나가거나 행동을 종료하는 때가 있다.

break를 쓰기도 하고, continue를 쓰기도 하고, return을 쓰기도 한다.

갑자기 문득 이건 왜 쓸까 하고 궁금했는데, chat gpt의 도움을 받아 작성한다.

 

1. break

반복문(for, while, do-while) 및 switch문에서 사용된다.

for (int i=0; i<5; i++){
    if (i==3){
          break;
    }
    System.out.println(i);
}

//=======결과=====
//0
//1
//2
//=================
switch(text){
    case "break":
        break;
    default:
        System.out.println(text);
        break;
}

break를 만나면 반복문의 조건을 만족하건 말건 즉시 루프를 탈출한다.

 

2.continue

continue는 반복문 안에서도 사용된다.

반복문을 완전히 빠져나가지 않고, 다음 루프로 이동한다.

for (int i=0; i<5; i++){
    if (i==3){
          continue;
    }
    System.out.println(i);
}

//=======결과=====
//0
//1
//2
//4
//=================

3.return

'메소드 내부'에서 '메소드'가 반환하는 값을 지정하고 메소드 실행을 종료한다.

return을 만나면 '메소드'가 즉시 종료된다.

 

지구 온난화로 인하여 북극의 빙산이 녹고 있다. 빙산을 그림 1과 같이 2차원 배열에 표시한다고 하자. 빙산의 각 부분별 높이 정보는 배열의 각 칸에 양의 정수로 저장된다. 빙산 이외의 바다에 해당되는 칸에는 0이 저장된다. 그림 1에서 빈칸은 모두 0으로 채워져 있다고 생각한다.

             
  2 4 5 3    
  3   2 5 2  
  7 6 2 4    
             

그림 1. 행의 개수가 5이고 열의 개수가 7인 2차원 배열에 저장된 빙산의 높이 정보

빙산의 높이는 바닷물에 많이 접해있는 부분에서 더 빨리 줄어들기 때문에, 배열에서 빙산의 각 부분에 해당되는 칸에 있는 높이는 일년마다 그 칸에 동서남북 네 방향으로 붙어있는 0이 저장된 칸의 개수만큼 줄어든다. 단, 각 칸에 저장된 높이는 0보다 더 줄어들지 않는다. 바닷물은 호수처럼 빙산에 둘러싸여 있을 수도 있다. 따라서 그림 1의 빙산은 일년후에 그림 2와 같이 변형된다.

그림 3은 그림 1의 빙산이 2년 후에 변한 모습을 보여준다. 2차원 배열에서 동서남북 방향으로 붙어있는 칸들은 서로 연결되어 있다고 말한다. 따라서 그림 2의 빙산은 한 덩어리이지만, 그림 3의 빙산은 세 덩어리로 분리되어 있다.

             
    2 4 1    
  1   1 5    
  5 4 1 2    
             

그림 2

             
      3      
        4    
  3 2        
             

그림 3

한 덩어리의 빙산이 주어질 때, 이 빙산이 두 덩어리 이상으로 분리되는 최초의 시간(년)을 구하는 프로그램을 작성하시오. 그림 1의 빙산에 대해서는 2가 답이다. 만일 전부 다 녹을 때까지 두 덩어리 이상으로 분리되지 않으면 프로그램은 0을 출력한다.

입력


첫 줄에는 이차원 배열의 행의 개수와 열의 개수를 나타내는 두 정수 N과 M이 한 개의 빈칸을 사이에 두고 주어진다. N과 M은 3 이상 300 이하이다. 그 다음 N개의 줄에는 각 줄마다 배열의 각 행을 나타내는 M개의 정수가 한 개의 빈 칸을 사이에 두고 주어진다. 각 칸에 들어가는 값은 0 이상 10 이하이다. 배열에서 빙산이 차지하는 칸의 개수, 즉, 1 이상의 정수가 들어가는 칸의 개수는 10,000 개 이하이다. 배열의 첫 번째 행과 열, 마지막 행과 열에는 항상 0으로 채워진다.

 

 

출력


첫 줄에 빙산이 분리되는 최초의 시간(년)을 출력한다. 만일 빙산이 다 녹을 때까지 분리되지 않으면 0을 출력한다.

 

 

 

 

풀이


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

	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];
		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());
			}
		}

		int year = 0;

		while (true){
			visited = new boolean[N][M];
			count = 0;
			for (int i=0; i<N; i++){
				for (int j=0; j<M; j++){
					if (!visited[i][j] && field[i][j] > 0){
						DFS(i, j);
						count++;
					}
				}
			}

			if (count == 0){
				year = 0;
				break;
			} else if (count >= 2){
				break;
			}
			BFS();
			year++;
		}

		System.out.println(year);

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

		while (!queue.isEmpty()){
			int[] point = queue.poll();
			int count = 0;
			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 (!visited[nx][ny] && field[nx][ny] == 0){
						count++;
					}
				}
			}
			if (field[point[0]][point[1]] >= count){
				field[point[0]][point[1]] -= count;
			} else {
				field[point[0]][point[1]] = 0;
			}
		}

	}
}

해설


BFS와 DFS, 그리고 분기처리까지 잘 할수 있냐를 물어보는 문제였다.

큰 틀은 다음과 같다.

  1. DFS로 돌면서 빙산 개수 체크하기
  2. BFS로 1년씩 빙산 녹이기
    1. 주의할 점은 방문 배열을 잘 이용해서 지금 녹아 없어지는 빙하가 다른 빙하에 영향을 주면 안됨
  3. 빙산의 개수 체크하기
    1. 빙산이 한 번에 다 녹아버린다면(DFS의 결과가 0이라면) 0을 출력
    2. 이외에는 지난 연도 출력
static int N, M, count;
static int[][] field;
static boolean[][] visited;
static int[] dx = {0,0,-1,1};
static int[] dy = {1,-1,0,0};

사전에 선언해줄 변수들은 가로 세로를 나타내는 N, M, 그리고 빙산의 개수를 카운팅해줄 count라는 변수를 선언한다.

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];
    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());
        }
    }
    int year = 0;
    while (true){
        visited = new boolean[N][M];
        count = 0;
        for (int i=0; i<N; i++){
            for (int j=0; j<M; j++){
                if (!visited[i][j] && field[i][j] > 0){
                    DFS(i, j);
                    count++;
                }
            }
        }
        if (count == 0){
            year = 0;
            break;
        } else if (count >= 2){
            break;
        }
        BFS();
        year++;
    }
    System.out.println(year);
}

메인 함수부터는, 위에 써놓은 틀대로 따라가면 된다.

먼저 field를 채우고, year를 선언해주고 while문 안으로 들어간다.

여기서 중요한게 매 해마다 빙산의 개수가 변할 수 있으므로 visited배열을 꼭 초기화해준다.

물론 DFS함수 내부에서 visited배열을 true/false로 조작해도 되지만, 알아보기 쉽게 밖에서 초기화해준다.

그리고 DFS함수가 몇 번 실행됐는지(빙산이 몇개인지)를 count변수로 체크하고,

count변수에 따라 분기처리를 해서 1이 아니라면 break해서 빠져나간다.

만약 count==1이라면 빙산이 아직 덜 녹은 것이므로

BFS로 한번 더 녹이고, year를 후위덧셈 해준다.

static void DFS(int x, int y){
    visited[x][y] = true;
    for (int i=0; i<4; i++){
        int nx = x + dx[i];
        int ny = y + dy[i];
        if (nx >=0 && ny >= 0 && nx < N && ny < M){
            if (!visited[nx][ny] && field[nx][ny] > 0){
                DFS(nx, ny);
            }
        }
    }
}

DFS함수 같은 경우는 단순한 로직을 띈다. 그냥 방문 배열과 조건에 맞춰서 체크만 해주면 되는 로직을 탄다.

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

    while (!queue.isEmpty()){
        int[] point = queue.poll();
        int count = 0;
        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 (!visited[nx][ny] && field[nx][ny] == 0){
                    count++;
                }
            }
        }
        if (field[point[0]][point[1]] >= count){
            field[point[0]][point[1]] -= count;
        } else {
            field[point[0]][point[1]] = 0;
        }
    }

}

마지막으로 제일 애먹었던 BFS 로직이다.

시작점을 어디서 추가해야 하나 엄청 고민했는데, 결국 배열을 돌면서 빙산이 있는 필드를 큐에 담고 시작하는게 정답이었다. 

while문을 들어올때 처음으로 visited 배열을 초기화하고 들어와서 DFS를 진행했으므로,

지금은 visited에 빙산만 체크되어 있을 것이다.

왜 visited를 사용해야 하는지에 대한 이유는 다음과 같다.

0 0 0 0

0 3 4 0

0 0 0 0

이라는 배열이 있다고 했을 때, 1년이 지난다면 상식적으로

0 0 0 0

0 0 1 0

0 0 0 0

이 되어야 한다.

그러나 방문 배열이 없을 경우, 배열을 돌면서 차례로 큐에 [1,1], [1,2]가 들어가기 때문에,

[1,1]이 먼저 0으로 녹아 없어지면서 [1,2] 주위에는 빙산이 4개가 되어버린다.

결국

0 0 0 0

0 0 0 0

0 0 0 0

이런 형태를 띄게 된다.

 

그리고나서 count(사실은 변수를 다르게 하는게 좋다. 나는 실수로 지역변수로 똑같이 선언해버렸다)를 확인하고, 개수대로 빼주되, 음수가 나올 경우 0으로 바꿔주는 로직만 있으면 된다.

 

풀고 보니 로직 자체가 어려운 문제는 아니였지만, 처음 풀 때는 와 이거 어떻게 해야하지? 라는 생각을 가득 했던것 같다.

하지만 DFS+BFS를 둘 다 이용하는 중요하고 좋은 문제였다.

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

[백준/11758] - CCW(JAVA)  (0) 2023.09.28
[백준/2143] - 두 배열의 합(JAVA)  (0) 2023.09.27
[백준/7569] - 토마토(JAVA)  (0) 2023.09.19
[백준/14502] - 연구소(JAVA)  (0) 2023.09.19
[백준/7576] - 토마토(JAVA)  (0) 2023.09.19

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

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

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

입력


첫 줄에는 상자의 크기를 나타내는 두 정수 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

+ Recent posts