PS/Implementation

백준 20056번: 마법사 상어와 파이어볼 (JAVA)

닻과매 2022. 6. 8. 17:09

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

 

20056번: 마법사 상어와 파이어볼

첫째 줄에 N, M, K가 주어진다. 둘째 줄부터 M개의 줄에 파이어볼의 정보가 한 줄에 하나씩 주어진다. 파이어볼의 정보는 다섯 정수 ri, ci, mi, si, di로 이루어져 있다. 서로 다른 두 파이어볼의 위치

www.acmicpc.net

문제

어른 상어가 마법사가 되었고, 파이어볼을 배웠다.

마법사 상어가 크기가 N×N인 격자에 파이어볼 M개를 발사했다. 가장 처음에 파이어볼은 각자 위치에서 이동을 대기하고 있다. i번 파이어볼의 위치는 (r_i, c_i), 질량은 m_i이고, 방향은 d_i, 속력은 s_i이다. 위치 (r, c)는 r행 c열을 의미한다.

격자의 행과 열은 1번부터 N번까지 번호가 매겨져 있고, 1번 행은 N번과 연결되어 있고, 1번 열은 N번 열과 연결되어 있다.

파이어볼의 방향은 어떤 칸과 인접한 8개의 칸의 방향을 의미하며, 정수로는 다음과 같다.

7 0 1
6   2
5 4 3

마법사 상어가 모든 파이어볼에게 이동을 명령하면 다음이 일들이 일어난다.

  1. 모든 파이어볼이 자신의 방향 d_i로 속력 s_i칸 만큼 이동한다.
    • 이동하는 중에는 같은 칸에 여러 개의 파이어볼이 있을 수도 있다.
  2. 이동이 모두 끝난 뒤, 2개 이상의 파이어볼이 있는 칸에서는 다음과 같은 일이 일어난다.
    1. 같은 칸에 있는 파이어볼은 모두 하나로 합쳐진다.
    2. 파이어볼은 4개의 파이어볼로 나누어진다.
    3. 나누어진 파이어볼의 질량, 속력, 방향은 다음과 같다.
      1. 질량은 ⌊(합쳐진 파이어볼 질량의 합)/5⌋이다.
      2. 속력은 ⌊(합쳐진 파이어볼 속력의 합)/(합쳐진 파이어볼의 개수)⌋이다.
      3. 합쳐지는 파이어볼의 방향이 모두 홀수이거나 모두 짝수이면, 방향은 0, 2, 4, 6이 되고, 그렇지 않으면 1, 3, 5, 7이 된다.
    4. 질량이 0인 파이어볼은 소멸되어 없어진다.

마법사 상어가 이동을 K번 명령한 후, 남아있는 파이어볼 질량의 합을 구해보자.

입력

첫째 줄에 N, M, K가 주어진다.

둘째 줄부터 M개의 줄에 파이어볼의 정보가 한 줄에 하나씩 주어진다. 파이어볼의 정보는 다섯 정수 ri, ci, mi, si, di로 이루어져 있다.

서로 다른 두 파이어볼의 위치가 같은 경우는 입력으로 주어지지 않는다.

출력

마법사 상어가 이동을 K번 명령한 후, 남아있는 파이어볼 질량의 합을 출력한다.

제한

  • 4 ≤ N ≤ 50
  • 0 ≤ M ≤ N2
  • 1 ≤ K ≤ 1,000
  • 1 ≤ r_i, c_i ≤ N
  • 1 ≤ m_i ≤ 1,000
  • 1 ≤ s_i ≤ 1,000
  • 0 ≤ d_i ≤ 7

 


 

풀이

구현 문제인 만큼, 문제 조건을 하나도 빠짐없이 충실히 구현하면 됩니다. 살짝 까다로운 구현 부분이라고 한다면,

1) 이동 처리: 문제에서는 행, 열이 1부터 시작하나, 0부터 시작하도록 자료를 받은 경우 (r + (dr[i]+N)*s_i) % N으로 처리하면 됩니다.

2) 파이어볼 저장: 파이어볼로 하는 연산의 특성 상 Queue가 살짝 더 좋을 듯한데, 일단은 'Fireball' class를 담는 이차원 배열을 선언해도 시간이 문제되진 않습니다.

 

틀린 이유

1. 큰 구현 실수 - 질량이 0이면 파이어볼이 없어진다는 부분을 구현하지 않았다.

2. 사소한 구현 실수

 1) '모두 짝수 혹은 홀수'를 구하는 부분을, 첫 번째 방향을 입력 받은 후 이후 입력값과의 차를 2로 나눈 나머지가 1이면 '모두 짝수 혹은 홀수가 아니다'와 같은 로직으로 구현하였는데, JAVA의 % 연산 특성 상 절댓값을 씌워줬어야 한다.

 2) 위 구현이랑 연결되는 부분으로, 처음 dir을 구분하기 위해 dir 초기값은 8(존재하지 않음)으로 입력했는데, 그 다음 dir > 8로 초기 입력을 구분하여 dir 값이 8에서 다시 할당되지 않았다. 문제 주어진 예제에선 우연히 오류가 발생하지 않았을 뿐...

 

1.은 설계를 대충한 대가라면, 2.는 집중력 부족이다. 이런 실수를 계속하면서 구현 문제를 풀면 푸는 의미가 없는데, 막상 하면 설계도 대충하고 싶고~ 기본 구현이 끝나면 바로 제출하고 싶고~...

 

 

(시간복잡도 관점에서) 개선점?

1. 수행하는 연산이 해당 위치에서 넣고 빼고 하는 작업이다 -> queue가 적당하지 않을까 싶다.

2. '이동을 수행하는 method'를, 매 이동마다 새로운 배열을 만들어 움직인 파이어볼을 새로운 배열에 넣어 움직인 파이어볼과 움직이지 않은 파이어볼을 구분하였는데, class에 이를 구분할 변수를 하나 더 넣어주면 좋을 듯하다.

 

 

 

코드

풀이 1. List 이용한 풀이

import java.io.*;
import java.util.*;

public class Main {

    static int[] dr = { -1, -1, 0, 1, 1, 1, 0, -1};
    static int[] dc = { 0, 1, 1, 1, 0, -1, -1, -1};
    static int N, M, K;
    static List<Fireball>[][] fireballs;

    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());
        K = Integer.parseInt(st.nextToken());
        fireballs = (List<Fireball>[][]) new ArrayList[N][N];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                fireballs[i][j] = new ArrayList<Fireball>();
            }
        }
        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(br.readLine());
            int r = Integer.parseInt(st.nextToken())-1;
            int c = Integer.parseInt(st.nextToken())-1;
            int m = Integer.parseInt(st.nextToken());
            int s = Integer.parseInt(st.nextToken());
            int d = Integer.parseInt(st.nextToken());
            fireballs[r][c].add(new Fireball(m, s, d));
        }		
		
        // K번 움직이기
        for (int i = 0; i < K; i++) {
            move();
            merge();
        }
		
        // 남은 질량 구하기
        int mass = 0;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                if (!fireballs[i][j].isEmpty()) {
                    for (Fireball fireball: fireballs[i][j]) {
                        mass += fireball.m;
                    }
                }
            }
        }
        System.out.println(mass);
    }

	// 'Fireball' class 선언
    static class Fireball {
        int m;
        int s;
        int d;

        public Fireball(int m, int s, int d) {
            this.m = m;
            this.s = s;
            this.d = d;
        }

        public String toString() {
            return "Fireball [m=" + m + ", s=" + s + ", d=" + d + "]";
        }
    }

    // 파이어볼이 움직이는 과정을 구현한 method
    static void move() {
        List<Fireball>[][] tempFireballs;
        tempFireballs = (List<Fireball>[][]) new ArrayList[N][N];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                tempFireballs[i][j] = new ArrayList<Fireball>();
            }
        }

        for (int r = 0; r < N; r++) {
            for (int c = 0; c < N; c++) {
                if (!fireballs[r][c].isEmpty()) {
                    for (Fireball fireball: fireballs[r][c]) {
                        int s = fireball.s;
                        int d = fireball.d;
                        int nr = (r + (N+dr[d]) * s) % N;
                        int nc = (c + (N+dc[d]) * s) % N;
                        tempFireballs[nr][nc].add(fireball);
                    }
                }
            }
        }
        fireballs = tempFireballs;
    }

    // 파이어볼이 합쳐지는 과정을 구현한 method
    static void merge() {
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                if (fireballs[i][j].size() > 1) {
                    int sSum = 0, mSum = 0, dir = 8;
                    boolean isSame = true;
                    for (Fireball fireball: fireballs[i][j]) {
                        sSum += fireball.s;
                        mSum += fireball.m;
                        if (dir >= 8) dir = fireball.d;
                        else {
                            if (Math.abs(dir - fireball.d) % 2 == 1) isSame = false;
                        }
                    }
                    int start = (isSame)? 0: 1;
                    sSum /= fireballs[i][j].size();
                    mSum /= 5;
                    fireballs[i][j].clear();
                    if (mSum == 0) continue;
                    for (int d = start; d < 8; d+=2) {
                        fireballs[i][j].add(new Fireball(mSum, sSum, d));
                    }
                }
            }
        }
    }
}

 

풀이 2. 개선점을 적용함

import java.io.*;
import java.util.*;

public class Main {

    static int[] dr = { -1, -1, 0, 1, 1, 1, 0, -1};
    static int[] dc = { 0, 1, 1, 1, 0, -1, -1, -1};
    static int N, M, K;
    static Queue<Fireball>[][] fireballs;

    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());
        K = Integer.parseInt(st.nextToken());
        fireballs = (Queue<Fireball>[][]) new ArrayDeque[N][N];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                fireballs[i][j] = new ArrayDeque<Fireball>();
            }
        }
        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(br.readLine());
            int r = Integer.parseInt(st.nextToken())-1;
            int c = Integer.parseInt(st.nextToken())-1;
            int m = Integer.parseInt(st.nextToken());
            int s = Integer.parseInt(st.nextToken());
            int d = Integer.parseInt(st.nextToken());
            fireballs[r][c].add(new Fireball(m, s, d, 0));
        }		

        // K번 움직이기
        for (int i = 0; i < K; i++) {
            move(i);
            merge(i);
        }

        // 남은 질량 구하기
        int mass = 0;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                while (!fireballs[i][j].isEmpty()) {
                    Fireball fireball = fireballs[i][j].poll();
                    mass += fireball.m;
                }
            }
        }
        System.out.println(mass);
    }

    // 'Fireball' class 선언
    static class Fireball {
        int m;
        int s;
        int d;
        int moved;

        public Fireball(int m, int s, int d, int moved) {
            this.m = m;
            this.s = s;
            this.d = d;
            this.moved = moved;
        }

        public String toString() {
            return "Fireball [m=" + m + ", s=" + s + ", d=" + d + "]";
        }
    }

    // 파이어볼이 움직이는 과정을 구현한 method
    static void move(int level) {
        for (int r = 0; r < N; r++) {
            for (int c = 0; c < N; c++) {
                if (!fireballs[r][c].isEmpty()) {
                    while (!fireballs[r][c].isEmpty()) {
                        if (fireballs[r][c].peek().moved > level) {
                            break;
                        }
                        Fireball fireball = fireballs[r][c].poll();
                        int m = fireball.m;
                        int s = fireball.s;
                        int d = fireball.d;
                        int moved = fireball.moved;
                        int nr = (r + (N+dr[d]) * s) % N;
                        int nc = (c + (N+dc[d]) * s) % N;
                        fireballs[nr][nc].offer(new Fireball(m, s, d, moved+1));
                    }
                }
            }
        }
    }

    // 파이어볼이 합쳐지는 과정을 구현한 method
    static void merge(int level) {
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                if (fireballs[i][j].size() > 1) {
                    int size = fireballs[i][j].size();
                    int sSum = 0, mSum = 0, dir = 8;
                    int moved = level + 1;
                    boolean isSame = true;
                    while (!fireballs[i][j].isEmpty()) {
                        Fireball fireball = fireballs[i][j].poll();
                        sSum += fireball.s;
                        mSum += fireball.m;
                        if (dir >= 8) dir = fireball.d;
                        else {
                            if (Math.abs(dir - fireball.d) % 2 == 1) isSame = false;
                        }
                    }
                    int start = (isSame)? 0: 1;
                    sSum /= size;
                    mSum /= 5;
                    if (mSum == 0) continue;
                    for (int d = start; d < 8; d+=2) {
                        fireballs[i][j].offer(new Fireball(mSum, sSum, d, moved));
                    }
                }
            }
        }
    }
}

 

 

두 풀이 비교

풀이 2가 풀이 1보다 대략 150ms 정도 빠르다.

1. List 대신 Queue를 사용하는 이점을 사실 얼마 없을 듯하다(파이어볼이 많이 겹칠수록 좋은데, 그런 케이스가 그렇게 많이 발생하지 않아, 오히려 List가 더 나을지도?)

2. 매 명령마다 fireball을 담는 이차원 배열을 새로 만들어 주지 않은 이득은 있는 듯하다.