본문 바로가기
알고리즘/백준

16197번 - 두 동전(bfs)

by HWK 2024. 9. 26.

문제:

 

풀이:

최소 몇번 움직여야하는지 맞춰야 하는 문제이다.

이를 dfs로 풀게되면 모든 경우의 수를 다 살펴봐야하지만,
bfs로 풀면 가장 처음 목적지에 도착할때, 그 횟수를 반환해 줄 수 있다. 

 

동전이 움직일때 꼭 고려해야 하는 사항은 두개이다.

  1. 동전의 움직임을 기록하고, 중복된 위치에 동전이 있지 않게 해준다.
    1. 두 동전의 지금 위치를 기록한다.
      1  
      2  
    2. 두 동전의 위치를 바꿔서 기록한다.
      2  
      1  
    3. 두 동전이 각 동전의 자리에 한번에 모여있는 상황을 기록한다.
      1, 2  
         
  2. 두 동전이 동시에 판 밖으로 떨어지면 안된다.

또한 동전은 4방향으로 움직일 수 있으니, 4방향 탐색법을 이용하면 된다. 아래 static 메서드를 이용하도록 하자.

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

 

이번 풀이에서는 클래스를 보다 적극적으로 활용하도록 할 것이다.

 

이 문제에서 중요한 것은 판의 생김새, 동전의 움직임이다.

각각을 클래스로 분리해주도록 하자.

class Coin {
    int y;
    int x;

    Coin(int y, int x) {
        this.y = y;
        this.x = x;
    }

    public Coin move(int dy, int dx, Board board) {
        int nextY = y + dy;
        int nextX = x + dx;

        if (board.isOutOfBounds(nextY, nextX)) {
            return null; // 경계를 벗어나면 null 반환
        }

        if (board.isWall(nextY, nextX)) {
            return new Coin(y, x); // 벽을 만나면 이동하지 않음
        }

        return new Coin(nextY, nextX); // 새로운 위치 반환
    }
}

위 클래스는 동전의 위치, 움직임을 나타낸다.

move() 메서드에서는 Board 클래스를 이용해 받아온 정보를 적극 활용한다.

 

class Board {
    char[][] board;
    boolean[][][][] visited;
    int N, M;

    Board(char[][] board, int N, int M) {
        this.board = board;
        this.N = N;
        this.M = M;
        visited = new boolean[N][M][N][M];
    }

    boolean isOutOfBounds(int y, int x) {
        return y < 0 || y >= N || x < 0 || x >= M;
    }

    boolean isWall(int y, int x) {
        return board[y][x] == '#';
    }

    boolean isVisited(Coin coin1, Coin coin2) {
        return visited[coin1.y][coin1.x][coin2.y][coin2.x];
    }

    void markVisited(Coin coin1, Coin coin2) { // 중복 방지
        visited[coin1.y][coin1.x][coin2.y][coin2.x] = true;
        visited[coin2.y][coin2.x][coin1.y][coin1.x] = true;
        visited[coin1.y][coin1.x][coin1.y][coin1.x] = true;
        visited[coin2.y][coin2.x][coin2.y][coin2.x] = true;
    }
}

위 클래스에서는 판의 생김새, 그동안 동전이 이동한 기록을 저장한다.

각 메서드는 동전이 다음 위치로 이동할 수 있는지 판정하기 위해 사용된다.

 

이렇게 클래스를 이용해 동전의 위치, 판의 생김새, 그동안 동전이 이동한 기록을 알 수 있다면,

위 클래스들을 이용한 메서드를 작성할 수 있을 것이다.

static int dropCoin(Coin[] coins, Board board) {
    Queue<int[]> queue = new LinkedList<>();
    queue.add(new int[]{coins[0].y, coins[0].x, coins[1].y, coins[1].x, 0}); // 코인 위치, 이동 횟수 기록
    board.markVisited(coins[0], coins[1]); // 첫 위치 기록

    while (!queue.isEmpty()) {
        int[] now = queue.poll();
        int coin1Y = now[0];
        int coin1X = now[1];
        int coin2Y = now[2];
        int coin2X = now[3];
        int count = now[4];

        if (count == 10) {
            return -1;
        }

        for (int i = 0; i < 4; i++) {
            Coin nextCoin1 = new Coin(coin1Y, coin1X);
            Coin nextCoin2 = new Coin(coin2Y, coin2X);

            // 두 코인 모두 움직여봄
            nextCoin1 = nextCoin1.move(move[i][0], move[i][1], board);
            nextCoin2 = nextCoin2.move(move[i][0], move[i][1], board);

            if ((nextCoin1 == null && nextCoin2 != null )
                    || (nextCoin1 != null && nextCoin2 == null)) { // 둘 중 하나 떨어짐
                return count + 1;
            }

            if (nextCoin1 != null && nextCoin2 != null) { // 둘다 떨어지지 않은 경우
                if (!board.isVisited(nextCoin1, nextCoin2)) {
                    board.markVisited(nextCoin1, nextCoin2);
                    queue.add(new int[]{nextCoin1.y, nextCoin1.x, nextCoin2.y, nextCoin2.x, count + 1});
                }
            }
        }

    }
    return -1; // queue가 비면 -1 반환(경우의 수 없음)
}

위 메서드는 bfs와 Coin, Board 클래스를 이용하여 작성된 메서드이다. 작동 방식은 아래와 같다.

  1. 두 동전의 처음 위치 정보와, 이동 횟수(0)을 queue에 저장하고, 위치를 기록한다.
  2. 큐가 비기 전까지 아래 과정을 반복한다.
    1. queue.poll을 이용해, 동전의 위치, 이동 횟수를 가져온다.
    2. 만약 이동 횟수가 10번이라면 -1을 반환한다.
      bfs 특성상 queue에 저장된 깊이는 지금 보고있는 깊이보다 깊을 수 없기 때문이다.
    3. 두 코인을 각각 4방향으로 움직여본다.
    4. 움직여본 결과, 둘 중 하나가 떨어진다면, 지금 이동 횟수 + 1을 반환한다.
    5. 둘 다 떨어지지 않았고, 이미 이동한적 있는 위치가 아니라면,
      queue에 두 동전의 위치와 이동횟수 + 1을 저장해준다.
  3. 큐가 빌 동안 반환이 이뤄지지 않았다면, -1을 반환한다.
    두 동전중 한 동전만 먼저 떨어지는 경우는 없다는 말이다.

 

전체코드:

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

// Coin 클래스는 동전의 위치, 움직임을 담당
class Coin {
    int y;
    int x;

    Coin(int y, int x) {
        this.y = y;
        this.x = x;
    }

    public Coin move(int dy, int dx, Board board) {
        int nextY = y + dy;
        int nextX = x + dx;

        if (board.isOutOfBounds(nextY, nextX)) {
            return null; // 경계를 벗어나면 null 반환
        }

        if (board.isWall(nextY, nextX)) {
            return new Coin(y, x); // 벽을 만나면 이동하지 않음
        }

        return new Coin(nextY, nextX); // 새로운 위치 반환
    }
}

// Board 클래스는 판의 모양, 기록를 알려줌
class Board {
    char[][] board;
    boolean[][][][] visited;
    int N, M;

    Board(char[][] board, int N, int M) {
        this.board = board;
        this.N = N;
        this.M = M;
        visited = new boolean[N][M][N][M];
    }

    boolean isOutOfBounds(int y, int x) {
        return y < 0 || y >= N || x < 0 || x >= M;
    }

    boolean isWall(int y, int x) {
        return board[y][x] == '#';
    }

    boolean isVisited(Coin coin1, Coin coin2) {
        return visited[coin1.y][coin1.x][coin2.y][coin2.x];
    }

    void markVisited(Coin coin1, Coin coin2) { // 중복 방지
        visited[coin1.y][coin1.x][coin2.y][coin2.x] = true;
        visited[coin2.y][coin2.x][coin1.y][coin1.x] = true;
        visited[coin1.y][coin1.x][coin1.y][coin1.x] = true;
        visited[coin2.y][coin2.x][coin2.y][coin2.x] = true;
    }
}

public class Main {

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

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

        int N = Integer.parseInt(st.nextToken()); // 행
        int M = Integer.parseInt(st.nextToken()); // 열

        Coin[] coins = new Coin[2];
        char[][] board = new char[N][M];
        int coinIndex = 0;

        for (int i = 0; i < N; i++) {
            String line = br.readLine();
            for (int j = 0; j < M; j++) {
                board[i][j] = line.charAt(j);
                if (board[i][j] == 'o') { // 동전 발견 시 위치 저장
                    coins[coinIndex] = new Coin(i, j);
                    coinIndex++;
                }
            }
        }

        Board realBoard = new Board(board, N, M);

        System.out.println(dropCoin(coins, realBoard));
    }

    static int dropCoin(Coin[] coins, Board board) {
        Queue<int[]> queue = new LinkedList<>();
        queue.add(new int[]{coins[0].y, coins[0].x, coins[1].y, coins[1].x, 0}); // 코인 위치, 이동 횟수 기록
        board.markVisited(coins[0], coins[1]); // 첫 위치 기록

        while (!queue.isEmpty()) {
            int[] now = queue.poll();
            int coin1Y = now[0];
            int coin1X = now[1];
            int coin2Y = now[2];
            int coin2X = now[3];
            int count = now[4];

            if (count == 10) {
                return -1;
            }

            for (int i = 0; i < 4; i++) {
                Coin nextCoin1 = new Coin(coin1Y, coin1X);
                Coin nextCoin2 = new Coin(coin2Y, coin2X);

                // 두 코인 모두 움직여봄
                nextCoin1 = nextCoin1.move(move[i][0], move[i][1], board);
                nextCoin2 = nextCoin2.move(move[i][0], move[i][1], board);

                if ((nextCoin1 == null && nextCoin2 != null )
                        || (nextCoin1 != null && nextCoin2 == null)) { // 둘 중 하나 떨어짐
                    return count + 1;
                }

                if (nextCoin1 != null && nextCoin2 != null) { // 둘다 떨어지지 않은 경우
                    if (!board.isVisited(nextCoin1, nextCoin2)) {
                        board.markVisited(nextCoin1, nextCoin2);
                        queue.add(new int[]{nextCoin1.y, nextCoin1.x, nextCoin2.y, nextCoin2.x, count + 1});
                    }
                }
            }

        }
        return -1; // queue가 비면 -1 반환(경우의 수 없음)
    }

}

 

시행착오:

처음에는 클래스를 이용하지 않고 풀었고 아래와 같은 풀이가 나왔다.

더보기
import java.io.*;
import java.util.*;

public class Main {

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

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

        int N = Integer.parseInt(st.nextToken()); // 행
        int M = Integer.parseInt(st.nextToken()); // 열

        int[][] coins = new int[2][2];
        char[][] board = new char[N][M];
        int coinIndex = 0;

        for (int i = 0; i < N; i++) {
            String line = br.readLine();
            for (int j = 0; j < M; j++) {
                board[i][j] = line.charAt(j);
                if (board[i][j] == 'o') { // 동전 발견 시 위치 저장
                    coins[coinIndex][0] = i;
                    coins[coinIndex][1] = j;
                    coinIndex++;
                }
            }
        }

        System.out.println(dropCoin(coins, board, N, M));
    }

    static int dropCoin(int[][] coins, char[][] board, int N, int M) {
        boolean[][][][] visited = new boolean[N][M][N][M];
        Queue<int[]> queue = new LinkedList<>();
        queue.add(new int[]{coins[0][0], coins[0][1], coins[1][0], coins[1][1], 0});
        visited[coins[0][0]][coins[0][1]][coins[1][0]][coins[1][1]] = true;
        visited[coins[0][0]][coins[0][1]][coins[0][0]][coins[0][1]] = true;
        visited[coins[1][0]][coins[1][1]][coins[1][0]][coins[1][1]] = true;

        while (!queue.isEmpty()) {
            int[] now = queue.poll();
            int firstY = now[0];
            int firstX = now[1];
            int secondY = now[2];
            int secondX = now[3];
            int count = now[4];

            if (count == 10) {
                return -1;
            }
            
            for (int i = 0; i < 2; i++) {
                int nextFirstY = firstY + move[i][0];
                int nextSecondY = secondY + move[i][0];
                if (nextFirstY == N || nextFirstY < 0) {
                    if (nextSecondY < N && nextSecondY >= 0) {
                        return count + 1;
                    }
                    continue;
                } else if (nextSecondY == N || nextSecondY < 0) {
                    return count + 1;
                }
                if (board[nextFirstY][firstX] == '#') {
                    nextFirstY = firstY;
                }
                if (board[nextSecondY][secondX] == '#') {
                    nextSecondY = secondY;
                }
                if (!visited[nextFirstY][firstX][nextSecondY][secondX]) {
                    visited[nextFirstY][firstX][nextSecondY][secondX] = true;
                    visited[nextFirstY][firstX][nextFirstY][firstX] = true;
                    visited[nextSecondY][secondX][nextSecondY][secondX] = true;
                    queue.add(new int[]{nextFirstY, firstX, nextSecondY, secondX, count+1});
                }
            }
            for (int i = 2; i < 4; i++) {
                int nextFirstX = firstX + move[i][1];
                int nextSecondX = secondX + move[i][1];

                if (nextFirstX == M || nextFirstX < 0) {
                    if (nextSecondX < M && nextSecondX >= 0) {
                        return count + 1;
                    }
                    continue;
                } else if (nextSecondX == M || nextSecondX < 0) {
                    return count + 1;
                }
                if (board[firstY][nextFirstX] == '#') {
                    nextFirstX = firstX;
                }
                if (board[secondY][nextSecondX] == '#') {
                    nextSecondX = secondX;
                }
                if (!visited[firstY][nextFirstX][secondY][nextSecondX]) {
                    visited[firstY][nextFirstX][secondY][nextSecondX] = true;
                    visited[firstY][nextFirstX][firstY][nextFirstX] = true;
                    visited[secondY][nextSecondX][secondY][nextSecondX] = true;
                    queue.add(new int[]{firstY, nextFirstX, secondY, nextSecondX, count+1});
                }
            }

        }

        return -1;
    }

}

길이차이가 크지않고, 속도도 크게 다르지 않지만,
너무 하나의 메서드에 책임이 많다. 동전의 이동, 다음칸이 이동할 수 있는 칸인지, 동전 하나만 떨어졌는지를
모두 하나의 메서드에서 책임져야 한다.

이는 객체지향 설계의 원칙 중 단일 책임 원칙을 위배한다.

물론 당장의 속도차이는 나지 않지만, 만일 어떤 프로젝트에서 이런 코드를 작성하게 된다면,

유지보수성, 가독성이 떨어지게 될 것이다.

 

면접을 준비하며 느낀건데, 확실히 그동안 CS 공부가 부족했던 것 같다.

CS 공부를 병행하며 이론에 맞는 코드를 작성하는 습관을 들여야겠다.