이 문제는 주어진 nxn 보드에 도로를 건설하는 것이 목표다. 최단거리, 최소비용으로 도착점에 도달해야한다.
const solution = (board) => {
const n = board.length
const Q = [[0,0,'',0]]
const directionAdds = [0, 1, 3]
const checkPush = ([x,y]) => {
return x >= 0 && x < n && y >= 0 && y < n && !(x === 0 && y === 0) && board[y][x] !== 1
}
while (Q.length) {
let [x,y,direction,price] = Q[0]
if (board[y][x] >= price || board[y][x] === 0) {
board[y][x] = price
directionAdds.forEach(n => {
let D = (direction+n)%4
let a = D === 0 ? x+1 : (D === 2 ? x-1 : x)
let b = D === 1 ? y+1 : (D === 3 ? y-1 : y)
if (checkPush([a,b])) {
Q.push([a,b, D, n === 0 || direction === '' ? price + 100 : price + 600])
}
})
}
Q.shift()
}
return board[n-1][n-1]
}
경주로 건설 문제
건설회사의 설계사인 죠르디는 고객사로부터 자동차 경주로 건설에 필요한 견적을 의뢰받았습니다. 제공된 경주로 설계 도면에 따르면 경주로 부지는 N x N 크기의 정사각형 격자 형태이며 각 격자는 1 x 1 크기입니다. 설계 도면에는 각 격자의 칸은 0 또는 1 로 채워져 있으며, 0은 칸이 비어 있음을 1은 해당 칸이 벽으로 채워져 있음을 나타냅니다.
경주로의 출발점은 (0, 0) 칸(좌측 상단)이며, 도착점은 (N-1, N-1) 칸(우측 하단)입니다. 죠르디는 출발점인 (0, 0) 칸에서 출발한 자동차가 도착점인 (N-1, N-1) 칸까지 무사히 도달할 수 있게 중간에 끊기지 않도록 경주로를 건설해야 합니다. 경주로는 상, 하, 좌, 우로 인접한 두 빈 칸을 연결하여 건설할 수 있으며, 벽이 있는 칸에는 경주로를 건설할 수 없습니다.
이때, 인접한 두 빈 칸을 상하 또는 좌우로 연결한 경주로를 직선 도로 라고 합니다.
또한 두 직선 도로가 서로 직각으로 만나는 지점을 코너 라고 부릅니다.
건설 비용을 계산해 보니 직선 도로 하나를 만들 때는 100원이 소요되며, 코너를 하나 만들 때는 500원이 추가로 듭니다. 죠르디는 견적서 작성을 위해 경주로를 건설하는 데 필요한 최소 비용을 계산해야 합니다.
예를 들어 아래 그림은 직선 도로 6개와 코너 4개로 구성된 임의의 경주로 예시이며, 건설 비용은 6 x 100 + 4 x 500 = 2600원 입니다.
또 다른 예로, 아래 그림은 직선 도로 4개와 코너 1개로 구성된 경주로이며, 건설 비용은 4 x 100 + 1 x 500 = 900원 입니다.
도면의 상태(0은 비어 있음, 1은 벽)을 나타내는 2차원 배열 board가 매개변수로 주어질 때, 경주로를 건설하는데 필요한 최소 비용을 return 하도록 solution 함수를 완성해주세요.
[제한사항]
입출력 예에 대한 설명
■ 입출력 예 #1
본문의 예시와 같습니다.
■ 입출력 예 #2
위와 같이 경주로를 건설하면 직선 도로 18개, 코너 4개로 총 3800원이 듭니다.
■ 입출력 예 #3
위와 같이 경주로를 건설하면 직선 도로 6개, 코너 3개로 총 2100원이 듭니다.
■ 입출력 예 #4
붉은색 경로와 같이 경주로를 건설하면 직선 도로 12개, 코너 4개로 총 3200원이 듭니다.
만약, 파란색 경로와 같이 경주로를 건설한다면 직선 도로 10개, 코너 5개로 총 3500원이 들며, 더 많은 비용이 듭니다.
일단 기본으로 주어지는 장애물 없는 3x3칸을 살펴보자. 이 경우 한번만 꺾어 도착지점에 도달하는 900원이 답이된다.
다만 (2, 0)을 지나 도착지점에 갈 수도 있고 (0, 2)를 지나 도착지점에 갈 수도 있다.
하지만 이 외에도 무수히 많은 경로가 존재할 수 있다. 이 모든 경로들을 탐색해서 위처럼 최소비용으로 건설할 수 있는 방법을 찾아야한다.
따라서 모든 경로들을 탐색할 필요가 있다.
여기서 BFS(Breadth First Search) 방식으로 모든 가능성을 살펴본다.
BFS는 가장 가까운 거리의 노드, 즉 지금 당장 한칸 이동으로 어디에 갈 수 있는지만 모두 파악한다. 위 문제에서 처음에 갈 수 있는 곳은 두가지다.
그렇다면 다음 방문 칸을 (1, 0)과 (0, 1)이라고 저장해보자.
다음 방문 칸을 (1, 0)이라고만 저장한다면 아래와 같은 문제가 발생한다.
따라서 칸의 좌표만 저장하는 것이 아니라 총 네 가지 정보를 저장하기로 한다.
1. 방문할 칸의 x좌표
2. 방문할 칸의 y좌표
3. 해당칸을 방문한 방향
4. 해당칸까지의 경주로 건설 비용
여기서 해당칸을 방문한 방향은 어떤칸에서 왔는지를 알려준다. 방향은 아래와 같이 0, 1, 2, 3의 숫자로 나타낸다.
x, y, D, price
[1, 0, 0, 100]
이제 가능한 모든 경우의 수를 목록(queue)에 저장해야한다.
[x좌표, y좌표, 최근방향, 현재까지 비용]
형식으로 가장 낮은 깊이부터 탐색 및 저장을 하게된다.
하지만 다음 탐색 칸이 보드 밖으로 넘어가거나 이미 왔던 길을 되돌아가는 등의 불필요한 탐색이 발생할 수 있으니 규칙을 정해야한다.
0 ≤ x < n
0 ≤ y < n
x = 0 AND y = 0 이면 안된다 ( 시작점으로 가면 안됨)
board[y][x] = 0 (1이면 장애물이 있다는 뜻으로 해당 칸에 경주로를 건설할 수 없다)
다음 방향을 설정하는데 있어 가장 중요한 것은 이미 왔던 길을 되돌아가면 안된다는 것이다.
방향이 0이었다면 다음 방향이 2가 되면 안된다
방향이 1이었다면 다음 방향이 3이 되면 안된다
방향이 2였다면 다음 방향이 0이 되면 안된다
방향이 3이었다면 다음 방향이 1이 되면 안된다.
되돌아가는 것만 제외한 방향을 구해보자
k = [0, 1, 3]
D_new = (D_old + k) % 4
D_old = 0;
D_new = (0 + 0) % 4 = 0; (k=0)
D_new = (0 + 1) % 4 = 1; (k=1)
D_new = (0 + 3) % 4 = 3; (k=3)
D_new = [0, 1, 3];
방문할 칸의 목록을 작성하는 방법은 알아봤다.
하지만 방문순서만으로는 정답 경로가 무엇인지 확인하기 어렵다.
때문에 보드판에 경주로 비용을 계속 입력해줘야한다.
다음과 같은 탐색 목록이 작성되었다고 해보자. 편의상 번호를 매겼다.
1. [1, 0, 0, 100]
2. [0, 1, 1, 100]
3. [2, 0, 0, 200]
4. [1, 1, 1, 700]
5. [0, 2, 1, 200]
6. [1, 1, 0, 700]
보드판에는 위와같은 비용들이 입력된다. 아직 탐색을 끝마치지 않았기 때문에 도착점까지의 비용은 입력되지않았다.
필요한 규칙을 모두 정했으니 이제 탐색을 시작해보자. BFS 방식으로 어느 칸에 갈지 탐색 목록을 만들고 각 이동을 보드판에 비용으로 기록할 것이다.
시작점에서 갈 수 있는 곳은 우측과 아래, 두가지뿐이다.
따라서 목록에는 [1, 0, 0, 100]
과 [0, 1, 1, 100]
이렇게 두가지만 저장된다.
목록은 Q 라는 배열을 만들어 관리하도록 하자.
Q = [
[1, 0, 0, 100],
[0, 1, 1, 100]
]
다음 이동은 1번과 2번에서 각각 뻗어나간다.
1번과 2번에서 각각 두가지 탐색을 진행했기 때문에 총 네가지의 이동이 목록에 추가된다.
Q = [
[1, 0, 0, 100],
[0, 1, 1, 100],
[2, 0, 0, 200],
[1, 1, 1, 700],
[0, 2, 1, 200],
[1, 1, 0, 700]
]
그리고 보드에 비용을 차례대로 입력해주면 된다.
탐색 목록은 링크에서 확인할 수 있다.
번호순으로 탐색해 목록 Q에 차례대로 저장하면서 보드판에 비용을 입력해준다.
최종적으로 도착지점에 입력된 비용이 경주로 건설의 최소비용이 된다.
참고한 코드는 아래와 같다.
const solution = (board) => {
const n = board.length
const Q = [[0,0,'',0]]
const Dadd = [0, 1, 3]
const checknext = ([x,y]) => {
return x >= 0 && x < n && y >= 0 && y < n && !(x === 0 && y === 0) && board[y][x] !== 1
}
while (Q.length) {
let [x,y,D,price] = Q[0]
if (board[y][x] >= price || board[y][x] === 0) {
board[y][x] = price
Dadd.forEach(k => {
let D = (D_old+n)%4
let a = D === 0 ? x+1 : (D === 2 ? x-1 : x)
let b = D === 1 ? y+1 : (D === 3 ? y-1 : y)
if (checknext([a,b])) {
Q.push([a,b, D, k === 0 || D_old === '' ? price + 100 : price + 600])
}
})
}
Q.shift()
}
return board[n-1][n-1]
}