문제 풀이 날짜: 2023.10.13
포스트 작성일: 2023.10.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 2473번: 세 용액 (골드3)

 

키워드

이분 탐색

 

풀이 접근법

  • 주어진 수의 범위가 크므로 범위에 주의하자. 배열이나 변수를 Long으로 선언해야 한다.
  • 세 가지 용액을 혼합하여 답을 도출해야 하므로, 서로 다른 세 용액을 가리키는 변수(ansL, ansR, ansM)를 선언하였다.
  • 하나의 mid를 기준점으로 잡고, 양 끝에서부터 left와 right를 좁혀가며 이분탐색을 한다. 이분 탐색 포인터가 범위를 넘어갔다면 mid의 위치를 조정하여 다시 양 끝부터 탐색한다.
  • 현재까지 발견한 0에 가장 가까운 특성값 key를 갱신하며 탐색을 진행한다. key보다 더 작은 특성값이 발견된다면 key를 더 작은 값으로 갱신하고 그 순간의 left, right, mid를 저장한다.
    • 용액의 특성값이 0보다 작다면, 0에 더욱 가까운 근사치를 찾기 위해 특성값이 조금 더 큰 용액을 더해보아야 한다. 따라서 left를 1 증가시킨다.
    • 반대로 용액의 특성값이 0보다 크다면 특성값이 조금 더 작은 용액을 더해보아야 한다. 따라서 right를 1 감소시킨다.
  • mid가 배열 끝 인덱스까지 커졌다면 탐색을 종료한다. 이 때 ansL, ansR, ansM에 저장된 값들이 0에 가장 가까운 특성값을 만드는 세 용액을 가리키는 포인터 변수이다.

 

코드

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

public class Main {

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		int N = Integer.parseInt(br.readLine());

		long[] sol = new long[N];

		StringTokenizer st = new StringTokenizer(br.readLine(), " ");
		for (int i = 0; i < N; i++) {
			sol[i] = Long.parseLong(st.nextToken());
		}

		Arrays.sort(sol);

		int ansL = 0;
		int ansR = 0;
		int ansM = 0;
		
		long key = Long.MAX_VALUE;

		for(int mid = 1; mid < N - 1; mid++) {
			int left = 0;
			int right = N - 1;
			while (left <= right) {
				long mixed = sol[left] + sol[right] + sol[mid];
				
				if(left >= mid || right <= mid) { break; }
				
				if(Math.abs(0 - mixed) < Math.abs(0 - key)) {
					key = Math.abs(0 - mixed);
					ansL = left;
					ansR = right;
					ansM = mid;
				}
				
				if (mixed < 0) {
					left++;
				} else if (mixed > 0) {
					right--;
				} else { //(mixed == 0) : 최적해를 찾았다면
					break;
				}
			}
		}
		
		StringBuilder sb = new StringBuilder();
		sb.append(sol[ansL]).append(" ").append(sol[ansM])
			.append(" ").append(sol[ansR]);
		
		System.out.print(sb);
	} 

}

 

 

git 링크

(git 링크 첨부)

문제 풀이 날짜: 2023.10.13
포스트 작성일: 2023.10.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 1939번: 중량제한 (골드3)

 

키워드

다익스트라

 

풀이 접근법

  • 서로 다른 두 공장 A, B가 있다고 할 때, A → B로 이동하는 모든 경로(직+간접) 중에서 ‘최대 하중’을 구하는 문제이다. 따라서 다익스트라로 풀이하였다.
  • 단, 전체 경로 중에서 건너게 되는 다리마다의 최대 하중은 각기 다르며, 그 다리 중 가장 버틸 수 있는 무게가 적은 쪽을 기준으로 맞추어야 한다. 따라서 경로 중 경유하는 간선 중 가장 가중치가 작은 간선을 ‘하중’으로 택한다.
  •  가중치 큰 간선을 우선 선택한다. pq에는 무게가 큰 순서대로 Node를 넣어야 한다. (내림차순 정렬) ⇒ 가능한 하중이 큰 경로를 찾아야 하므로
  • 현재까지의 최대 하중보다 작으면 가지치기한다 ⇒ 그 경로에서는 최대 하중보다 큰 짐을 수송할 수 없으므로

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;

public class Main {

	private static List<Node>[] graph;
	private static int[] maxWeight;
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		String[] temp = br.readLine().split(" ");

		int N = Integer.parseInt(temp[0]); // 섬의 개수
		int M = Integer.parseInt(temp[1]); // 다리의 개수

		graph = new LinkedList[N + 1];
		maxWeight = new int[N + 1]; //dist 배열

		for (int i = 1; i <= N; i++) {
			graph[i] = new LinkedList<>();
			maxWeight[i] = -1;
		}

		for (int i = 1; i <= M; i++) {
			temp = br.readLine().split(" ");

			int A = Integer.parseInt(temp[0]);
			int B = Integer.parseInt(temp[1]);
			int C = Integer.parseInt(temp[2]);

			graph[A].add(new Node(B, C)); // 양방향
			graph[B].add(new Node(A, C));
		}

		temp = br.readLine().split(" ");
		int start = Integer.parseInt(temp[0]);
		int end = Integer.parseInt(temp[1]);

		int maxWeight = dijkstra(start, end);

		System.out.println(maxWeight);
	}

	private static int dijkstra(int start, int end) {
		PriorityQueue<Node> pq = new PriorityQueue<>((o1, o2) -> Integer.compare(o2.weight, o1.weight)); // 가중치 큰 간선 우선

		maxWeight[start] = Integer.MAX_VALUE;
		pq.add(new Node(start, maxWeight[start]));
		
		// Node에 기록된 '최소' 중량 중 최대인 것을 찾는다.
		while (!pq.isEmpty()) {
			Node n = pq.poll();

			// 현재까지 구해진 최대값보다 작으면,
			// 즉 n까지 도달하는 데 소모되는 weight가 유망하지 않으면 가지치기
			if(n.weight < maxWeight[n.num]) {
				continue;
			}

			for (Node next : graph[n.num]) {
				int currWeight = Math.min(n.weight, next.weight); //이 경로의 하중
				if (currWeight > maxWeight[next.num]) { //더 큰 것으로 교체될 수 있다면 add
					maxWeight[next.num] = currWeight;
					pq.add(new Node(next.num, maxWeight[next.num]));
				}
			}
		}

		return maxWeight[end];
	}

}

class Node {
	int num; // 현재 노드 번호
	int weight; // 해당 경로까지의 '최소' 중량

	public Node(int num, int weight) {
		this.num = num;
		this.weight = weight;
	}
}

 

 

문제 풀이 날짜: 2023.10.13
포스트 작성일: 2023.10.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 1019번: 책 페이지 (골드1)

 

키워드

수학, 나머지 연산(모듈러)

 

풀이 접근법

  • SWEA 5604번: [Professional] 구간 합 문제와 거의 유사한 문제이다. 구하는 배열은 같고 구간 합만 계산하지 않는다는 차이가 있다.
  • N이 10^9이하의 자연수이기 때문에 O(N)보다도 더 빠른 알고리즘이 필요하다.
  • N자리의 수가 있다고 할 때, 각 자리마다 떼어서 0~9가 몇 개 나왔는지를 살핀다.
  • 예를 들어 4자리 수의 0~9 출현 횟수를 구하고 싶다면, 일의 자리를 0~9로 변형시켜서 개수를 센다. 이 과정을 일의 자리부터 천의 자리까지 반복한다.
  • 7542라는 숫자가 있다고 할 때, 구간은 다음과 같이 나눈다.
  • 아래의 구간마다 0~9의 개수를 세면 된다.
7540 이상 7542 미만 사이의 모든 숫자
7500 이상 7540 미만 사이의 모든 숫자
7000 이상 7500 미만 사이의 모든 숫자
0000 이상 7000 미만 사이의 모든 숫자

 

  • 7500 이상 7540 미만 사이의 모든 숫자
    • 7500부터 7539까지 7과 5라는 숫자는 항상 반복적으로 나타난다. 따라서 7과 5의 개수는 7500부터 7539 사이의 모든 수의 개수이다. ⇒ 7539 - 7500 + 1 = 40개
    • 10의 자리에는 —0- 부터 —3-까지 0, 1, 2, 3이 출현한다. 이 수들은 각각 10회씩 출현한다. (총 40개)
    • 1의 자리에는 0부터 9가 모두 출현한다. 이 수들은 각각 4회씩 출현한다. (총 40개)
  • 7000 이상 7500 미만 사이의 모든 숫자
    • 7000부터 7499까지 7이라는 숫자는 항상 반복적으로 나타난다. 따라서 7의 개수는 7000부터 7499 사이의 모든 수의 개수이다 ⇒ 7499 - 7000 + 1 = 500개
    • 100의 자리에는 -0— 부터 -4— 까지 0, 1, 2, 3, 4가 출현한다. 이 수들은 각각 100회씩 출현한다. (예: 400부터 499까지 총 100회 출현한다.)
    • 10의 자리와 1의 자리에는 00부터 99가 온다. 100의 자리에 총 5가지 숫자가 오므로 이 수들은 각각 5회씩 출현한다. (예: 99의 경우 099, 199, 299, 399, 499의 5회)

 

코드

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

public class Main {

	private static long start;
	private static long end;
	private static long mul;
	
	private static long[] numbers;
	
	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		//1 <= N <= 1_000_000_000
		int N = Integer.parseInt(br.readLine());
		
		start = 1;
		end = N;
		mul = 1;
		
		numbers = new long[10];
        
		if(start == 0) start = 1; 
		while(start <= end) {
			while(start % 10 != 0 && start <= end) {
				calculatePlaces(start);
				start++; 
			}
			
			if(start > end) break;
            
			while(end % 10 != 9 && start <= end) {
				calculatePlaces(end);
				end--; 
			}
			
			long gap = (end / 10) - (start / 10) + 1;
			
			for(int i = 0; i < 10; i++) {
				numbers[i] += (gap * mul);
			}
            
			mul *= 10;
			start /= 10;
			end /= 10;
		}
		
		StringBuilder sb = new StringBuilder();
		for(long n : numbers) {
			sb.append(n).append(" ");
		}
		
		System.out.println(sb);
	}
	
	private static void calculatePlaces(long target) {
		for(long i = target; i > 0; i /= 10) {
			String s = Long.toString(i);
			int digit = s.charAt(s.length() - 1) - '0';
			
			numbers[digit] += mul;
		}
	}
}

수의 규칙성을 찾는 것이 꽤나 어려운 문제였다. 수의 각 자릿수와 나머지 연산을 적절히 활용하여 수가 출현한 개수를 세는 게 핵심이다. 처음에는 규칙을 찾는 것이 어려워 다른 블로그의 풀이를 참고하여 풀었다.

 

참고한 블로그

https://velog.io/@sinclairr/boj-1019

문제 풀이 날짜: 2023.10.13
포스트 작성일: 2023.10.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 24230번: 트리 색칠하기 (골드5)

 

키워드

트리의 특징, BFS

 

풀이 접근법

  • 단순 DFS로 탐색하면 시간 초과가 난다. 또, 트리를 양방향 그래프로 생성하지 않으면 반례가 생기므로 주의한다.
  • 그래프로 구현한 트리를 루트부터 탐색하면서, 답안으로 주어진 색과 동일하다면 넘어가고, 동일하지 않다면 부모노드의 색깔로 색칠하며 리프 노드까지 탐색한다.
    • 자식 노드를 칠할 때는, 부모 노드의 색깔을 가져다가 쓴다.(colors[child] = prevColors[parent])
    • 이때, 각 노드마다 부모노드의 색을 기억해야 하므로 별도의 배열을 선언해준다.
  • 부모노드를 다시 칠하면 별도로 기억해두었던 부모노드의 색깔(prevColors)도 변경되므로 이를 갱신해주어야 한다. 
    • prevColors[자기자신]를 갱신시키지 않아 0으로 남아있을 경우 불필요한 도색이 또 필요하게 된다. 
    • 그러므로 부모 노드의 색을 저장하는 prevColors[자기자신] 값은 항상 color[자기자신]로 갱신되어야 한다. 그렇지 않으면 모든 자식노드들에게 연쇄적으로 색이 칠해지지 않으니 주의한다.

 

코드

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

public class Main {

	private static int N;
	
	private static int[] colors;
	private static int[] prevColors;
	private static int[] targets;
	
	private static boolean[] visited;
	private static List<Integer>[] tree;
	
	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		N = Integer.parseInt(br.readLine());
		
		colors = new int[N + 1];
		prevColors = new int[N + 1];
		targets = new int[N + 1];
		
		visited = new boolean[N + 1];
		
		tree = new LinkedList[N + 1];
		
		StringTokenizer st = new StringTokenizer(br.readLine(), " ");
		for(int i = 1; i <= N; i++) {
			targets[i] = Integer.parseInt(st.nextToken());
			tree[i] = new LinkedList<>();
		}

		for(int i = 0; i < N - 1; i++) {
			String[] temp = br.readLine().split(" ");

			int u = Integer.parseInt(temp[0]);
			int v = Integer.parseInt(temp[1]);
			
			tree[u].add(v);
			tree[v].add(u);
		}
		
		int count = BFS(1);
		
		System.out.print(count);
	}
	
	private static int BFS(int start) {
		Queue<Integer> queue = new ArrayDeque<>();
		int count = 0;
		
		queue.add(start);
		visited[start] = true;

		while(!queue.isEmpty()) {
			int parent = queue.poll();
			
			if(colors[parent] != targets[parent]) {
				colors[parent] = targets[parent]; //원하는 색을 칠한다.
				prevColors[parent] = colors[parent]; //부모 노드의 색을 저장한다. => 자식 노드에도 색칠
				count++;
			}
			
			for(int child : tree[parent]) {
                if(visited[child]) { continue; }
				visited[child] = true;
                
				colors[child] = prevColors[parent]; //부모 노드의 색과 같은 것으로 칠한다.
                prevColors[child] = colors[child];
				queue.add(child);
			}
		}
		
		return count;
	}
}

 

참고한 링크

https://amor-fati.tistory.com/235

문제 풀이 날짜: 2023.10.11
포스트 작성일: 2023.10.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

SWEA 14510번: 나무 높이 (D2)

 

키워드

그리디, 수학, 나머지 연산(모듈러)

 

풀이 접근법

  • 모든 나무가 성장해야 하는 높이 (remains[i]) 를 2로 나눈 몫은 필요한 짝수 날짜의 수이고, 2로 나눈 나머지는 필요한 홀수 날짜의 수이다.
  • 이때 홀수 날짜와 짝수 날짜의 수는 항상 쌍을 이루어야 한다. (예: 홀수 날짜에 3회 물을 주었다면 짝수 날짜에도 3회 물을 주어야 한다.
  • 쌍을 이루지 않는 날짜는 하루를 쉬고 건너서 물을 준 것이다. (편의상 건너뛴 날을 로 표현한다.)
    • 그런데, 짝수 날짜(∅ + 2m)는 '연달아서 물을 준 홀수 날짜와 짝수 날짜' 쌍(1m + 2m)으로 압축이 가능하다.
    • 예를 들어, 총 6m를 성장시킨 경우는  ∅ + 2m +  ∅ + 2m  ∅ + 2m 로 나타낼 수도 있지만 1m + 2m + 1m + 2m 으로 나타낼 수도 있다. 이 경우 6일치의 성장을 4일치로 줄일 수 있다.
    • 우리는 최소 기간을 구해야 하므로 날짜를 압축해주어야 한다. 압축하고도 남은 나머지 날짜가 홀수라면 그냥 1일을 더해주고, 짝수라면 하루 쉰 것까지 포함해서 2일을 결과값에 더해준다.

 

코드

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

public class Solution {

	private static int[] trees;
	private static int[] remains;
	
	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		int T = Integer.parseInt(br.readLine());

		StringBuilder sb = new StringBuilder();
		for (int test_case = 1; test_case <= T; test_case++) {
			int N = Integer.parseInt(br.readLine());
			
			trees = new int[N];
			remains = new int[N];
			
			int maxHeight = 0;
			StringTokenizer st = new StringTokenizer(br.readLine(), " ");
			for(int i = 0; i < N; i++) {
				trees[i] = Integer.parseInt(st.nextToken());
				maxHeight = Math.max(trees[i], maxHeight);
			}
			
			int oddCount = 0; //홀수 날짜의 갯수
			int evenCount = 0; //짝수 날짜의 갯수
			for(int i = 0; i < N; i++) {
				remains[i] = maxHeight - trees[i];
				
				oddCount += remains[i] % 2;
				evenCount += remains[i] / 2;
			}
			
			int result = oddCount + evenCount;
			if(oddCount < evenCount) { //짝수 일자는 홀수 일자로 압축할 수 있다 => 날짜 수를 더욱 줄여보자.
				int gap = (evenCount - oddCount) * 2; //쉬는 날을 포함한 짝수 날짜의 수 (∅ + 2)
				int remain = (gap / 3) * 2; //짝수 날짜를 홀수 날짜로 압축한다.
				
				if(gap % 3 == 2) { //압축하고 남은 나머지가 짝수 날짜라면
					remain += 2; //홀수 날짜도 함께 쉰다. (∅ + 2)
				} else if (gap % 3 == 1) remain += 1; //그렇지 않으면 홀수 날짜만 더한다.
				
				result = (oddCount * 2) + remain;
			} else if (oddCount - evenCount > 1){ //홀수가 더 많을 경우 : 1 ∅ 1 ∅ 1 ... => ∅의 개수를 더한다.
				result += (oddCount - evenCount - 1);
			}
			
			sb.append("#").append(test_case).append(" ").append(result).append("\n");
		}
		
		System.out.print(sb);
	}

}

처음에는 다른 방식으로 풀었다가 답이 제대로 나오지 않았다. 짝수 날짜의 개수와 홀수 날짜의 개수를 세야한다는 발상을 하지 못해, 해당 부분만 다른 사람의 풀이를 참고하여 풀었다. 

홀&짝에 따라 물을 주는 규칙을 찾고, 쉰 날짜를 더해주는 것이 특히나 중요한 부분이었다.

 

참고한 링크

https://velog.io/@woonchoi/SWEA14510-나무-높이

문제 풀이 날짜: 2023.10.11
포스트 작성일: 2023.10.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 17182번: 우주 탐사선 (골드3)

 

키워드

플로이드-워샬, DFS

 

풀이 접근법

  • 입력 그래프가 인접 행렬로 주어지며, 모든 행성을 탐사하기 위한 최소 시간이 필요하므로 플로이드-워샬로 풀이한다.
  • 그런데 여기서 플로이드 워샬로 구해진 결과물은 단순히 각 점으로 가는 최소 시간(최단 경로)일 뿐이다. 우리가 구하고 싶은 것은 시작점부터 모든 정점을 지나는 최소 시간이다.
  • 따라서 0부터 모든 정점을 지나는 경로는 DFS(그래프 탐색)로 구해준다. (N은 최대 10)
  • 시작점 K는 문제에서 주어진다. 중복 경우를 제거하기 위해 시작점을 visit하고 depth 1부터 시작한다.
    • Why? : 어떤 시작점 A에서 출발해서 B, C, D를 방문한다고 가정할 때, A부터 방문해야 하는 것은 이미 자명한 사실임.
    • ⇒ 시작점을 제외한 (N-1)! 가지 경우의 수만 구하면 된다.
    • 또, A에서 출발하여 B에 도착하는 경우를 찾고싶을 때 A-B-(C-D) 라는 경우까지 구해버리면 유의미한 경우의 수가 아님.

 

코드

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

public class Main {

	private static int N;
	private static int K;
	
	private static final int INF = 9_999_999;
	
	private static boolean[] visited;
	private static int[][] D;
	
	private static int min = Integer.MAX_VALUE;
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		String[] temp = br.readLine().split(" ");
		
		N = Integer.parseInt(temp[0]);
		K = Integer.parseInt(temp[1]);
		
		visited = new boolean[N];
		D = new int[N][N];
		
		StringTokenizer st;
		for(int i = 0; i < N; i++) {
			st = new StringTokenizer(br.readLine(), " ");
			for(int j = 0; j < N; j++) {
				D[i][j] = Integer.parseInt(st.nextToken());
			}
		}
				
		//플로이드 워샬
		for(int k = 0; k < N; k++) { //경
			for(int i = 0; i < N; i++) { //출
				for(int j = 0; j < N; j++) { //도
					D[i][j] = Math.min(D[i][k] + D[k][j], D[i][j]);
				}
			}
		}
		
		visited[K] = true; //시작점을 체크하고 진입한다.
		DFS(1, K, 0); //주의: depth가 1부터 시작
		
		System.out.println(min);
	}

	private static void DFS(int depth, int start, int dist) {
		if(depth == N) {
			min = Math.min(dist, min);
			return;
		}
		
		for(int i = 0; i < N; i++) {
			if(i == start) { continue; }
			
			if(!visited[i]) {
				visited[i] = true;
				DFS(depth + 1, i, dist + D[start][i]);
				visited[i] = false;
			}
		}
	}
}

 

 

참고한 링크

https://bleron.tistory.com/215
https://dlwnsdud205.tistory.com/253

문제 풀이 날짜: 2023.10.10
포스트 작성일: 2023.10.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

구현, 시뮬레이션

 

키워드

백준 온라인 저지 17144번: 미세먼지 안녕! (골드4)

 

풀이 접근법

  • 행렬의 모든 점을 중에서 미세먼지의 위치(Point)를 List로 취합하여 해당 부분을 중심으로 인접한 네 방향으로 확산시킨다.
    • 단, List를 사용하면 메모리를 더 많이 차지하므로 행렬의 모든 점(R * C)을 탐색하는 것보다 효율적일 수도, 비효율적일 수도 있다.
  • 미세먼지를 4방 확산시키면서, 미세먼지가 위치한 곳을 다시 List에 취합한다. 기존 미세먼지가 사라졌을 수도 있고, 기존에는 없었던 위치에 새 미세먼지가 생겼을 수도 있기 때문이다.
  • 단, 미세먼지를 확산시킬 때에는 주의해야 할 점이 있다.
    • 분산시킨 결과새 배열에 저장해주어야 한다.
    • 그렇지 않으면 기존에 존재했던 미세먼지와 새로 밀려들어온 미세먼지가 합산되며 잘못된 결과가 나올 수 있다.
    • 예를 들어, 미세먼지가 5만큼 있던 지점은 본래 사방으로 미세먼지를 1씩 확산시켜야 한다.
    • 그런데 다른 지점으로부터 5만큼의 미세먼지가 흘러들어왔고, 현재 10의 미세먼지를 보유하게 되었다고 하자.
    • 이 경우 사방으로 1이 아닌 2만큼의 미세먼지를 확산시키게 되는데, 이는 잘못된 값이다.
    • 따라서 분산시킨 결과 배열(newMatrix)과 원본 배열(matrix)을 합산해서 확산 결과를 도출한다.
  • 퍼트리기를 끝내면 공기청정기로 미세먼지를 청소(배열 돌리기 로직) 한다.
    • 공기청정기를 기준으로 상단은 반시계 방향, 하단은 시계방향으로 흘러가므로 cleanUp(배열을 돌리는 메소드)위, 아래 두 번 호출해주었다.
    • delta 배열은 시계방향, 반시계방향을 따로따로 선언해주었다.
  • 미세먼지를 이동시킬 때에는 delta값을 따라 배열을 밀어주면 된다.
    • 이때, 배열값을 밀고싶은 목적지(B)에 현재 위치(A)의 값을 대입하면서 밀면 B에 있는 배열값이 사라지게 되므로 주의한다.
    • 예를 들어, (0,1) - (0,2) - (0,3) - ... 순으로 탐색할 때, (0,2)에 (0,1)의 값을 대입하면 그 다음에 (0,2)의 원래 값을 (0,3)에 대입할 수 없다.
    • 따라서 B의 위치를 가리킨 상태에서 A 위치의 값을 끌어다 쓰는 형식으로 배열값을 대입힌다.
    • 즉, (0,3) - (0,2) - (0,1) - ... 순으로 탐색하면서 (0,3) 위치에 (0,2)의 값을 대입하고 (0,2)로 포인터를 옮긴다. 그리고 (0,2)에 (0,1)의 값을 대입해준다. 이 과정을 공기청정기를 다시 만날 때까지 반복한다.

 

코드

import java.awt.Point;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

public class Main {

	private static int R;
	private static int C;
	private static int T;

	private static int[][] matrix;

	// 시계방향 delta (아래쪽)
	private static int[] dr = { 1, 0, -1, 0 };
	private static int[] dc = { 0, 1, 0, -1 };

	// 반시계방향 delta (윗쪽)
	private static int[] rdr = { -1, 0, 1, 0 };
	private static int[] rdc = { 0, 1, 0, -1 };

	private static List<Point> airCleaners = new ArrayList<>();
	private static List<Point> dusts = new ArrayList<>();

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		String[] temp = br.readLine().split(" ");

		R = Integer.parseInt(temp[0]);
		C = Integer.parseInt(temp[1]);
		T = Integer.parseInt(temp[2]);

		matrix = new int[R][C];

		StringTokenizer st;
		for (int i = 0; i < R; i++) {
			st = new StringTokenizer(br.readLine(), " ");
			for (int j = 0; j < C; j++) {
				matrix[i][j] = Integer.parseInt(st.nextToken());

				if (matrix[i][j] == -1)
					airCleaners.add(new Point(j, i));
				
				if(matrix[i][j] > 0) 
					dusts.add(new Point(j, i));
			}
		}

		int answer = solution(T, matrix);

		System.out.println(answer);
	}

	private static int solution(int T, int[][] matrix) {
		for (int t = 0; t < T; t++) {
			int[][] newMatrix = new int[R][C];

			// 1.미세먼지 확산
			for (Point d : dusts) {
				int i = d.y; int j = d.x;
				if (matrix[i][j] != 0 && matrix[i][j] != -1) {
					spread(i, j, matrix, newMatrix);
				}
			}

			sumDust(matrix, newMatrix);

			// 2.공기청정기로 미세먼지 날리기
			cleanUp(airCleaners.get(0), airCleaners.get(1), rdr, rdc, matrix);
			cleanUp(airCleaners.get(1), airCleaners.get(0), dr, dc, matrix);
			
			// 3.미세먼지가 있는 곳 다시 수집
			dusts.clear();
			for (int i = 0; i < R; i++) {
				for (int j = 0; j < C; j++) {
					if(matrix[i][j] > 0) dusts.add(new Point(j, i));
				}
			}
		}

		int count = 0; // 미세먼지량 세기
		for (int i = 0; i < R; i++) {
			for (int j = 0; j < C; j++) {
				if (matrix[i][j] != 0 && matrix[i][j] != -1) {
					count += matrix[i][j];
				}
			}
		}

		return count;
	}

	private static void sumDust(int[][] matrix, int[][] newMatrix) {
		for (int i = 0; i < R; i++) {
			for (int j = 0; j < C; j++) {
				matrix[i][j] += newMatrix[i][j];
			}
		}
	}

	private static void spread(int r, int c, int[][] matrix, int[][] newMatrix) {
		int count = 0;
		int dust = (int) Math.floor(matrix[r][c] / 5);

		int nr = 0;
		int nc = 0;
		for (int i = 0; i < dr.length; i++) {
			nr = r + dr[i];
			nc = c + dc[i];

			if (!isValid(nr, nc) || matrix[nr][nc] == -1) {
				continue;
			}

			newMatrix[nr][nc] += dust;
			count += 1;
		}

		matrix[r][c] -= dust * count;
	}

	private static void cleanUp(Point point, Point anotherPoint, int[] dr, int[] dc, int[][] matrix) {
		int currR = point.y;
		int currC = point.x;
		
		int dir = 0;
		int nextR = currR; 
		int nextC = currC;
		do {
			nextR = currR + dr[dir];
			nextC = currC + dc[dir];
			
			if (!isValid(nextR, nextC)
					|| nextR == anotherPoint.y) { // 벽에 부딪혔다면  or 반대편에 닿았다면 방향 전환
				dir = (dir + 1) % 4;
				continue;
			}
			
			if(matrix[currR][currC] != -1) {
				matrix[currR][currC] = matrix[nextR][nextC];
			}
			
			if(matrix[currR][currC] == -1 && (currR != point.y || currC != point.x))
				matrix[currR][currC] = 0;
			
			currR = nextR; currC = nextC;	
		} while (!(nextR == point.y && nextC == point.x)); // 원점으로 돌아올 때까지 반복
	}

	private static boolean isValid(int nr, int nc) {
		return (nr > -1 && nc > -1 && nr < R && nc < C);
	}
}

리스트를 사용해서 그런지 다른 사람들의 코드에 비해 메모리와 시간이 많이 소모되었다. List뿐만 아니라 Set이나 Hash 등의 자료구조를 쓰면 특히 시간을 더 많이 소모하기 때문에, 언제나 최적의 효율을 보장하는 것은 아니다.

 

문제 풀이 날짜: 2023.10.05
포스트 작성일: 2023.10.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

SWEA 5656번: [모의 SW 역량테스트] 벽돌 깨기

 

키워드

구현, 시뮬레이션

 

풀이 접근법

  • 기본 절차
    1. W개의 벽돌 중 깨트릴 것을 고른다.
    2. 해당 벽돌을 깨트리고 newMatrix에 기록한다.
    3. 벽돌을 바닥으로 내린다.
    4. 다음 depth에 복사된 newMatrix를 인자로 넘겨준다.
    5. 리턴받는다면 원본 matrix를 이용하여 새 결과를 계산한다.
  • 기저조건
    • 더이상 깰 벽돌이 없다면 minCount를 0으로 갱신하고 true(최적해를 찾음)를 리턴한다.
    • 모든 구슬을 다 던졌다면 잔여 벽돌 수도 minCount를 갱신하고 false(더 탐색해야 함)를 리턴한다.
  • 1. 깨트릴 벽돌을 고른다.
    • N 개의 구슬을 떨어트려 W * H 칸의 안의 벽돌을 부순다. ⇒ 최대 12 * 15칸
    • 부술 수 있는 벽돌은 각 col별로 가장 최상단에 위치한 벽돌이다.
    • 따라서 항상 W개의 벽돌 중에서 뽑는다.
    • 어떤 col의 벽돌이 깨져도 그 col에 존재하는 다른 벽돌을 또 깰 수 있으므로 중복 순열로 뽑는다. 12^4가지 경우의 수
  • 2. 벽돌을 깨트리고 newMatrix에 기록한다.
    • breakBlocks() : 선정된 벽돌과 그 영향을 받는 블록을 Flood Fill(BFS)로 깬다.
    • 파괴되는 범위가 1 초과인 블록들만 queue 에 넣으며 탐색한다.
  • 3. 벽돌을 바닥으로 내린다.
    • resetBlocks() : 각 행을 탐색하면서 블록을 인덱스 조작으로 옮겨준다. (배열 값 교체하기)
    • 옮길 블록을 가리키는 행 번호는 nr이며, 이 nr이 가리키는 블록이 0이 될 때까지 (빈 자리를 발견할 때까지) 행을 위로 올린다.
    • nr이 0이 되었다면 해당 행(row)의 빈 칸은 모두 탐색을 완료한 것이다. 따라서 다음 열(col)을 탐색한다.
  • 4. 다음 depth에 복사된 newMatrix를 인자로 넘겨준다.
  • 5. 리턴받는다면 원본 matrix를 이용하여 새 결과를 계산한다.

 

코드

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

public class Solution {

	private static int N;
	private static int W;
	private static int H;
	private static int minCount;

	private static int matrix[][];

	private static int[] dr = { 0, 1, -1, 0 };
	private static int[] dc = { 1, 0, 0, -1 };

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		int T = Integer.parseInt(br.readLine());

		StringBuilder sb = new StringBuilder();
		for (int test_case = 1; test_case <= T; test_case++) {
			String[] temp = br.readLine().split(" ");

			N = Integer.parseInt(temp[0]);
			W = Integer.parseInt(temp[1]);
			H = Integer.parseInt(temp[2]);
			minCount = Integer.MAX_VALUE;

			matrix = new int[H][W];

			StringTokenizer st;
			for (int i = 0; i < H; i++) {
				st = new StringTokenizer(br.readLine(), " ");
				for (int j = 0; j < W; j++) {
					matrix[i][j] = Integer.parseInt(st.nextToken());
				}
			}

			DFS(0, matrix);

			sb.append("#").append(test_case).append(" ").append(minCount).append("\n");
		}

		System.out.print(sb);
	}

	/*
	 * 구슬 던지기 : 중복 순열 리턴값 => 모든 벽돌이 제거되었는지 여부를 체크
	 */
	private static boolean DFS(int depth, int[][] matrix) {
		int result = getRemain(matrix);
		if(result == 0) { // 구슬을 던지기 전에 잔여 벽돌 수 체크
			minCount = 0;
			return true;
		}
		
		if (depth == N) {
			// 모든 구슬을 다 던졌다면 잔여 벽돌 수로 최소값을 갱신
			minCount = Math.min(result, minCount);
			return false;
		}

		int[][] newMatrix = new int[H][W];
		for (int col = 0; col < W; col++) {
			// 해당 열에 떨어트릴 경우 제거되는 맨 윗 벽돌 찾기
			int row = 0;
			while (row < H && matrix[row][col] == 0) ++row;

			// 주의: 별도로 잔여 벽돌 수를 체크해주었기 때문에 다음 깊이를 탐색하지 않아도 됨.
			// 그렇지 않으면 벽돌이 없는 열도 방문한 후 다음 depth로 넘어가야 한다.
			// 깰 벽돌이 존재하지 않는다면 해당 열은 모두 빈칸임 => continue;
			if (row == H) continue;

			copyMatrix(matrix, newMatrix);

			// 벽돌이 존재한다면 연쇄적으로 주변 벽돌도 제거
			breakBlocks(newMatrix, row, col);
			resetBlocks(newMatrix);

			// 다음 구슬 던지러 가기 : 재귀호출 => 호출 결과가 true라면 종료
			if(DFS(depth + 1, newMatrix)) return true;
		}
		return false;
	}

	// 선정된 블록과 그 영향을 받는 블록을 모두 깨트린다 : Flood Fill (4방 BFS)
	private static void breakBlocks(int[][] matrix, int row, int col) {
		Queue<Node> queue = new ArrayDeque<>();

		if (matrix[row][col] > 1) { queue.add(new Node(row, col, matrix[row][col])); }
		matrix[row][col] = 0; // 방문 체크

		while (!queue.isEmpty()) {
			Node n = queue.poll();
			
			for(int dir = 0; dir < 4; dir++) {
				int nr = n.row;
				int nc = n.col;
				
				for(int i = 0; i < n.power - 1; i++) {
					nr += dr[dir];
					nc += dc[dir];
					
					if(!isValid(nr, nc)) { continue; }
					
					if(matrix[nr][nc] > 0) {
						if (matrix[nr][nc] > 1) { queue.add(new Node(nr, nc, matrix[nr][nc])); }
						matrix[nr][nc] = 0;
					}
				}
			}
		}

		return;
	}

	// 벽돌 내리기 1 : 빈 자리를 찾아서 벽돌 내리기.
	// 벽돌 내리기 2 : 매 열마다 맨 윗행부터 벽돌을 모두 스택에 넣고, 빈칸으로 만든다. 그 다음 밑바닥부터 벽돌을 채운다.
	private static void resetBlocks(int[][] matrix) {
		for (int col = 0; col < W; col++) {
			int row = H - 1; 
			int nr = -1;
			
			while(row > 0) {
				if(matrix[row][col] == 0) { //빈칸이라면 내릴 벽돌을 찾는다.
					nr = row - 1;
					
					while(nr > 0 && matrix[nr][col] == 0) --nr; //행을 위로 올린다.
					
					matrix[row][col] = matrix[nr][col]; //새로 찾은 벽돌을 빈 칸에 넣는다.
					matrix[nr][col] = 0;
				}
				
				if(nr == 0) break; //해당 행의 탐색 종료
				
				--row; //아직 해당 행을 다 못 봤다면 계속 탐색
			}
		}
	}

	// 배열 복사하기
	private static void copyMatrix(int[][] matrix, int[][] newMatrix) {
		for (int i = 0; i < H; i++) {
			for (int j = 0; j < W; j++) {
				newMatrix[i][j] = matrix[i][j];
			}
		}
	}

	// 남은 벽돌 개수 구하기 : 매번 구슬 던지기 전에 사용할 메소드
	private static int getRemain(int[][] matrix) {
		int count = 0;
		for (int i = 0; i < H; i++) {
			for (int j = 0; j < W; j++) {
				if (matrix[i][j] != 0)
					count++;
			}
		}
		return count;
	}

	private static boolean isValid(int nr, int nc) {
		return (nr > -1 && nc > -1 && nr < H && nc < W);
	}
}

class Node {
	int row, col, power;

	public Node(int row, int col, int count) {
		this.row = row;
		this.col = col;
		this.power = count;
	}
}

 

+ Recent posts