송도에 사는 상근이와 친구들은 송도에서 열리는 펜타포트 락 페스티벌에 가려고 한다. 올해는 맥주를 마시면서 걸어가기로 했다. 출발은 상근이네 집에서 하고, 맥주 한 박스를 들고 출발한다. 맥주 한 박스에는 맥주가 20개 들어있다. 목이 마르면 안되기 때문에 50미터에 한 병씩 마시려고 한다. 즉, 50미터를 가려면 그 직전에 맥주 한 병을 마셔야 한다.

상근이의 집에서 페스티벌이 열리는 곳은 매우 먼 거리이다. 따라서, 맥주를 더 구매해야 할 수도 있다. 미리 인터넷으로 조사를 해보니 다행히도 맥주를 파는 편의점이 있다. 편의점에 들렸을 때, 빈 병은 버리고 새 맥주 병을 살 수 있다. 하지만, 박스에 들어있는 맥주는 20병을 넘을 수 없다. 편의점을 나선 직후에도 50미터를 가기 전에 맥주 한 병을 마셔야 한다.

편의점, 상근이네 집, 펜타포트 락 페스티벌의 좌표가 주어진다. 상근이와 친구들이 행복하게 페스티벌에 도착할 수 있는지 구하는 프로그램을 작성하시오.

입력


첫째 줄에 테스트 케이스의 개수 t가 주어진다. (t ≤ 50)

각 테스트 케이스의 첫째 줄에는 맥주를 파는 편의점의 개수 n이 주어진다. (0 ≤ n ≤ 100).

다음 n+2개 줄에는 상근이네 집, 편의점, 펜타포트 락 페스티벌 좌표가 주어진다. 각 좌표는 두 정수 x와 y로 이루어져 있다. (두 값 모두 미터, -32768 ≤ x, y ≤ 32767)

송도는 직사각형 모양으로 생긴 도시이다. 두 좌표 사이의 거리는 x 좌표의 차이 + y 좌표의 차이 이다. (맨해튼 거리)

출력


각 테스트 케이스에 대해서 상근이와 친구들이 행복하게 페스티벌에 갈 수 있으면 "happy", 중간에 맥주가 바닥나서 더 이동할 수 없으면 "sad"를 출력한다. 

풀이


public class Main {
	static int N;
	static final int distance = 1000;
	static Queue<Integer> queue;
	static int[][] field;
	static boolean[] visited;
	static StringTokenizer st;
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringBuilder sb = new StringBuilder();
		int T = Integer.parseInt(br.readLine());
		while (T-- > 0){
			queue = new LinkedList<>();
			N = Integer.parseInt(br.readLine());
			visited = new boolean[N+2];
			field = new int[N+2][2];
			//초기 설정
			for (int i=0; i<N+2; i++){
				st = new StringTokenizer(br.readLine());
				field[i][0] = Integer.parseInt(st.nextToken());
				field[i][1] = Integer.parseInt(st.nextToken());
				if (getDistance(field[i][0], field[i][1], field[0][0], field[0][1]) <= distance){
					queue.offer(i);
					visited[i] = true;
				}
			}
			BFS();
			if (visited[N+1]) {
				sb.append("happy").append("\n");
			} else {
				sb.append("sad").append("\n");
			}
		}
		System.out.println(sb.toString());
	}
	static int getDistance(int x1, int y1, int x2, int y2){
		return Math.abs(x2-x1) + Math.abs(y2-y1);
	}

	static void BFS(){
		while (!queue.isEmpty()){
			int now = queue.poll();
			for (int i=1; i<N+2; i++){
				if (!visited[i] && getDistance(field[i][0], field[i][1], field[now][0], field[now][1]) <=distance){
					queue.offer(i);
					visited[i] = true;
				}
			}
		}
	}
}

해설


기존 BFS문제를 풀 때 늘 좌표에 Queue를 이용해서 풀다보니 이 문제도 자연스레 그렇게 문제를 들어가다가

OOME에 막혀서 다른 풀이를 찾아보다가 그래프를 이용한 풀이를 보고 깜짝 놀란 문제였다.

물론 배열로 풀어도 상관없다.

다른 블로그에서 list를 이용한 풀이를 많이 설명했으므로, 배열을 이용한 풀이로 설명한다.

 

리스트를 이용한 풀이든, 배열을 이용한 풀이든 핵심은 다음과 같다.

  1. 맥주를 마시기 전에 집에서 갈 수 있는 편의점은 큐에 넣는다.
  2. 편의점을 저장할 수 있는 배열을 만든다.

상세한 코드 설명은 다음과 같다.

먼저 메인 함수를 보자.

public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringBuilder sb = new StringBuilder();
		int T = Integer.parseInt(br.readLine());
		while (T-- > 0){
			queue = new LinkedList<>();
			N = Integer.parseInt(br.readLine());
			visited = new boolean[N+2];
			field = new int[N+2][2];
			//초기 설정
			for (int i=0; i<N+2; i++){
				st = new StringTokenizer(br.readLine());
				field[i][0] = Integer.parseInt(st.nextToken());
				field[i][1] = Integer.parseInt(st.nextToken());
				if (getDistance(field[i][0], field[i][1], field[0][0], field[0][1]) <= distance){
					queue.offer(i);
					visited[i] = true;
				}
			}
			BFS();
			if (visited[N+1]) {
				sb.append("happy").append("\n");
			} else {
				sb.append("sad").append("\n");
			}
		}
		System.out.println(sb.toString());
	}

테스트 케이스 T를 받아서 T가 줄어들때까지 케이스를 돌린다.

가장 중요한건 [N+2][2]의 정수형 이차 배열 구조를 가진 field와, [N+2]의 불린형 일차원 배열인 visited를 선언해주는 것이다.

물론 x,y좌표를 담는 point class와 ArrayList<Point>를 만들어서 나중에 get으로 뽑아도 상관없다.

그리고 시작점과 비교해서 맨해튼 거리가 1000 이하인 편의점에 대해 큐에 넣고, visited로 처리해준다.

마지막에 BFS 함수를 돌리고, visited[N+1]로 펜타포트에 갈 수 있는지 / 없는지를 조사하고 append한다.

static int getDistance(int x1, int y1, int x2, int y2){
    return Math.abs(x2-x1) + Math.abs(y2-y1);
}

static void BFS(){
    while (!queue.isEmpty()){
        int now = queue.poll();
        for (int i=1; i<N+2; i++){
            if (!visited[i] && getDistance(field[i][0], field[i][1], field[now][0], field[now][1]) <=distance){
                queue.offer(i);
                visited[i] = true;
            }
        }
    }
}

getDistance는 맨해튼 거리를 구하는 함수, 그리고 BFS는 단순하게 큐에 있는 편의점을 뽑아서 다음 편의점과 거리가 1000 미만이면 새로 큐에다가 추가해주면 된다.

이진 탐색

이진탐색(binary search)은 데이터가 정렬돼어 있는 상태에서 원하는 값을 찾아내는 알고리즘입니다. 대상 데이터의 중앙값과 찾고자 하는 값을 비교해 데이터의 크기를 절반씩 줄이면서 대상을 찾습니다.

기능 특징 시간복잡도
타깃 데이터 탐색 - 중앙값 비교를 통한 대상 축소 방식 O(logN)

이진 탐색의 핵심 이론
이진 탐색은 오름차순으로 정렬된 데이터에서 다음 4가지 과정을 반복합니다. 내림차순이라면 조건을 반대로 하면 됩니다.

이진 탐색 과정

  1. 현재 데이터셋의 중앙값을 선택한다.
  2. 중앙값 > 타깃 데이터일때 중앙값 기준으로 왼쪽 데이터셋을 선택한다.
  3. 중앙값 < 타깃 데이터일때 중앙값 기준으로 오른쪽 데이터셋을 선택한다.
  4. 과정 1~3을 반복하다가 중앙값 == 타깃 데이터일때 탐색을 종료한다.

예를 들어 16개의 데이터가 있는 데이터셋에서 값이 55인 데이터를 찾아보자.
[target data = 55, data size(N) = 16]
                                           ⇓ 중앙값(median) : 타겟보다 작으므로 오른쪽 데이터셋 선택
| 3 | 7 | 13 | 15 | 23 | 35 | 38 | 41 | 46 | 49 | 55 | 67 | 68 | 72 | 77 | 86 |
                                                                           ⇓ 중앙값과 비교해서 타겟보다 크므로 왼쪽 데이터셋
| 3 | 7 | 13 | 15 | 23 | 35 | 38 | 41 | 46 | 49 | 55 | 67 | 68 | 72 | 77 | 86 |
                                                        ⇓ 중앙값과 비교해서 타겟보다 작으므로 오른쪽 데이터셋 선택
| 3 | 7 | 13 | 15 | 23 | 35 | 38 | 41 | 46 | 49 | 55 | 67 | 68 | 72 | 77 | 86 |
                                                              ⇓ 중앙값 == 타겟 : 탐색 종료
| 3 | 7 | 13 | 15 | 23 | 35 | 38 | 41 | 46 | 49 | 55 | 67 | 68 | 72 | 77 | 86 |

이렇게 이진 탐색을 사용해서 N개의 데이터에서 log(N)의 연산으로 원하는 데이터의 위치를 찾을 수 있습니다.
가장 중요한 것은, 이진 탐색은 데이터가 정렬되어 있어야 한다는 것입니다.

풀어볼 문제

문제 029 - 원하는 정수 찾기(1920)

문제 030 - 기타 레슨(2343)

문제 031 - 배열에서 K번째 수 찾기(1300)

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

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

2차원 좌표 평면 위에 있는 점 3개 P1, P2, P3가 주어진다. P1, P2, P3를 순서대로 이은 선분이 어떤 방향을 이루고 있는지 구하는 프로그램을 작성하시오.

입력


첫째 줄에 P1의 (x1, y1), 둘째 줄에 P2의 (x2, y2), 셋째 줄에 P3의 (x3, y3)가 주어진다. (-10,000 ≤ x1, y1, x2, y2, x3, y3 ≤ 10,000) 모든 좌표는 정수이다. P1, P2, P3의 좌표는 서로 다르다.

출력


P1, P2, P3를 순서대로 이은 선분이 반시계 방향을 나타내면 1, 시계 방향이면 -1, 일직선이면 0을 출력한다.

풀이


public class Main {
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		int x1 = Integer.parseInt(st.nextToken());
		int y1 = Integer.parseInt(st.nextToken());
		st = new StringTokenizer(br.readLine());
		int x2 = Integer.parseInt(st.nextToken());
		int y2 = Integer.parseInt(st.nextToken());
		st = new StringTokenizer(br.readLine());
		int x3 = Integer.parseInt(st.nextToken());
		int y3 = Integer.parseInt(st.nextToken());
		System.out.println(ccw(x1, y1, x2, y2, x3, y3));

	}
	static int ccw(int x1, int y1, int x2, int y2, int x3, int y3) {
		int temp = x1*y2+x2*y3+x3*y1 - y1*x2-y2*x3-y3*x1;
		if (temp > 0) {
			return 1;
		} else if (temp < 0) {
			return -1;
		} else {
			return 0;
		}
	}
}

해설


CCW = Counter,Counter-ClockWise의 줄임말로, 시계방향과 시계 반대방향을 찾아내는 문제이다.

https://www.acmicpc.net/blog/view/27

 

점 3개의 방향성을 나타내는 CCW

세 점 P1(x1, y1), P2(x2, y2), P3(x3, y3)가 있을 떄, 점 3개를 이은 선분은 어떤 방향성을 나타내게 될까요? 11758번 문제: CCW 가능한 경우의 수는 총 3가지가 있습니다. 반시계 방향, 시계 방향, 일직선. 시

www.acmicpc.net

를 읽어보자.

결론은 두 선분의 외적으로 방향을 구하는 것이다.

두 벡터를 외적하면 두 벡터에 수직인 벡터를 구할 수 있는데,

이 벡터가 위로 가는지, 아래로 가는지의 부호를 검사하는 것이다.

 

삼각형이나 넓이를 구하는 기본 알고리즘중에 신발끈 알고리즘이 있다.

기하쪽 문제를 풀 때에는 여지없이 등장하는 알고리즘이다.

삼각 이상의 다각형에서도 적용되는데, 다각형을 쪼개 삼각형으로 분할해 답을 도출해내는 방식이다.

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

[백준/2512] - 예산(JAVA)  (0) 2023.10.08
[백준/2143] - 두 배열의 합(JAVA)  (0) 2023.09.27
[백준/2573] - 빙산(JAVA)  (0) 2023.09.20
[백준/7569] - 토마토(JAVA)  (0) 2023.09.19
[백준/14502] - 연구소(JAVA)  (0) 2023.09.19

한 배열 A[1], A[2], …, A[n]에 대해서, 부 배열은 A[i], A[i+1], …, A[j-1], A[j] (단, 1 ≤ i ≤ j ≤ n)을 말한다. 이러한 부 배열의 합은 A[i]+…+A[j]를 의미한다. 각 원소가 정수인 두 배열 A[1], …, A[n]과 B[1], …, B[m]이 주어졌을 때, A의 부 배열의 합에 B의 부 배열의 합을 더해서 T가 되는 모든 부 배열 쌍의 개수를 구하는 프로그램을 작성하시오.

예를 들어 A = {1, 3, 1, 2}, B = {1, 3, 2}, T=5인 경우, 부 배열 쌍의 개수는 다음의 7가지 경우가 있다.

T(=5) = A[1] + B[1] + B[2]
      = A[1] + A[2] + B[1]
      = A[2] + B[3]
      = A[2] + A[3] + B[1]
      = A[3] + B[1] + B[2]
      = A[3] + A[4] + B[3]
      = A[4] + B[2] 

입력


첫째 줄에 T(-1,000,000,000 ≤ T ≤ 1,000,000,000)가 주어진다. 다음 줄에는 n(1 ≤ n ≤ 1,000)이 주어지고, 그 다음 줄에 n개의 정수로 A[1], …, A[n]이 주어진다. 다음 줄에는 m(1 ≤ m ≤ 1,000)이 주어지고, 그 다음 줄에 m개의 정수로 B[1], …, B[m]이 주어진다. 각각의 배열 원소는 절댓값이 1,000,000을 넘지 않는 정수이다.

출력


첫째 줄에 답을 출력한다. 가능한 경우가 한 가지도 없을 경우에는 0을 출력한다.

풀이


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;

public class Main {
	static StringTokenizer st;
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		int T = Integer.parseInt(br.readLine());
		int n = Integer.parseInt(br.readLine());
		int[] A = new int[n];
		st = new StringTokenizer(br.readLine());
		for (int i=0; i<n; i++){
			A[i] = Integer.parseInt(st.nextToken());
		}
		int m = Integer.parseInt(br.readLine());
		int[] B = new int[m];
		st = new StringTokenizer(br.readLine());
		for (int i=0; i<m; i++){
			B[i] = Integer.parseInt(st.nextToken());
		}
		Map<Long, Long> mapA = new HashMap<>();
		Map<Long, Long> mapB = new HashMap<>();
		for (int i=0; i<n; i++){
			long sum = 0;
			for (int j=i; j<n; j++){
				sum += A[j];
				long count = mapA.getOrDefault(sum, 0L);
				mapA.put(sum, count+1);
			}
		}
		for (int i=0; i<m; i++){
			long sum = 0;
			for (int j=i; j<m; j++){
				sum += B[j];
				long count = mapB.getOrDefault(sum, 0L);
				mapB.put(sum, count+1);
			}
		}
		long answer = 0;
		for (Map.Entry<Long, Long> entryA :mapA.entrySet()){
			Long aSum = entryA.getKey();
			Long aCount = entryA.getValue();
			long diff = T - aSum;
			Long bCount = mapB.getOrDefault(diff, 0L);
			answer += (aCount * bCount);
		}
		System.out.println(answer);
	}
}

해설


누적합 + 부배열을 다룰 수 있는지를 물어보는 문제였다.

슬라이딩 윈도우로도 풀 수 있는 문제지만, 어짜피 개수 * 개수를 리턴해야 하는 문제라고 생각해서

Map에 누적합이 몇개 있는지 count를 넣어주었다.

예를 들어 A = {1,3,1,2}, B = {1,3,2}라면

mapA에는 7가지 경우의 수(1,2,3,4,5,6,7)

mapB에는 6가지 경우의 수(1,2,3,4,5,6)가 들어갈 것이다.

 

그중에서 연속되어 있는 수열을 찾기 위해 중첩포문을 두번 돌려줄 것이다.

그리고 map의 entryset을 뒤져서

mapA = 1이라면, mapB = T-mapA = 4가 되어야 하므로

mapB의 4에 해당하는 entryset을 찾아서

mapA의 count (값이 1이 되는 부분수열의 개수) * mapB의 count(값이 2가 되는 부분수열의 개수)를 해주고 print한다.

 

물론 sliding window나 이분 탐색으로 풀 수도 있다.

그러나 규칙 / 분류에 의거해서 기계적으로 문제를 푸는 일을 굉장히 지양하려고 해서 어떻게 할까 생각하다 보니

map으로 풀어보았고, 시간도 오히려 sliding window에 비해 빨랐다.

 

 

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

[백준/2512] - 예산(JAVA)  (0) 2023.10.08
[백준/11758] - CCW(JAVA)  (0) 2023.09.28
[백준/2573] - 빙산(JAVA)  (0) 2023.09.20
[백준/7569] - 토마토(JAVA)  (0) 2023.09.19
[백준/14502] - 연구소(JAVA)  (0) 2023.09.19

구간 합

구간 합은 합 배열을 이용하여 시간 복잡도를 더 줄이기 위해 사용하는 특수한 목적의 알고리즘입니다.
배열 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

+ Recent posts