프로그래머스 경주로 건설

임찬형·2022년 9월 19일
0

문제 팁

목록 보기
35/73

https://school.programmers.co.kr/learn/courses/30/lessons/67259

맨 왼쪽 위 (0, 0)위치에서 맨 오른쪽 아래 까지 최소 비용으로 경로를 생성하는 문제이다.

직선 도로는 100원, 코너는 500원의 비용이 든다.

위처럼 도로를 건설할 경우 직선도로 1~6까지 6개 + 코너(동그라미) 4개 = 600 + 2000 = 2600원의 비용이 든다.

cost를 기준으로 하는 Priority Queue를 이용한 bfs로 풀이를 시도했으나, 한 케이스에 대해 생각하지 못했고 이를 정리하려 한다.

풀이 방법은 아래와 같다.

enum class DIR{
    VERTICAL,
    HORIZONTAL
}

data class Pos(
    val v: Int,
    val h: Int,
    val dir: DIR,
    val cost: Int = 0
)

먼저 위처럼 bfs에 사용될 두 클래스를 생성한다.

DIR은 현재 세로로 이동하는 상태인지, 가로로 이동하는 상태인지를 나타낸다.

Pos는 현재 세로 위치(v), 가로 위치(h), 방향(DIR), 비용을 나타낸다.

시작점에서 세로로 출발하는 경우와 가로로 출발하는 경우를 각각 계산해서 더 적은 코스트를 정답으로 하였다.

bfs 도중 비용을 계산하는 방법은 다음과 같다.

현재 VERTICAL 상태일 경우 - 위 or 아래로 움직일 때 cost +100, 왼쪽 or 오른쪽으로 움직일 때 cost +600 (직선1 + 코너1).

현재 HORIZONTAL 상태일 경우 - 왼쪽 or 오른쪽으로 움직일 때 cost +100, 위 or 아래로 움직일 때 cost +600 (직선1 + 코너1)

이런 방법으로 bfs를 진행해 (N-1, N-1)에 도착하면 바로 정답으로 하려 했다.

하지만 이 방법에는 빈틈이 존재했다.

위 상태처럼 두 위치에서 (3, 2)위치를 확장하려고 하는 경우이다.

Priority Queue이므로 cost가 적은 3900(HOR) 상태를 먼저 확장하게 되지만, (4, 2)에는 cost 4000(VER)인 상태가 더 비용이 싸진다.

Cost값 뿐만아니라 그 상태에서의 HORIZONTAL, VERTICAL 상태도 영향을 줄 수 있는 것이다.

따라서 방문 체크에 대한 로직을 변경하기로 했다.

방문 체크를 이차원 Int배열 2개를 사용하여, HORIZONTAL 상태와 VERTICAL 상태의 Cost를 각각 저장하고 갱신하는 방법이다.

그렇게 하면 위 예시의 (3, 2)에 대해 visitedV[3][2]에는 4100 (Cost 4000 위치에서 아래로 이동) visitedH[3][2]에는 4000 (Cost 3900위치에서 우측으로 이동)이 각각 저장된다.

즉 (3, 2)에 대해 (3, 2, VER, 4100) 상태와 (3, 2, HOR, 4000) 상태가 모두 존재한다.

이후 (4, 2)로 확장할 때 (3, 2, VER, 4100)상태와 (3, 2, HOR, 4000)상태 모두 아래 방향(VER)으로 확장한다.

Priority Queue에 의해서 (3, 2, HOR, 4000)상태를 먼저 꺼내오게 되고 (4, 2)로 확장하면 (4, 2, VER, 4600)이 되어 visitedV[4][2] = 4600이 된다.

다음으로 (3, 2, VER, 4100)을 꺼내와 (4, 2)로 확장하면 (4, 2, VER, 4200)이 되어 visitedV[4][2] = 4200으로 갱신하게 된다.

다음으로 또 Priority Queue에 의해서, 4200 Cost인 케이스를 먼저 확장하게 되며, 4600 Cost인 케이스는 이후 4200 Cost로 확장한 케이스보다 비용이 더 들어 확장할 수 없게 되어 최적을 보장한다.

class Solution {
    val dirs = intArrayOf(-1, 0, 1, 0, -1)

    fun solution(board: Array<IntArray>): Int {
        return min(getSolution(DIR.VERTICAL, board), getSolution(DIR.HORIZONTAL, board))
    }

    fun getSolution(startDir: DIR, board: Array<IntArray>): Int{
        val N = board.size
        val pq = PriorityQueue<Pos>{p1, p2 -> p1.cost - p2.cost}
        val visitedV = Array(N){IntArray(N){Int.MAX_VALUE}}
        val visitedH = Array(N){IntArray(N){Int.MAX_VALUE}}
        pq.offer(Pos(0, 0, startDir))
        if(startDir == DIR.VERTICAL) visitedV[0][0] = 0 else visitedH[0][0] = 0

        var smallest = Int.MAX_VALUE
        while(pq.isNotEmpty()){
            val next = pq.poll()
            if(next.dir == DIR.VERTICAL && visitedV[next.v][next.h] < next.cost) continue
            if(next.dir == DIR.HORIZONTAL && visitedH[next.v][next.h] < next.cost) continue
            if(next.dir == DIR.VERTICAL)
                visitedV[next.v][next.h] = next.cost
            else
                visitedH[next.v][next.h] = next.cost

            if(next.v == N - 1 && next.h == N - 1){
                if(smallest > next.cost)
                    smallest = next.cost
            }

            for(i in 0..3){
                // 0위 1오른 2아래 3왼
                val nv = next.v + dirs[i]
                val nh = next.h + dirs[i + 1]

                if(isInside(nv, nh, N) && board[nv][nh] != 1){
                    when{
                        i % 2 == 0 && next.dir == DIR.VERTICAL -> pq.offer(Pos(nv, nh, DIR.VERTICAL, next.cost + 100))
                        i % 2 == 1 && next.dir == DIR.HORIZONTAL -> pq.offer(Pos(nv, nh, DIR.HORIZONTAL, next.cost + 100))
                        i % 2 == 0 && next.dir == DIR.HORIZONTAL -> pq.offer(Pos(nv, nh, DIR.VERTICAL, next.cost + 600))
                        i % 2 == 1 && next.dir == DIR.VERTICAL -> pq.offer(Pos(nv, nh, DIR.HORIZONTAL, next.cost + 600))
                    }
                }
            }
        }

        return smallest
    }

    private fun isInside(v: Int, h: Int, N: Int) = v in 0 until N && h in 0 until N
}

0개의 댓글

관련 채용 정보