알고리즘/Other

SWEA 2382번 - 미생물 격리 [JAVA]

2025. 4. 3. 10:41
반응형

시간 제한: 50개 TC 5초 / 메모리 제한: 256MB

 

문제 요약

  1. 최초 각 미생물 군집의 위치와 군집 내 미생물의 수, 이동 방향이 주어진다. 약품이 칠해진 부분에는 미생물이 배치되어 있지 않다. 이동방향은 상, 하, 좌, 우 네 방향 중 하나이다.
  2. 각 군집들은 1시간마다 이동방향에 있는 다음 셀로 이동한다.
  3. 미생물 군집이 빨간색에 닿으면 군집 내 미생물의 절반이 죽고(소수점 버림), 이동방향이 반대로 바뀐다.
    • 미생물 수가 0이 되면, 군집이 사라진다.
  4. 이동 후 두 개 이상의 군집이 한 셀에 모이는 경우 군집들이 합쳐지게 된다. 합쳐 진 군집의 미생물 수는 군집들의 미생물 수의 합이며, 이동 방향은 군집들 중 미생물 수가 가장 많은 군집의 이동방향이 된다. 합쳐지는 군집의 미생물 수가 같은 경우는 주어지지 않으므로 고려하지 않아도 된다.

M 시간 동안 이 미생물 군집들을 격리하였다. M시간 후 남아 있는 미생물 수의 총합을 구하여라.

 

제약 사항

  • 구역의 모양은 정사각형으로 주어지며, 한 변의 셀의 개수 N은 5이상 100이하의 정수
  • 미생물 군집의 개수 K는 5이상 1,000이하의 정수
  • 격리 시간 M은 1이상 1,000이하의 정수
  • 각 군집 내 미생물 수는 1 이상 10,000 이하의 정수

 

접근

누가봐도 시뮬레이션 문제. 하라는 대로 잘 하면 되지만 주의를 해야하는 부분은 미생물 군집이 합쳐질 때이다.

 

1. NAIVE

N * N에 해당하는 ArrayList 배열을 만들고, 좌표에 맞게 미생물 군집을 append 해준다. 군집이 2개 이상인 좌표에 대해 하나로 합쳐주는 과정을 거친다.

for (1 to M) {
	ArrayList<군집 정보(미생물 수, 방향)> list[N][N];
	for (미생물 군집 G : 미생물 군집 집합) {
		미생물 군집 G 이동 수행
		list[이동 후 G의 x좌표][이동 후 G의 y좌표].append({이동 후 G의 미생물 수, 방향});
	}
	for (i = 1 to N) {
		for (j = 1 to N) {
			if (list[i][j].size() == 0) continue;
			int cnt = 0, max = 0, dir = 0;
			for (k = 1 to list[i][j].size()) {
				cnt += list[i][j].get(k)[0];
				if (max < list[i][j].get(k)[0]) { // 군집이 2개 이상일 때, 가장 큰 군집의 방향을 따라가기 위함
					max = list[i][j].get(k)[0];
					dir = list[i][j].get(k)[1];
				}
			}
			새로운 미생물 군집 집합에 미생물 군집 정보 push
		}
	}
	현 미생물 군집 집합을 새롭게 만든 미생물 군집 집합으로 교체 (다음 스텝을 위한)
}

시간 복잡도: \( O(M*(N^2 + K)) \)

 

​ SWEA 문제 특성상 모든 테케가 극한으로 들어오지 않기 때문에 통과할 것 같은데 이중 포문 사용으로 시간이 많이 걸리기도 하고 코드 길이가 길어져 구현도 귀찮을 것 같다.

 

2. 정렬

잘 생각해보면, 굳이 N * N 배열을 만들지 않고도 군집을 합칠 수 있다. ArrayList<{미생물 x좌표, y좌표, 미생물 수, 방향}>을 만들고, 미생물 x, y좌표, 미생물 수 순서대로 오름차순 정렬을 수행하면 된다.

압축 과정은 간단한 반복문을 통해 잘 구현할 수 있을 겁니다.. 따로 수도 코드를 넣진 않을게요..

시간 복잡도: \( O(M*KlgK) \)

 

3. Map - merge 메소드

해당 스텝의 미생물 군집을 움직이고, 리스트에 push하고, 정렬하고, 합치고, 다음 스텝의 군집 리스트에 푸시하고,,, 시간복잡도를 대폭 줄일 수 있지만..

역시나 귀찮다. 군집이 이동한 좌표에 군집 정보를 넣으면 해당 좌표에 있는 이전 값과 비교해 자동으로 합쳐지게 만들면(압축) 편할 것 같다는 생각이 들었다. 그래서 생각난게 Map 자료구조의 merge 메소드이다.

Map 의 `merge()` 메소드란?

`merge()` 메소드는 키가 존재하면 값을 갱신하고, 없으면 새 값을 추가하는 기능을 제공합니다.
public V merge(
    K key,
    V value,
    java.util.function.BiFunction<? super V, ? super V, ? extends V> remappingFunction
)
  • 키(key)가 없으면value를 그대로 삽입.
  • 키(key)가 있으면 → 기존 값과 valueremappingFunction을 이용해 병합 후 갱신.

간단한 예제

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("hello", "Hi");

        // "hello" 키가 존재하므로 문자열을 이어붙임
        map.merge("hello", " ssafy13!", (oldVal, newVal) -> oldVal + newVal);

        // "bye" 키가 없으므로 새 값 추가
        map.merge("bye", "Goodbye!", (oldVal, newVal) -> oldVal + newVal);

        System.out.println(map);
    }
}

실행결과 -> {hello=Hi ssafy13!, bye=Goodbye!}

 

그러면 같은 좌표가 들어왔을 때 merge를 잘 시켜줘야겠죠? 위의 정렬에서 같은 좌표에 있는 군집의 미생물 수의 max 값을 이용해서 방향을 정해줬던 것처럼, Map의 merge 메소드 내부에서도 이를 이용해서 군집의 최종 이동 방향을 정할 수 있게 해주면 된다. 그러면 Map<좌표정보, {현재 좌표의 총 미생물 수, 최종 이동 방향, 군집 중 최대 미생물 수}>  로 정의할 수 있다.

 

이제 코드 흐름을 생각해보면

  1. Map<좌표정보, {…}> step 변수에서 군집 정보를 하나씩 가져온다.
  2. 군집을 움직인다.
  3. 새로 만든 Map<좌표정보, {…}> nextStep 변수에 이동된 군집 정보를 merge한다.
  4. step 변수의 내용을 nextStep 내용으로 교체한다. (step = nextStep — copy by referece)

그런데 문제가 있다. Map의 key에 넣을 좌표정보를 int[]나 새로운 class를 만들어 정의하게 되면 문제(혹은 귀찮음)가 생긴다.

 

Map 자료구조의 Key 비교 방식

  • HashMap → hashCode() & equals()
  • TreeMap → compareTo() or Comparator

HashMap을 쓰려고 했더니 hashCode()는 메모리 주소 기반으로 생성된다.. 따라서 내용이 같더라도 다른 객체로 인식되어 올바른 value를 찾을 수 없다. 오버라이드 해줘야함..

TreeMap을 쓰려고 했더니 comparator 만들어야함..

둘 다 구현하기 귀찮다. 좋은 방법이 없을까? 다시 미생물 군집의 좌표를 봤다. X좌표와 Y좌표 모두 100 이하의 정수이다.

XY를 붙여서 XY꼴의 integer로 만들면 Map<Integer, {…}>꼴로 만들어 쉽게 관리할 수 있다!

XY로 붙이는 것도 간단하다. X * 1000 + Y로 만들면 되고, 분리할 때도, X좌표XY / 1000 , Y좌표XY % 1000으로 쉽게 떼어낼 수 있다!

 

핵심 코드

// 상 하 좌 우
final static int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};

public void solve(int testNumber) {
    int n = in.nextInt(), t = in.nextInt(), m = in.nextInt();
    // 좌표 -> 총 미생물 수, 방향, 합칠 때 최대 미생물 수
    HashMap<Integer, int[]> step = new HashMap<>(), nextStep;
    for (int i = 0; i < m; i++) { // 미생물 군집 정보 받고 step에 넣기
        int a = in.nextInt(), b = in.nextInt(), c = in.nextInt(), d = in.nextInt();
        step.put(1000 * a + b, new int[] {c, d - 1, 0});
    }
    while (t-- > 0) {
        nextStep = new HashMap<>();
        for (Entry<Integer, int[]> entry : step.entrySet()) {
            int x = entry.getKey() / 1000, y = entry.getKey() % 1000;
            int[] v = entry.getValue();
            x += dx[v[1]]; y += dy[v[1]]; // 군집 이동
            if (x == 0 || x == n - 1 || y == 0 || y == n - 1) { // 빨간색 닿으면
                v[0] /= 2;
                v[1] ^= 1; // 방향 바꿔줌 0->1, 1->0, 2->3, 3->2
            }
            v[2] = v[0];
            nextStep.merge(1000 * x + y, v, (oldV, newV) -> {
                return new int [] {
                        oldV[0] + newV[0], // 총 미생물 수
                        oldV[2] > newV[0] ? oldV[1] : newV[1], // 지금까지 추가된 군집 중 최대 미생물 수와 새로 들어올 군집의 미생물 수를 비교해서 방향 결정
                        Math.max(oldV[2], newV[0]) // 최대 미생물 수 갱신
                };
            });
        }
        step = nextStep;
    }
    out.println("#" + testNumber + " " + step.values().stream().mapToInt(v -> v[0]).sum());
}

 

시간 복잡도: HashMap사용 시 \( O(M*K) \) 이지만 상수가 커 생각보다 느림 / TreeMap 사용 시 \( O(M*KlgK) \)

 

전체 코드

더보기
import java.io.*;
import java.lang.*;
import java.util.*;
import java.util.Map.Entry;

public class Solution {
    public static void main(String args[]) throws Exception {
        InputReader in = new InputReader();
        PrintWriter out = new PrintWriter(System.out);
        int tests = in.nextInt();
        for (int i = 1; i <= tests; i++) (new Task(in, out)).solve(i);
        out.close();
    }

    static class Task {
        static InputReader in;
        static PrintWriter out;

        Task(InputReader in, PrintWriter out) {
            this.in = in;
            this.out = out;
        }

        final static int INF = (int) 1e9;
        final static int MOD = (int) 1e9 + 7;

        // 상 하 좌 우
        final static int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};

        public void solve(int testNumber) {
            int n = in.nextInt(), t = in.nextInt(), m = in.nextInt();
            // 좌표 -> 미생물 수, 방향, 합칠 때 최대 미생물 수
            HashMap<Integer, int[]> step = new HashMap<>(), nextStep;
            for (int i = 0; i < m; i++) {
                int a = in.nextInt(), b = in.nextInt(), c = in.nextInt(), d = in.nextInt();
                step.put(1000 * a + b, new int[] {c, d - 1, 0});
            }
            while (t-- > 0) {
                nextStep = new HashMap<>();
                for (Entry<Integer, int[]> entry : step.entrySet()) {
                    int x = entry.getKey() / 1000, y = entry.getKey() % 1000;
                    int[] v = entry.getValue();
                    x += dx[v[1]]; y += dy[v[1]];
                    if (x == 0 || x == n - 1 || y == 0 || y == n - 1) {
                        v[0] /= 2;
                        v[1] ^= 1;
                    }
                    v[2] = v[0];
                    nextStep.merge(1000 * x + y, v, (oldV, newV) -> {
                        return new int [] {
                                oldV[0] + newV[0],
                                oldV[2] > newV[0] ? oldV[1] : newV[1],
                                Math.max(oldV[2], newV[0])
                        };
                    });
                }
                step = nextStep;
            }
            out.println("#" + testNumber + " " + step.values().stream().mapToInt(v -> v[0]).sum());
        }
    }

    static class InputReader {
        public BufferedReader reader;
        public StringTokenizer tokenizer;

        public InputReader() {
            reader = new BufferedReader(new InputStreamReader(System.in));
            tokenizer = null;
        }

        public String next() {
            while (tokenizer == null || !tokenizer.hasMoreTokens()) {
                try {
                    tokenizer = new StringTokenizer(reader.readLine());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            return tokenizer.nextToken();
        }

        public int nextInt() { return Integer.parseInt(next()); }

        public long nextLong() { return Long.parseLong(next()); }

        public double nextDouble() { return Double.parseDouble(next()); }

        public String nextLine() {
            String str = "";
            try {
                str = reader.readLine();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return str;
        }
    }
}