[C++로 풀이] 블록 이동하기 (BFS, 해시 함수)⭐⭐⭐

Date:     Updated:

카테고리:

태그:

📌 블록 이동하기

난이도 ⭐⭐⭐

🚀 문제

image

image

image


🚀 내 풀이

문제는 일반적인 BFS 문제 풀듯이 풀면 된다. 그러나 주의 깊게 생각해야 할 부분은 다음과 같다.

  • 1️⃣ 로봇은 회전이 가능하다는 점! 현재 위치에서 로봇이 회전된 위치들 또한 큐에 삽입하여야 한다.
  • 2️⃣ 로봇은 두 파트로 이루어져 있기 때문에 2 개의 좌표를 차지한다. 따라서 방문 체크를 어떻게 할 것인지 신경 써야 한다. 로봇이 “두 파트 동시에 있었던 두 좌표”는 재방문하면 최단경로를 구할 수 없기 떄문에 재방문 하지 않도록 해야 한다.
    • 2 가지 방법으로 풀었다. 2 개의 코드 모두 다 살펴볼 것이다.
      1. 방향까지 고려한 3 차원 배열로 방문 체크하는 방법. 로봇의 두 파트는 왼쪽, 오른쪽 혹은 위쪽 아래쪽으로 ‘구분’할 수 가 있다. 따라서 방향까지 고려하여 두 파트 각각 좌표 방문 체크를 해주는 것.
      2. 두 파트(위치)를 로봇 구조체로 묶어 로봇 그 자체를 해시 테이블에 보관하여 방문 체크 하는 방법. 즉, 로봇 구조체 그 자체가 이미 해시테이블에 저장이 되어있는지 아닌지로 방문 체크를 한다.

특히 이.. ‘회전’을 구현하는 부분이 좀 까다로웠다.😥

image

image

  • 로봇이 1 초로 움직여 갈 수 있는 위치들
    • 4 가지 종류의 “이동” : 상, 하, 좌, 우
      • 로봇의 두 파트 모두가 갈 수 있는 곳이어야 한다.
    • 4 가지 종류의 “90도 회전” : 상, 하, 좌, 우
      • 가로 방향인 로봇의 경우 (90도 회전이므로 상하 회전만 가능)
        • 위로 회전하는 2 가지 경우 모두 위 두칸, 아래로 회전하는 2 가지 경우 모두 아래 두칸이 막혀있지 않아야 한다.
        • 회전하여 옮겨질 일부 한 파트가 회전하여 갈 예정인 곳도 갈 수 있는 곳이어야 한다.
      • 세로 방향인 로봇의 경우 (90도 회전이므로 좌우 회전만 가능)
        • 오른쪽으로 회전하는 2 가지 경우 모두 오른쪽 두 칸, 왼쪽으로 회전하는 2 가지 경우 모두 왼쪽 두 칸이 막혀있지 않아야 한다.
        • 회전하여 옮겨질 일부 한 파트가 회전하여 갈 예정인 곳도 갈 수 있는 곳이어야 한다.

최대 8 개의 로봇 위치를 큐에 삽입할 수 있다. 이 8 가지 후보를 로봇이 추후 갈 수 있는지 검사를 모두 진행해야 한다.


🔥 3 차원 배열로 방문 체크한 풀이 ⭕

#include <string>
#include <vector>
#include <queue>

using namespace std;

#define UP 0
#define DOWN 1
#define LEFT 2
#define RIGHT 3

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

// 하나의 위치 좌표
struct Pos {
    int y; // 행
    int x; // 열
    int dir; // 로봇의 중심에서 어느쪽에 있는지 그 방향 ⭐
};

// 로봇
struct Robot {
    Pos part1; // 로봇의 일부 1 의 위치와 방향
    Pos part2; // 로봇의 일부 2 의 위치와 방향
    int time;  // 현재까지 걸린 시간 (배열 안쓰고 구조체에 저장했다.)
};

// 두 part1, part2 위치가 board 에서 벗어난 곳은 아닌지, 벽은 아닌지 검사
// 즉 기본적으로 갈 수 있는 곳인지 검사
bool possible(vector<vector<int>> board, Pos part1, Pos part2) {
    // 두 위치 다 범위에서 벗어난 곳인지
    if (part1.y < 0 || part1.y >= N || part1.x < 0 || part1.x >= N ||
        part2.y < 0 || part2.y >= N || part2.x < 0 || part2.x >= N)
        return false;
    // 두 위치 다 벽은 아닌지
    if (board[part1.y][part1.x] == 1 ||
        board[part2.y][part2.x] == 1)
        return false;

    // 두 위치 모두 다 범위 내 + 벽이 아니면 true
    return true;
}

int solution(vector<vector<int>> board) {
    int answer = 0;
    N = board.size();
   
    queue<Robot> q;
    // 큐 삽입(예약) 체크 배열.
    // 로봇은 위치 좌표를 2 개를 쓰고, 또한 회전을 하기 때문에 방문 체크를 방향까지 더한 3차원 배열로 한다.  
    // 로봇을 이루는 두 위치 모두 따로따로 방문 체크를 진행할 것이다.
    bool checked[100][100][4] = { false }; 

    // 로봇의 시작 상태
    // 일부1 : (0, 0) 로봇의 왼쪽 파트
    // 일부2 : (0, 1) 로봇의 오른쪽 파트
    Robot start{ { 0, 0, LEFT }, { 0, 1, RIGHT }, 0 };
    checked[0][0][LEFT] = true; 
    checked[0][1][RIGHT] = true;
    q.push(start);

    while (!q.empty()) {
        // 방문
        Robot now = q.front();
        q.pop();

        // 로봇이 목적지에 도착했다면(로봇의 두 파트 중 하나라도 도착했다면)
        if (now.part1.y == N - 1 && now.part1.x == N - 1 || now.part2.y == N - 1 && now.part2.x == N - 1) {
            answer = now.time;
            break;
        }
        
        /* 예약 : 이동 */
        for (int i = 0; i < 4; ++i) {
            // 다음 방문 위치 후보 (로봇의 두 파트 각각의 위치)
            Pos next_part_1{ now.part1.y + dy[i], now.part1.x + dx[i], now.part1.dir };
            Pos next_part_2{ now.part2.y + dy[i], now.part2.x + dx[i], now.part2.dir };
            
            // 후보 검사
            if (!possible(board, next_part_1, next_part_2)) // 두 위치 모두 다 범위 내 & 벽이 아니어야 한다.
                continue;
            if (checked[next_part_1.y][next_part_1.x][next_part_1.dir] && checked[next_part_2.y][next_part_2.x][next_part_2.dir]) // 두 위치와 방향 모두 다 큐에 들어간적이 없어야 한다.
                continue;

            Robot next{ next_part_1, next_part_2, now.time + 1 };
            q.push(next);
            checked[next.part1.y][next.part1.x][next.part1.dir] = true; // 파트1 예약 체크
            checked[next.part2.y][next.part2.x][next.part2.dir] = true; // 파트2 예약 체크
        }

        /* 예약 : 회전 */
        if (now.part1.dir == LEFT) { // 현재 로봇이 가로로 놓여있다면
            // 1. 위로 회전 
            Pos up_left{ now.part1.y - 1, now.part1.x, UP };
            Pos up_right{ now.part2.y - 1, now.part2.x, UP };
            if (possible(board, up_left, up_right)) { // up_left, up_right 위쪽 두칸이 모두 위치 모두 다 범위 내 & 벽이 아니어야 한다. (회전할 때 가로막히면 안된다.)
                // 1-1. 왼쪽을 축으로 위로 회전
                if (!checked[up_left.y][up_left.x][UP] || !checked[now.part1.y][now.part1.x][DOWN]) { // 왼쪽 파트를 축으로 회전했을 때 다음 위치 up_left, now.part1(기존)
                    q.push({ up_left, { now.part1.y, now.part1.x, DOWN } , now.time + 1});
                    checked[up_left.y][up_left.x][UP] = true;
                    checked[now.part1.y][now.part1.x][DOWN] = true;
                }
                // 1-2. 오른쪽을 축으로 위로 회전
                if (!checked[up_right.y][up_right.x][UP] || !checked[now.part2.y][now.part2.x][DOWN]) { // 오른쪽 파트를 축으로 회전했을 때 다음 위치 up_right, now.part2(기존)
                    q.push({ up_right, { now.part2.y, now.part2.x, DOWN } , now.time + 1 });
                    checked[up_right.y][up_right.x][UP] = true;
                    checked[now.part2.y][now.part2.x][DOWN] = true;
                }
            }
            // 2. 아래로 회전
            Pos down_left{ now.part1.y + 1, now.part1.x, DOWN };
            Pos down_right{ now.part2.y + 1, now.part2.x, DOWN };
            if (possible(board, down_left, down_right)) {
                // 2-1. 왼쪽을 축으로 아래로 회전
                if (!checked[down_left.y][down_left.x][DOWN] || !checked[now.part1.y][now.part1.x][UP]) {
                    q.push({ { now.part1.y, now.part1.x, UP } , down_left, now.time + 1 });
                    checked[down_left.y][down_left.x][DOWN] = true;
                    checked[now.part1.y][now.part1.x][UP] = true;
                }
                // 2-2. 오른쪽을 축으로 아래로 회전
                if (!checked[down_right.y][down_right.x][DOWN] || !checked[now.part2.y][now.part2.x][UP]) {
                    q.push({ { now.part2.y, now.part2.x, UP } , down_right, now.time + 1 });
                    checked[down_right.y][down_right.x][DOWN] = true;
                    checked[now.part2.y][now.part2.x][UP] = true;
                }
            }
        }
        if (now.part1.dir == UP) { // 현재 로봇이 세로로 놓여있다면
            // 1. 왼쪽으로 회전
            Pos left_up{ now.part1.y, now.part1.x - 1, LEFT };
            Pos left_down{ now.part2.y, now.part2.x - 1, LEFT };
            if (possible(board, left_up, left_down)) {
                // 1-1. 위쪽을 축으로 왼쪽으로 회전
                if (!checked[left_up.y][left_up.x][LEFT] || !checked[now.part1.y][now.part1.x][RIGHT]) {
                    q.push({ left_up, { now.part1.y, now.part1.x, RIGHT } , now.time + 1 });
                    checked[left_up.y][left_up.x][LEFT] = true;
                    checked[now.part1.y][now.part1.x][RIGHT] = true;
                }
                // 1-2. 아래쪽을 축으로 왼쪽으로 회전
                if (!checked[left_down.y][left_down.x][LEFT] || !checked[now.part2.y][now.part2.x][RIGHT]) {
                    q.push({ left_down, { now.part2.y, now.part2.x, RIGHT } , now.time + 1 });
                    checked[left_down.y][left_down.x][LEFT] = true;
                    checked[now.part2.y][now.part2.x][RIGHT] = true;
                }
            }
            // 2. 오른쪽으로 회전
            Pos right_up{ now.part1.y, now.part1.x + 1, RIGHT };
            Pos right_down{ now.part2.y, now.part2.x + 1, RIGHT };
            if (possible(board, right_up, right_down)) {
                // 2-1. 위쪽을 축으로 오른쪽으로 회전
                if (!checked[right_up.y][right_up.x][RIGHT] || !checked[now.part1.y][now.part1.x][LEFT]) {
                    q.push({ { now.part1.y, now.part1.x, LEFT } , right_up, now.time + 1 });
                    checked[right_up.y][right_up.x][RIGHT] = true;
                    checked[now.part1.y][now.part1.x][LEFT] = true;
                }
                // 2-1. 아래쪽을 축으로 오른쪽으로 회전
                if (!checked[right_down.y][right_down.x][RIGHT] || !checked[now.part2.y][now.part2.x][LEFT]) {
                    q.push({ { now.part2.y, now.part2.x, LEFT } , right_down, now.time + 1 });
                    checked[right_down.y][right_down.x][RIGHT] = true;
                    checked[now.part2.y][now.part2.x][LEFT] = true;
                }
            }
        }
    }

    return answer;
}

image

image

3 차원 배열을 쓰지 않고 2 차원 배열, 즉 위치 좌표로만 방문 체크를 했다면 로봇이 위 그림에서처럼 1번, 2번 이렇게 움직였다면 3 번은 이미 방문했던 곳으로 판단되어 로봇이 3 번 위치로 이동하지 못한다. 실제로 가능해야하는데도 말이다. 3 번의 두 일부분이 각각 1번, 2번에 의해 방문 처리가 되었기 때문이다. 따라서 배열로 방문 체크를 하기 위해선 “방향” 같이 체크해야할 정보가 더 필요하다. 3 차원 배열을 썼다면 1 번에서는 UP, 2 번에서도 UP 이었을테니 3 번에서 동일한 좌표가 각각 LEFT, RIGHT 방향일 떄는 방문된적이 없는 것이므로 로봇이 3 번 위치로 이동하는 것이 가능해진다.


🔥 해시를 사용하여 방문 체크한 풀이(+ 커스텀 해시함수) ⭕

#include <string>
#include <vector>
#include <unordered_set>
#include <queue>

using namespace std;

#define UP 0
#define DOWN 1
#define LEFT 2
#define RIGHT 3

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

// 하나의 위치 좌표
struct Pos {
    int y; // 행
    int x; // 열
    int dir; // 로봇의 중심에서 어느쪽에 있는지 그 방향 ⭐
};

// 로봇
struct Robot {
    Pos part1; // 로봇의 일부 1 의 위치와 방향
    Pos part2; // 로봇의 일부 2 의 위치와 방향
    int time;  // 현재까지 걸린 시간

    // 해시에 필요한 == 연산자 오버로딩
    bool operator == (const Robot& other) const {
        bool condition1 = part1.y == other.part1.y && part1.x == other.part1.x;
        bool condition2 = part2.y == other.part2.y && part2.x == other.part2.x;
        bool condition3 = part1.y == other.part2.y && part1.x == other.part2.x;
        bool condition4 = part2.y == other.part1.y && part2.x == other.part1.x;
        if (condition1 && condition2 || condition3 && condition4)
            return true;
        return false;
    }
};

// 커스텀 해시 함수
struct MyHash {
    size_t operator()(const Robot& robot) const {
        hash<int> hash_func; // C++ 에서 제공하는 기본 자료형 데이터에 대해 사용할 수 있는 해시함수
        return hash_func(robot.part1.y) ^ robot.part1.x ^ robot.part2.y + hash_func(robot.part2.x); // XOR과 C++ 제공 해시 함수 짬뽕.. 일단 이런 수준으로 해시 함수를 대충 만들어봤는데 다행히 통과는 하였다..
    }
};

// 두 part1, part2 위치가 board 에서 벗어난 곳은 아닌지, 벽은 아닌지 검사
// 즉 기본적으로 갈 수 있는 곳인지 검사
bool possible(vector<vector<int>> board, Pos part1, Pos part2) {
    // 두 위치 다 범위에서 벗어난 곳인지
    if (part1.y < 0 || part1.y >= N || part1.x < 0 || part1.x >= N ||
        part2.y < 0 || part2.y >= N || part2.x < 0 || part2.x >= N)
        return false;
    // 두 위치 다 벽은 아닌지
    if (board[part1.y][part1.x] == 1 ||
        board[part2.y][part2.x] == 1)
        return false;

    // 두 위치 모두 다 범위 내 + 벽이 아니면 true
    return true;
}

int solution(vector<vector<int>> board) {
    int answer = 0;
    N = board.size();

    queue<Robot> q;
    unordered_set<Robot, MyHash> checked; // ⭐방문 체크를 해시테이블로⭐

    // 로봇의 시작 상태
    // 일부1 : (0, 0) 로봇 중심의 왼쪽
    // 일부2 : (0, 1) 로봇 중심의 오른쪽
    Robot start{ { 0, 0, LEFT }, { 0, 1, RIGHT }, 0 };
    checked.insert(start); // ⭐ 방문 체크
    q.push(start);

    while (!q.empty()) {
        // 방문
        Robot now = q.front();
        q.pop();

        // 로봇이 목적지에 도착했다면(로봇의 두 파트 중 하나라도 도착했다면)
        if (now.part1.y == N - 1 && now.part1.x == N - 1 || now.part2.y == N - 1 && now.part2.x == N - 1) {
            answer = now.time;
            break;
        }

        /* 예약 : 이동 */
        for (int i = 0; i < 4; ++i) {
            Pos next_part_1{ now.part1.y + dy[i], now.part1.x + dx[i], now.part1.dir };
            Pos next_part_2{ now.part2.y + dy[i], now.part2.x + dx[i], now.part2.dir };
            Robot next{ next_part_1, next_part_2, now.time + 1 };

            if (!possible(board, next_part_1, next_part_2))
                continue;
            if (checked.find(next) != checked.end()) // ⭐ 방문한적이 없다면 (즉, 해시테이블에 없다면)
                continue;

            q.push(next);
            checked.insert(next); // ⭐ 방문 체크
        }

        /* 예약 : 회전 */
        if (now.part1.dir == LEFT) {
            Pos up_left{ now.part1.y - 1, now.part1.x, UP };
            Pos up_right{ now.part2.y - 1, now.part2.x, UP };
            if (possible(board, up_left, up_right)) {
                Robot next1{ up_left, { now.part1.y, now.part1.x, DOWN } , now.time + 1 };
                if (checked.find(next1) == checked.end()) {
                    q.push(next1);
                    checked.insert(next1);
                }
                Robot next2{ up_right, { now.part2.y, now.part2.x, DOWN } , now.time + 1 };
                if (checked.find(next2) == checked.end()) {
                    q.push(next2);
                    checked.insert(next2);
                }
            }
            Pos down_left{ now.part1.y + 1, now.part1.x, DOWN };
            Pos down_right{ now.part2.y + 1, now.part2.x, DOWN };
            if (possible(board, down_left, down_right)) {
                Robot next1{ { now.part1.y, now.part1.x, UP }, down_left, now.time + 1 };
                if (checked.find(next1) == checked.end()) {
                    q.push(next1);
                    checked.insert(next1);
                }
                Robot next2{ { now.part2.y, now.part2.x, UP } , down_right, now.time + 1 };
                if (checked.find(next2) == checked.end()) {
                    q.push(next2);
                    checked.insert(next2);
                }
            }
        }
        if (now.part1.dir == UP) {
            Pos left_up{ now.part1.y, now.part1.x - 1, LEFT };
            Pos left_down{ now.part2.y, now.part2.x - 1, LEFT };
            if (possible(board, left_up, left_down)) {
                Robot next1{ left_up, { now.part1.y, now.part1.x, RIGHT } , now.time + 1 };
                if (checked.find(next1) == checked.end()) {
                    q.push(next1);
                    checked.insert(next1);
                }
                Robot next2{ left_down, { now.part2.y, now.part2.x, RIGHT } , now.time + 1 };
                if (checked.find(next2) == checked.end()) {
                    q.push(next2);
                    checked.insert(next2);
                }
            }
            Pos right_up{ now.part1.y, now.part1.x + 1, RIGHT };
            Pos right_down{ now.part2.y, now.part2.x + 1, RIGHT };
            if (possible(board, right_up, right_down)) {
                Robot next1{ { now.part1.y, now.part1.x, LEFT } , right_up, now.time + 1 };
                if (checked.find(next1) == checked.end()) {
                    q.push(next1);
                    checked.insert(next1);
                }
                Robot next2{ { now.part2.y, now.part2.x, LEFT } , right_down, now.time + 1 };
                if (checked.find(next2) == checked.end()) {
                    q.push(next2);
                    checked.insert(next2);
                }
            }
        }
    }

    return answer;
}

image

첫 번째 풀이처럼 로봇의 파트1, 파트2를 따로 따로 방문 체크하지 않고 Robot 구조체를 통째로 unordered_set 해시 테이블에 넣고 해시값을 통하여 방문 체크를 한다.

// 로봇
struct Robot {
    Pos part1; // 로봇의 일부 1 의 위치와 방향
    Pos part2; // 로봇의 일부 2 의 위치와 방향
    int time;  // 현재까지 걸린 시간

    // 해시에 필요한 == 연산자 오버로딩
    bool operator == (const Robot& other) const {
        bool condition1 = part1.y == other.part1.y && part1.x == other.part1.x;
        bool condition2 = part2.y == other.part2.y && part2.x == other.part2.x;
        bool condition3 = part1.y == other.part2.y && part1.x == other.part2.x;
        bool condition4 = part2.y == other.part1.y && part2.x == other.part1.x;
        if (condition1 && condition2 || condition3 && condition4)
            return true;
        return false;
    }
};

// 커스텀 해시 함수
struct MyHash {
    size_t operator()(const Robot& robot) const {
        hash<int> hash_func; // C++ 에서 제공하는 기본 자료형 데이터에 대해 사용할 수 있는 해시함수
        return hash_func(robot.part1.y) ^ robot.part1.x ^ robot.part2.y + hash_func(robot.part2.x); // XOR과 C++ 제공 해시 함수 짬뽕.. 일단 이런 수준으로 해시 함수를 대충 만들어봤는데 다행히 통과는 하였다..
    }
};

두 위치의 x, y 총 4 개의 파라미터가 전부 일치해야 같은 로봇으로 인식될 수 있도록(동일한 해시값을 도출하도록) 하였다. 이렇게 해시 함수와 == 연산자 오버로딩을 만들어두고 방문 체크를 하면 된다! 고유한 해시값을 도출할 수 있게끔 해시 함수를 잘 만드는 방법에 대해선 나중에 공부를 한번 해봐야할 것 같다.

[STL 컨테이너] set, unordered_set (+ map)에 사용자 정의 구조체 혹은 객체 담기



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Programmers 카테고리 내 다른 글 보러가기

댓글 남기기