알고리즘

[알고리즘, BOJ] 13460 구슬 탈출 2 - java

ignuy 2024. 10. 4.

문제

스타트링크에서 판매하는 어린이용 장난감 중에서 가장 인기가 많은 제품은 구슬 탈출이다. 구슬 탈출은 직사각형 보드에 빨간 구슬과 파란 구슬을 하나씩 넣은 다음, 빨간 구슬을 구멍을 통해 빼내는 게임이다.

보드의 세로 크기는 N, 가로 크기는 M이고, 편의상 1 ×1 크기의 칸으로 나누어져 있다. 가장 바깥 행과 열은 모두 막혀 있고, 보드에는 구멍이 하나 있다. 빨간 구슬과 파란 구슬의 크기는 보드에서 1×1크기의 칸을 가득 채우는 사이즈이고, 각각 하나씩 들어가 있다. 게임의 목표는 빨간 구슬을 구멍을 통해서 빼내는 것이다. 이때, 파란 구슬이 구멍에 들어가면 안 된다.

이때, 구슬을 손으로 건드릴 수는 없고, 중력을 이용해서 이리 저리 굴려야 한다. 왼쪽으로 기울이기, 오른쪽으로 기울이기, 위쪽으로 기울이기, 아래쪽으로 기울이기와 같은 네 가지 동작이 가능하다.

각각의 동작에서 공은 동시에 움직인다. 빨간 구슬이 구멍에 빠지면 성공이지만, 파란 구슬이 구멍에 빠지면 실패이다. 빨간 구슬과 파란 구슬이 동시에 구멍에 빠져도 실패이다. 빨간 구슬과 파란 구슬은 동시에 같은 칸에 있을 수 없다. 또, 빨간 구슬과 파란 구슬의 크기는 한 칸을 모두 차지한다. 기울이는 동작을 그만하는 것은 더 이상 구슬이 움직이지 않을 때 까지이다.

보드의 상태가 주어졌을 때, 최소 몇 번 만에 빨간 구슬을 구멍을 통해 빼낼 수 있는지 구하는 프로그램을 작성하시오.

입력

첫 번째 줄에는 보드의 세로, 가로 크기를 의미하는 두 정수 N, M (3 ≤ N, M ≤ 10)이 주어진다. 다음 N개의 줄에 보드의 모양을 나타내는 길이 M의 문자열이 주어진다. 이 문자열은 '.', '#', 'O', 'R', 'B' 로 이루어져 있다. '.'은 빈 칸을 의미하고, '#'은 공이 이동할 수 없는 장애물 또는 벽을 의미하며, 'O'는 구멍의 위치를 의미한다. 'R'은 빨간 구슬의 위치, 'B'는 파란 구슬의 위치이다.

입력되는 모든 보드의 가장자리에는 모두 '#'이 있다. 구멍의 개수는 한 개 이며, 빨간 구슬과 파란 구슬은 항상 1개가 주어진다.

출력

최소 몇 번 만에 빨간 구슬을 구멍을 통해 빼낼 수 있는지 출력한다. 만약, 10번 이하로 움직여서 빨간 구슬을 구멍을 통해 빼낼 수 없으면 -1을 출력한다.

풀이

문제 풀이의 컨셉부터 재밌는 문제였다. 보드를 기울이면 구슬이 더 이상 갈 수 없을 때까지 또르르 굴러간다. ㅋㅋ 이때 빨간 구슬이 파란 구슬보다 먼저 구멍에 빠져야 성공이다. 보드를 움직이는 횟수를 구해보자.

입력받기

st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken());
M = Integer.parseInt(st.nextToken());

map = new char[N][M];
String line;
for (int i = 0; i < N; i++) {
    line = br.readLine();
    for (int j = 0; j < M; j++) {
        map[i][j] = line.charAt(j);

        if(map[i][j] == 'O') {
            goal = new int[]{i, j};
        } else if (map[i][j] == 'R') {
            red = new int[]{i, j};
        } else if (map[i][j] == 'B') {
            blue = new int[]{i, j};
        }
    }
}

전체 map을 정성스럽게 한칸한칸 채워주자. 이때, 빨간 구슬의 처음 위치, 파란 구슬의 처음 위치, 구멍의 위치를 기록하자.

DFS

static int solution() {
    int depth = 0;
    int[] nowRed = Arrays.copyOf(red, 2);
    int[] nowBlue = Arrays.copyOf(blue, 2);

    dfs(depth, nowRed, nowBlue);

    return minDepth == Integer.MAX_VALUE ? -1 : minDepth;
}

그 후 dfs 응용으로 넘어간다. 기존 좌표평면 상에 대입해서 풀던 문제와는 조금 다른 컨셉으로 접근해야 한다. 아래 응용 DFS 코드 흐름을 참고하자.

    static void dfs(int depth, int[] nowRed, int[] nowBlue) {
        // 실패 or 성공 조건

       // 4가지 방향 고려
        for (int d = 0; d < 4; d++) {
            
            // 두 구슬의 다음 위치가 벽이면 continue

            // Red부터 굴리기
            while (map[nextRedY][nextRedX] != '#') {

                // goal이면 멈추기
            }
            // Blue 굴리기
            while (map[nextBlueY][nextBlueX] != '#') {
            
                // goal이면 멈추기
            }

            // 두 구슬의 위치가 겹쳤다면
            if (nextRedY == nextBlueY && nextRedX == nextBlueX) {
                
            }
            int[] nextRed = new int[]{nextRedY, nextRedX};
            int[] nextBlue = new int[]{nextBlueY, nextBlueX};

            // 두 구슬 모두 위치 변화가 없으면 의미없는 과정이므로 continue

            // 다음 dfs 수행
            dfs(depth + 1, nextRed, nextBlue);
        }
    }

실패 or 성공 조건

// 실패 조건
if (depth > 10) {
    return;
} else if (Arrays.equals(goal, nowBlue)) {
    return;
}
// 성공 조건
if (Arrays.equals(goal, nowRed)) {
    minDepth = Math.min(minDepth, depth);
    return;
}

구슬은 10번 이하로 움직여서 빨간 구슬을 빼낼 수 없으면 그냥 return 한다. 또한 어떠한 경우에도 파란 구슬의 위치가 구멍과 일치하면 return 한다.

실패 조건 아래 다른 조건 분기로 성공 조건을 작성한다.

빨간 구슬의 위치가 구멍과 일치하면 해당 dfs의 depth를 비교하고 결과(minDepth)에 반영한다.

빨간 구슬 & 파란 구슬 굴리기

// Red 굴리기
while (map[nextRedY][nextRedX] != '#') {
    if (map[nextRedY + dy[d]][nextRedX + dx[d]] == '#') {
        break;
    }
    nextRedY += dy[d];
    nextRedX += dx[d];

    // goal이면 멈추기
    if (goal[0] == nextRedY && goal[1] == nextRedX) {
        break;
    }
}

구슬이 벽을 만나기 전까지 보드를 기울인 방향(d)으로 구슬을 움직인다. 이때 구슬의 충돌은 고려하지 않는다. ← 계산이 좀 복잡해짐.

두 구슬의 위치가 겹치면?

// 두 구슬의 위치가 겹쳤다면
if (nextRedY == nextBlueY && nextRedX == nextBlueX) {
    // goal에서 만난거면 continue
    if (goal[0] == nextRedY && goal[1] == nextRedX) {
        continue;
    }
    // d == 0 아래, d == 1 위, d == 2 왼, d == 3 오
    else if (d == 0) {
        // red가 아래있었으면
        if (nowRed[0] > nowBlue[0]) {
            nextBlueY--;
        } else {
            nextRedY--;
        }
    } else if (d == 1) {
        // red가 위에 있었으면
        if (nowRed[0] < nowBlue[0]) {
            nextBlueY++;
        } else {
            nextRedY++;
        }
    } else if (d == 2) {
        // red가 왼쪽에 있었으면
        if (nowRed[1] < nowBlue[1]) {
            nextBlueX++;
        } else {
            nextRedX++;
        }
    } else if (d == 3) {
        // red가 오른쪽에 있으면
        if (nowRed[1] > nowBlue[1]) {
            nextBlueX--;
        } else {
            nextRedX--;
        }
    }
}

두 구슬을 서로의 간섭을 고려하지 않고 굴렸다면 구슬이 겹쳐있는 예외상황을 고려해야 한다. 이때 goal에서 서로 만난 것이라면 그냥 return 한다. 어차피 파란 구슬이 구멍에 들어가면 안 된다.

그다음 조건 분기가 조금 많다. 각자 방향에 대해서 어떤 구슬이 앞에 있었는지를 고려하여 기울인 방향 뒤에 있던 구슬의 위치를 한 칸 뒤로 옮겨준다.

직접 그렸다. 정성을 보았다면 공감을 눌러야겠지?

전체 코드

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

public class Main {

    static int[] dx = {0, 0, -1, 1};
    static int[] dy = {1, -1, 0, 0};

    static int N, M;
    static char[][] map;
    static int[] goal, red, blue;
    static int minDepth = Integer.MAX_VALUE;

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

        st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        map = new char[N][M];
        String line;
        for (int i = 0; i < N; i++) {
            line = br.readLine();
            for (int j = 0; j < M; j++) {
                map[i][j] = line.charAt(j);

                if(map[i][j] == 'O') {
                    goal = new int[]{i, j};
                } else if (map[i][j] == 'R') {
                    red = new int[]{i, j};
                } else if (map[i][j] == 'B') {
                    blue = new int[]{i, j};
                }
            }
        }

        int result = 0;
        result = solution();
        System.out.println(result);
    }

    static int solution() {
        int depth = 0;
        int[] nowRed = Arrays.copyOf(red, 2);
        int[] nowBlue = Arrays.copyOf(blue, 2);

        dfs(depth, nowRed, nowBlue);

        return minDepth == Integer.MAX_VALUE ? -1 : minDepth;
    }

    static void dfs(int depth, int[] nowRed, int[] nowBlue) {
        // 실패 조건
        if (depth > 10) {
            return;
        } else if (Arrays.equals(goal, nowBlue)) {
            return;
        }
        // 성공 조건
        if (Arrays.equals(goal, nowRed)) {
            minDepth = Math.min(minDepth, depth);
            return;
        }

        for (int d = 0; d < 4; d++) {
            int nextRedY = nowRed[0];
            int nextRedX = nowRed[1];

            int nextBlueY = nowBlue[0];
            int nextBlueX = nowBlue[1];

            // 둘다 다음 위치가 벽이면 continue
            if (map[nextRedY + dy[d]][nextRedX + dx[d]] == '#' &&
                    map[nextBlueY + dy[d]][nextBlueX + dx[d]] == '#') {
                continue;
            }

            // Red부터 굴리기
            while (map[nextRedY][nextRedX] != '#') {
                if (map[nextRedY + dy[d]][nextRedX + dx[d]] == '#') {
                    break;
                }
                nextRedY += dy[d];
                nextRedX += dx[d];

                // goal이면 멈추기
                if (goal[0] == nextRedY && goal[1] == nextRedX) {
                    break;
                }
            }
            // Blue 굴리기
            while (map[nextBlueY][nextBlueX] != '#') {
                if (map[nextBlueY + dy[d]][nextBlueX + dx[d]] == '#') {
                    break;
                }
                nextBlueY += dy[d];
                nextBlueX += dx[d];

                // goal이면 멈추기
                if (goal[0] == nextBlueY && goal[1] == nextBlueX) {
                    break;
                }
            }

            // 두 구슬의 위치가 겹쳤다면
            if (nextRedY == nextBlueY && nextRedX == nextBlueX) {
                // goal에서 만난거면 continue
                if (goal[0] == nextRedY && goal[1] == nextRedX) {
                    continue;
                }
                // d == 0 아래, d == 1 위, d == 2 왼, d == 3 오
                else if (d == 0) {
                    // red가 아래있었으면
                    if (nowRed[0] > nowBlue[0]) {
                        nextBlueY--;
                    } else {
                        nextRedY--;
                    }
                } else if (d == 1) {
                    // red가 위에 있었으면
                    if (nowRed[0] < nowBlue[0]) {
                        nextBlueY++;
                    } else {
                        nextRedY++;
                    }
                } else if (d == 2) {
                    // red가 왼쪽에 있었으면
                    if (nowRed[1] < nowBlue[1]) {
                        nextBlueX++;
                    } else {
                        nextRedX++;
                    }
                } else if (d == 3) {
                    // red가 오른쪽에 있으면
                    if (nowRed[1] > nowBlue[1]) {
                        nextBlueX--;
                    } else {
                        nextRedX--;
                    }
                }
            }
            int[] nextRed = new int[]{nextRedY, nextRedX};
            int[] nextBlue = new int[]{nextBlueY, nextBlueX};

            // 두 구슬 모두 위치 변화가 없으면 continue
            if (Arrays.equals(nextRed, nowRed) && Arrays.equals(nextBlue, nowBlue)) {
                continue;
            }

            dfs(depth + 1, nextRed, nextBlue);
        }
    }

}

 

댓글