[C++로 풀이] 경주로 건설 (DFS, BFS, 다익스트라)⭐⭐⭐

Date:     Updated:

카테고리:

태그:

📌 경주로 건설

난이도 ⭐⭐⭐

🚀 문제

image

image

image

image

image


🚀 내 풀이

🔥 1 차 풀이 ❌ (최소의 코너 횟수를 구해야하는걸로 착각한 풀이)

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

using namespace std;

struct Pos {
    int x;
    int y;
    int corner; // (y, x) 를 방문 했을 때 여태까지의 최소 코너 횟수
    Pos() { }
    Pos(int _y, int _x, int _c) { y = _y; x = _x; corner = _c; }
};

struct cmp {
    bool operator()(const Pos& a, const Pos& b) {
        return a.corner > b.corner; // min heap
    }
};

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

    // 시계방향 : 상->우->하->좌
    int deltaY[4] = { -1, 0, 1, 0 };
    int deltaX[4] = { 0, 1, 0, -1 };
    
    vector<vector<bool>> visited(n, vector<bool>(n)); // 방문 체크
    vector<vector<int>> dist(n, vector<int>(n)); // 각 정점마다 최소 횟수의 코너를 가졌을 때의 거리 (from 출발지)
    vector<vector<int>> shortestCorner(n, vector<int>(n, 625)); // 각 정점마다 최소 횟수의 코너
    vector<vector<Pos>> parent(n, vector<Pos>(n)); // 각 정점의 바로 윗 부모 (이전 정점)

    priority_queue<Pos, vector<Pos>, cmp> pq;

    /* 출발지 예약 */
    pq.push(Pos(0, 0, 0));
    dist[0][0] = 0;
    shortestCorner[0][0] = 0;
    parent[0][0] = Pos(0, 0, 0);

    while (!pq.empty()) {
        // 방문
        Pos now = pq.top();
        pq.pop();

        // 다익스트라에서 필요한 과정. (정점의 최소 코너 횟수를 업데이트할 때 이미 우선순위큐에 들어있는 정점은 수정할 수가 없으므로 똑같은 정점을 또 넣는다. 최적해를 가진 똑같은 정점은 이미 방문 되었을 것이므로 이렇게 방문 체크를 해주어 똑같지만 최적해는 아닌 다른 정점을 걸러주어야 한다.)
        // 더 자세한 설명은 <배달>문제 참고하기 https://ansohxxn.github.io/programmers/111/
        if (visited[now.y][now.x]) 
            continue;
        visited[now.y][now.x] = true;

        // 예약 (현재 방문한 정점을 기준으로한 상하좌우 4가지 방향에 대해)
        for (int i = 0; i < 4; ++i) {
            
            Pos next(now.y + deltaY[i], now.x + deltaX[i], 0); // 예약 후보 정점

            if (next.y < 0 || next.y >= n || next.x < 0 || next.x >= n) // 범위를 벗어난다면 예약될 수 없는 정점
                continue;
            if (board[next.y][next.x] == 1) // 벽이라 갈 수 없는 곳이면 예약될 수 없는 정점
                continue;
            if (visited[next.y][next.x]) // 이미 방문된 정점이라면 예약될 수 없는 정점
                continue;

            int numOfCorner = shortestCorner[now.y][now.x]; // 현재 방문 정점까지의 최소 코너 횟수
            if (now.y != 0 || now.x != 0) // 출발 정점이 아니고
                if ((next.x == now.x && now.x != parent[now.y][now.x].x) || (next.y == now.y && now.y != parent[now.y][now.x].y)) // 예약 후보 정점은 자신의 부모인 현재 방문 정점과 x 혹은 y 가 같지만 현재 방문 정점은 그의 부모와 x 혹은 y 가 다르면 코너가 발생한다고 판단했다.
                    numOfCorner++; 

            if (shortestCorner[next.y][next.x] < numOfCorner)  // 업뎃할 필요가 없으면 그냥 넘어감
                continue;

            // 업뎃하고 새롭게 예약
            next.corner = numOfCorner;
            pq.push(next);
            dist[next.y][next.x] = dist[now.y][now.x] + 1;
            shortestCorner[next.y][next.x] = numOfCorner;
            parent[next.y][next.x] = now;
        }
    }

    answer = shortestCorner[n - 1][n - 1] * 500 + dist[n - 1][n - 1] * 100;
    return answer;
}

문제를 좀 더 제대로 읽을걸 너무 대충 읽었나보다.. (반성) 다익스트라로 문제를 풀긴 했는데 “코너 횟수가 최소인 경로”를 만드는 기준으로 풀이한 틀린 풀이이다. 왜 이렇게 생각을 했을까.. 😭 이렇게 풀이해놓고는 문제를 다시 읽어보고 아차 싶었다.. 코너 건설 비용이 제일 비싸므로 코너를 최소로 하는 경로도 어느정도 총 건설 비용을 최소로 하는데에 상관관계가 있을 수는 있지만 정확한 인과관계로 이어지는 것은 아니다. 이 문제는 총 건설 비용을 최소로 해야하는 문제이지 코너를 최소로 하는 경로를 구하는 문제가 아니다. 코너 횟수를 최소롷 하는 경로보다 코너 좀 여러개인 경로가 더 저렴할 수도 있는 것이다!


🔥 2 차 풀이 ⭕ (최소 “금액”이 되는 경로)

1️⃣ 다익스트라를 사용한 풀이 (우선순위큐)

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

using namespace std;

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

struct Pos { // 정점
    int y, x, cost, dir; // 행, 열, 현재까지의 최소 금액, 바라보고 있는 방향
};

struct cmp {
    bool operator()(const Pos& a, const Pos& b) {
        return a.cost > b.cost;
    }
};

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

    // 상하좌우
    int deltaY[4] = { -1, 1, 0, 0 };
    int deltaX[4] = { 0, 0, -1, 1 };

    // 하나의 y, x 위치에 4 가지 바라보는 방향 저장. 하나의 위치마다 바라보는 방향별 최소 건설비용이 다를 수 있으므로! 
    // 2가지 정보 저장 (위치 + 바라보는 방향)
    int cost[25][25][4]; 
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            for (int k = 0; k < 4; ++k)
                cost[i][j][k] = INF; // 최소 비용을 업뎃할 것이므로 최대값으로 초기화
    for (int i = 0; i < 4; ++i)
        cost[0][0][i] = 0;
    
    priority_queue<Pos, vector<Pos>, cmp> pq;
    pq.push({ 0, 0, 0, -1}); // 출발지가 바라보는 방향은 UP DOWN RIGHT LEFT 어디에도 해당하지 않도록 -1 로 넘긴다. 오른쪽, 아래 두 정점 다 코너로 판단되지 않고 100이 될 수 있게.
    //if (board[1][0] == 0) pq.push({ 1, 0, 100, DOWN});
    //if (board[0][1] == 0) pq.push({ 0, 1, 100, RIGHT });

    while (!pq.empty()) {
        // 방문
        Pos now = pq.top();
        pq.pop();

        /* 
        방문 체크 안해줫음 그냥 (다익스트라 굳이 방문체크 안해줘도 답 도출에 지장없다.)
        근데 해주면 for문을 안들어가도 되니까 밑에 코너 계산, 비용 계산 안해도 되니까 성능상 조금은 유리하지 않을까 싶다.
        if (visited[now.y][now.x][now.dir])
            continue;
        */

        // 예약
        for (int i = 0; i < 4; ++i) {

            int nextY = now.y + deltaY[i];
            int nextX = now.x + deltaX[i];
            int nextDir = i;
            int nextCost = now.cost;

            // 범위를 벗어난 곳이라면 예약 불가
            if (nextY < 0 || nextY >= n || nextX < 0 || nextX >= n || board[nextY][nextX] == 1)
                continue;

            // 비용 계산 (코너가 아니라면 100, 코너라면 600)
            nextCost += 100;
            if (now.dir == UP || now.dir == DOWN) 
                if (nextDir == LEFT || nextDir == RIGHT)
                    nextCost += 500;
            if (now.dir == LEFT || now.dir == RIGHT) 
                if (nextDir == UP || nextDir == DOWN)
                    nextCost += 500;

            // 🤍현재 바라보는 방향에서의 여태 구한 최소비용보다 작으면 예약🤍
            if (nextCost < cost[nextY][nextX][nextDir]) {
                cost[nextY][nextX][nextDir] = nextCost;
                pq.push({ nextY, nextX, nextCost, nextDir });
            }
        }
    }

    // 목적지의 4 가지 방향별 최소비용 중 가장 작은 값을 선택하면 그게 바로 정답
    // cost[n-1][n-1][0], cost[n-1][n-1][1], cost[n-1][n-1][2], cost[n-1][n-1][3] 중 가장 작은 것.
    answer = *min_element(cost[n - 1][n - 1], cost[n - 1][n - 1] + 4);
    return answer;
}

이 문제 풀이에 있어 가장 중요한 포인트

  • 이미 방문한 정점이라도 재방문이 가능해야 한다.
    • 왜냐하면 같은 정점이라도 바라보는 방향, 즉 어느 방향에서 왔느냐에 따라 최소 비용이 달라지기 때문이다.
  • 내가 온 방향을 기억하고 있어야 한다. 👉 코너 때문에 내가 어느 방향을 바라보고 있느냐에 따라 비용이 완전히 달라지기 떄문이다.
    • 방문 정점(부모)이 현재 어느 방향을 바라보고 있느냐에 따라 예약이 진행될 그 방문 정점의 4 가지 방향에 있는 자식 정점들의 비용이 결정된다.
      • 방문 정점(부모)이 수평 방향을 바라보고 있다면 좌, 우 정점(자식)은 코너가 되므로 추가 비용이 +500 붙게 된다.
    • 왜냐하면 내가 지금 바라보고 있는 방향에 따라 (내가 어느 방향에서 왔느냐)에 따라 "앞으로의" 최소 비용이 달라질 수 있기 때문이다.
      • 즉, 하나의 정점이 바라볼 수 있는 방향은 4 가지 이니 하나의 정점마다 바라보는 방향별로 4 가지 종류의 최단 경로를 가지고 있어야 한다는 의미기도 하다.
        • 방향을 저장하지 않고, 즉 방향별로 최단 경로를 업데이트하지 않고 일반 다익스트라 최단경로 구하듯이 정점 좌표가지고만 (cost[y][x] 2차원 배열만 사용하여) 각각의 정점의 최단 경로를 업뎃하면 내가 바라보는
    • 따라서 y,x 좌표뿐만아니라 내가 바라보는 방향도 저장하는 3 차원 배열을 사용해야 한다.

image

정점마다 바라보는 방향 4 가지 별로 최소 비용을 따로 저장해야 하는 이유 (4 가지)

현재 도로 상태가 위와 같다고 가정해보자. 노란색은 도로, 회색은 갈 수 없는 벽이다. 정점 별로 방향을 저장하지 않고 cost[y][x] 이렇게 좌표에만 최소 비용을 기록했다고 가정해보자.

  • 초록색 경로를 통하여 (4, 4), (5, 4), 그리고 목적지인 (5, 5)가 각각 2100, 2700, 3300 최소 비용이 저장되어 있는 상태가 될 것이다.
  • 이 상태에서 보라색 경로(바라보는 방향:👇)를 통하여 최소비용이 2300인 (4, 4) 를 예약하려고 하는데 이미 방문된 정점이고 2100 < 2300 이기 때문에 예약 되지 않는다. 즉 해당 보라색 경로는 (4, 4) 이상 진행되지 않고 (3, 4) 에서 끊겨버린다.
    • 그러나! 이 보라색 경로를 따른다면 2300 - 2400 - 3000 이 되어 사실 초록 경로보다 목적지의 최소비용을 더 작은 값(3300보다 작은 3000) 으로 업데이트 할 수 있었다. 근데 일단 현재 좌표의 최소 비용만 따져서 나중에 방문할지 말지를 결정하기 때문에 보라색 경로는 (2100 < 2300)에 막혀 더 이상 진행되지 못하는 것이다.
    • 이와 같은 원인은 바라보는 방향은 따지지 않고 무작정 좌표의 최소비용만 따져 비교했기 때문이다. 이 문제는 “코너”가 있기 때문에 바라보는 방향에 따라 앞으로의 비용이 완전히 바뀔 수 있다. 더 작은 비용을 발견할 수도 있다.
      • 그렇기 떄문에 하나의 좌표라도 바라 보는 방향(내가 예약될 때 부모의 상하좌우 중 어디에 있었는지)이 4 가지가 있을 수 있으므로 이 좌표가 바라보고 있는 방향이 어디냐에 따라 이것과 연결된 이후 정점들의 최소 비용이 앞으로도 달라질 것이라는 것을 의미한다. 따라서 방향을 cost 에 함께 3차원 배열로 저장한다!

image

이렇게 모든 정점마다 바라보고 있는 방향(= 내가 온 방향) 4 가지별로 최소비용을 저장해야 한다! 목적지는 4 가지의 방향별 최소비용 가장 최소값을 선택하면 그게 바로 정답이 될 것이다.


✈ 준비
#define INF 999999
#define UP 0
#define DOWN 1
#define LEFT 2
#define RIGHT 3

struct Pos { // 정점
    int y;  // 행
    int x;  // 열
    int cost; // 이 정점이 예약됐던 시점까지 최소 금액
    int dir; // 이 정점이 예약됐던 시점에 바라보고 있었던 방향
};

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

    // 상하좌우
    int deltaY[4] = { -1, 1, 0, 0 };
    int deltaX[4] = { 0, 0, -1, 1 };


✈ 출발지 처리
    int cost[25][25][4]; 
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            for (int k = 0; k < 4; ++k)
                cost[i][j][k] = INF; // 최소 비용을 업뎃할 것이므로 최대값으로 초기화
    for (int i = 0; i < 4; ++i) // 출발지 4 방향 0 으로 초기화
        cost[0][0][i] = 0;

    priority_queue<Pos, vector<Pos>, cmp> pq;
    pq.push({ 0, 0, 0, -1}); // 출발지가 바라보는 방향은 UP DOWN RIGHT LEFT 어디에도 해당하지 않도록 -1 로 넘긴다. 오른쪽, 아래 두 정점 다 코너로 판단되지 않고 100이 될 수 있게.
    //if (board[1][0] == 0) pq.push({ 1, 0, 100, DOWN});
    //if (board[0][1] == 0) pq.push({ 0, 1, 100, RIGHT });
  • cost[y][x][dir] : (y, x) 정점이 dir 방향을 바라보고 있을 떄의 최소비용
  • pq : 우선순위 큐 (정점 구조체의 cost 멤버가 가장 작은게 먼저 빠져나온다.)
  • 모든 정점은 최대값에서 시작해야 한데 (그래야 최소비용으로 업데이트가 될테니까)
  • 출발지의 비용은 4 방향 모두 0 으로 초기화 한다.
    • 출발지가 새로운 최소 비용으로 업데이트 되면 안되니까 가장 작은 비용으로..


✈ 예약
        for (int i = 0; i < 4; ++i) {

            int nextY = now.y + deltaY[i];
            int nextX = now.x + deltaX[i];
            int nextDir = i;  
            int nextCost = now.cost; // 현재 이 정점까지의 최소 비용(now 의 비용). 여기서 +100 이되거나 +600 이 되는데 예약 정점의 최소 비용이 됨

            // 범위를 벗어난 곳이라면 예약 불가
            if (nextY < 0 || nextY >= n || nextX < 0 || nextX >= n || board[nextY][nextX] == 1)
                continue;

            // 비용 계산 (코너가 아니라면 +100, 코너라면 +600)
            nextCost += 100;
            if (now.dir == UP || now.dir == DOWN) // 방문 정점(부모)가 수직방향을 바라보고 있을 때
                if (nextDir == LEFT || nextDir == RIGHT) // 예약 후보 정점이 수평방향을 바라보고 있다면 코너
                    nextCost += 500;
            if (now.dir == LEFT || now.dir == RIGHT) // 방문 정점(부모)가 수평방향을 바라보고 있을 때
                if (nextDir == UP || nextDir == DOWN) // 예약 후보 정점이 수직방향을 바라보고 있다면 코너
                    nextCost += 500;

            // 🤍현재 바라보는 방향에서의 여태 구한 최소비용보다 작으면 예약🤍
            if (nextCost < cost[nextY][nextX][nextDir]) {
                cost[nextY][nextX][nextDir] = nextCost;
                pq.push({ nextY, nextX, nextCost, nextDir });
            }
        }


✈ 목적지의 최소 비용
answer = *min_element(cost[n - 1][n - 1], cost[n - 1][n - 1] + 4);

목적지에 저장된 바라보는 방향 4 가지 별 최소비용 중에서 가장 작은 것을 선택하면 된다.

  • *min_element(cost[n - 1][n - 1], cost[n - 1][n - 1] + 3) 처음엔 이렇게 3 으로 썼어서 틀린 부분 찾느라 얼마나 고민했는지 ㅠㅠ..
    • 내 풀이가 틀린건 줄 알고 계속 뜯어고치고 고민해보고 했었는데 계속 틀렸던 이유는 이 코드에서 내가 cost[n - 1][n - 1] + 3 으로 적었기 때문이었다..
      • 두 번째 파라미터는 최소값을 찾는 범위에 포함되지 않는다!!!!! \min_element(cost[n - 1][n - 1], cost[n - 1][n - 1] + 4) 이렇게 + 4 로 써야한다..


2️⃣ BFS 를 사용한 풀이 (그냥 큐)

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

using namespace std;

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

struct Pos {
    int y, x, cost, dir;
};

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

    int deltaY[4] = { -1, 1, 0, 0 };
    int deltaX[4] = { 0, 0, -1, 1 };

    int cost[25][25][4];
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            for (int k = 0; k < 4; ++k)
                cost[i][j][k] = INF;
    for (int i = 0; i < 4; ++i)
        cost[0][0][i] = 0;
    
    queue<Pos> q;
    q.push({ 0, 0, 0, -1});
    //if (board[1][0] == 0) q.push({ 1, 0, 100, DOWN});
    //if (board[0][1] == 0) q.push({ 0, 1, 100, RIGHT });

    while (!q.empty()) {

        Pos now = q.front();
        q.pop();

        for (int i = 0; i < 4; ++i) {

            int nextY = now.y + deltaY[i];
            int nextX = now.x + deltaX[i];
            int nextDir = i;
            int nextCost = now.cost;

            if (nextY < 0 || nextY >= n || nextX < 0 || nextX >= n || board[nextY][nextX] == 1)
                continue;

            nextCost += 100;
            if (now.dir == UP || now.dir == DOWN) 
                if (nextDir == LEFT || nextDir == RIGHT)
                    nextCost += 500;
            if (now.dir == LEFT || now.dir == RIGHT) 
                if (nextDir == UP || nextDir == DOWN)
                    nextCost += 500;

            if (nextCost < cost[nextY][nextX][nextDir]) {
                cost[nextY][nextX][nextDir] = nextCost;
                q.push({ nextY, nextX, nextCost, nextDir });
            }
        }
    }

    answer = *min_element(cost[n - 1][n - 1], cost[n - 1][n - 1] + 4);
    return answer;
}
✔ 그냥 일반큐 사용하여 BFS 로 풀어도 상관 없다.

위 풀이는 위위 풀이에서 우선순위큐를 그냥 일반 큐로 바꾼 것 말고는 차이가 없다.

  • 어차피 정점마다 4가지 방향별로 최소비용을 4 개씩이나 저장을 하기 때문에 그냥 BFS 방식으로 가장 먼저 들어간 것이 제일 먼저 방문되는 일반 큐를 사용해도 결과는 똑같았다. 방향별 최소비용이 다 저장이 되서 그런듯 하다. 우선순위큐 사용할 이유가 없었는지도 모른다.
  • 큐는 자연스럽게 줄어들 수 밖에 없다. 각각 x,y,dir 그 위치의 4가지 방향에 대해 최소값만 남게되면 더 이상 큐에 푸쉬가 안되기 때문이다.


3️⃣ DFS 를 사용한 풀이

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

using namespace std;

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

int deltaY[4] = { 1, 0, -1, 0 };
int deltaX[4] = { 0, 1, 0, -1 };
int n = 0;
int minDest = INF; // 현재까지 찾은 출발지->목적지 까지의 최~~~소의 비용
int cost_4Dir[25][25][4];

void DFS(vector<vector<int>>& board, int y, int x, int curCost, int dir) {
    // 현재 방문 정점 (y, x) 에 방향은 dir, 그리고 현재까지의 비용은 curCost

    /* 시간 효율성 높이는 기능! */
    // return 조건 1 (더 이상 이 경로는 검사 X ) : 현재까지 찾은 최소값보다 더 커질 때! 더 이상 검사할 필요가 없음
    if (minDest < curCost)
        return;
    // return 조건 2 (더 이상 이 경로는 검사 X ) : 목적지에 도착했을 떄
    if (y == n - 1 && x == n - 1) {
        minDest = min(curCost, minDest);
        return;
    }

    // 4가지 방향 중 다음 방문 후보 검사
    for (int i = 0; i < 4; ++i) {
        int nextY = y + deltaY[i];
        int nextX = x + deltaX[i];

        if (nextY < 0 || nextY >= n || nextX < 0 || nextX >= n || board[nextY][nextX] == 1)
            continue;

        int nextCost = curCost + 100;
        if (dir == UP || dir == DOWN)
            if (i == LEFT || i == RIGHT)
                nextCost += 500;
        if (dir == LEFT || dir == RIGHT)
            if (i == UP || i == DOWN)
                nextCost += 500;

        if (cost_4Dir[nextY][nextX][i] > nextCost) {
            cost_4Dir[nextY][nextX][i] = nextCost;
            DFS(board, nextY, nextX, nextCost, i);  // 바로 방문하러 ㄱㄱ
        }  
    }
}

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

    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            for (int k = 0; k < 4; ++k)
                cost_4Dir[i][j][k] = INF;

    DFS(board, 0, 0, 0, -1);

    answer = minDest;
    return answer;
}

image

위의 BFS 풀이랑 똑같이 풀면 된다. 큐에 예약하는 부분을 재귀로 DFS 함수를 호출하여 또 깊게 들어가는식으로 코드를 짜면 된다. DFS 에선 예약하지 않고 그냥 다음 방문할 후보를 보면 냅다 방문하러 들어가버리기 때문! 그리고 목적지 (n-1, n-1)에 도달하면 return 하여 되돌아오게끔 하면 된다.

    if (minDest < curCost)
        return;

다만 DFS 는 깊이 있는데로 다 들어가기 때문에 시간초과가 날 수도 있다!! 그렇기 때문에 현재까지 구한 목적지까지의 최소비용인 minDest보다 현재까지 구한 최소비용이 더 크다면 이 경로는 minDest를 업데이트 할 수 있는 경로가 아니므로 return 한다. 이렇게만 해주면 시간 초과 나지 않았다.

마찬가지로! DFS 도 방향까지 저장하여 최소비용을 업데이트 하는데에 3 차원 배열을 사용하였다.



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

맨 위로 이동하기

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

댓글 남기기